mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-06-02 00:57:41 +08:00
feat(channel): add Discord channel
This commit is contained in:
@@ -119,6 +119,9 @@ A single Agent instance can serve multiple channels in parallel. Most channels c
|
|||||||
| Channel | Text | Image | File | Voice | Group |
|
| Channel | Text | Image | File | Voice | Group |
|
||||||
| --- | :-: | :-: | :-: | :-: | :-: |
|
| --- | :-: | :-: | :-: | :-: | :-: |
|
||||||
| [Web Console](https://docs.cowagent.ai/en/channels/web) (default) | ✅ | ✅ | ✅ | ✅ | |
|
| [Web Console](https://docs.cowagent.ai/en/channels/web) (default) | ✅ | ✅ | ✅ | ✅ | |
|
||||||
|
| [Telegram](https://docs.cowagent.ai/en/channels/telegram) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| [Slack](https://docs.cowagent.ai/en/channels/slack) | ✅ | ✅ | ✅ | | ✅ |
|
||||||
|
| [Discord](https://docs.cowagent.ai/en/channels/discord) | ✅ | ✅ | ✅ | | ✅ |
|
||||||
| [WeChat](https://docs.cowagent.ai/en/channels/weixin) | ✅ | ✅ | ✅ | ✅ | |
|
| [WeChat](https://docs.cowagent.ai/en/channels/weixin) | ✅ | ✅ | ✅ | ✅ | |
|
||||||
| [Feishu / Lark](https://docs.cowagent.ai/en/channels/feishu) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
| [Feishu / Lark](https://docs.cowagent.ai/en/channels/feishu) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
| [DingTalk](https://docs.cowagent.ai/en/channels/dingtalk) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
| [DingTalk](https://docs.cowagent.ai/en/channels/dingtalk) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
@@ -127,8 +130,6 @@ A single Agent instance can serve multiple channels in parallel. Most channels c
|
|||||||
| [WeCom App](https://docs.cowagent.ai/en/channels/wecom) | ✅ | ✅ | ✅ | ✅ | |
|
| [WeCom App](https://docs.cowagent.ai/en/channels/wecom) | ✅ | ✅ | ✅ | ✅ | |
|
||||||
| [WeChat Customer Service](https://docs.cowagent.ai/en/channels/wechat-kf) | ✅ | ✅ | ✅ | ✅ | |
|
| [WeChat Customer Service](https://docs.cowagent.ai/en/channels/wechat-kf) | ✅ | ✅ | ✅ | ✅ | |
|
||||||
| [WeChat Official Account](https://docs.cowagent.ai/en/channels/wechatmp) | ✅ | ✅ | | ✅ | |
|
| [WeChat Official Account](https://docs.cowagent.ai/en/channels/wechatmp) | ✅ | ✅ | | ✅ | |
|
||||||
| [Telegram](https://docs.cowagent.ai/en/channels/telegram) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
||||||
| [Slack](https://docs.cowagent.ai/en/channels/slack) | ✅ | ✅ | ✅ | | ✅ |
|
|
||||||
|
|
||||||
> See the [Channels overview](https://docs.cowagent.ai/en/channels/index) for setup details.
|
> See the [Channels overview](https://docs.cowagent.ai/en/channels/index) for setup details.
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ def create_channel(channel_type) -> Channel:
|
|||||||
elif channel_type == const.SLACK:
|
elif channel_type == const.SLACK:
|
||||||
from channel.slack.slack_channel import SlackChannel
|
from channel.slack.slack_channel import SlackChannel
|
||||||
ch = SlackChannel()
|
ch = SlackChannel()
|
||||||
|
elif channel_type == const.DISCORD:
|
||||||
|
from channel.discord.discord_channel import DiscordChannel
|
||||||
|
ch = DiscordChannel()
|
||||||
elif channel_type in (const.WEIXIN, "wx"):
|
elif channel_type in (const.WEIXIN, "wx"):
|
||||||
from channel.weixin.weixin_channel import WeixinChannel
|
from channel.weixin.weixin_channel import WeixinChannel
|
||||||
ch = WeixinChannel()
|
ch = WeixinChannel()
|
||||||
|
|||||||
0
channel/discord/__init__.py
Normal file
0
channel/discord/__init__.py
Normal file
500
channel/discord/discord_channel.py
Normal file
500
channel/discord/discord_channel.py
Normal file
@@ -0,0 +1,500 @@
|
|||||||
|
"""
|
||||||
|
Discord channel via the Gateway (WebSocket) using discord.py.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Direct message & guild channel chat (text / image / file)
|
||||||
|
- Guild trigger: @mention or reply-to-bot (configurable)
|
||||||
|
- /cancel fast-path matches Web channel behaviour
|
||||||
|
- Gateway long connection: no public IP / callback URL required, works behind NAT
|
||||||
|
|
||||||
|
Implementation note:
|
||||||
|
discord.py is async-first. We run the client inside a dedicated thread
|
||||||
|
with its own asyncio loop so the rest of cow (which is sync) stays
|
||||||
|
untouched. Inbound messages are dispatched onto cow's existing sync
|
||||||
|
ChatChannel.produce() pipeline; outbound send() schedules coroutines
|
||||||
|
back onto that loop via asyncio.run_coroutine_threadsafe.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from bridge.context import Context, ContextType
|
||||||
|
from bridge.reply import Reply, ReplyType
|
||||||
|
from channel.chat_channel import ChatChannel, check_prefix
|
||||||
|
from channel.discord.discord_message import DiscordMessage
|
||||||
|
from common.expired_dict import ExpiredDict
|
||||||
|
from common.log import logger
|
||||||
|
from common.singleton import singleton
|
||||||
|
from config import conf
|
||||||
|
|
||||||
|
# Discord caps a single message at 2000 chars; split conservatively below.
|
||||||
|
DISCORD_MSG_LIMIT = 1900
|
||||||
|
|
||||||
|
|
||||||
|
@singleton
|
||||||
|
class DiscordChannel(ChatChannel):
|
||||||
|
NOT_SUPPORT_REPLYTYPE = []
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.bot_token = ""
|
||||||
|
self.bot_user_id = "" # used to strip @mention and ignore self messages
|
||||||
|
self.bot_username = ""
|
||||||
|
self._client = None
|
||||||
|
self._loop = None
|
||||||
|
self._loop_thread = None
|
||||||
|
self._stop_event = threading.Event()
|
||||||
|
# Idempotent dedup; guard against rare duplicate dispatch
|
||||||
|
self._received_msgs = ExpiredDict(60 * 60 * 1)
|
||||||
|
|
||||||
|
# Disable group whitelist / prefix checks (we handle triggering ourselves
|
||||||
|
# in _should_reply_in_guild), aligned with telegram / slack channels.
|
||||||
|
conf()["group_name_white_list"] = ["ALL_GROUP"]
|
||||||
|
conf()["single_chat_prefix"] = [""]
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Lifecycle
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def startup(self):
|
||||||
|
self.bot_token = conf().get("discord_token", "")
|
||||||
|
if not self.bot_token:
|
||||||
|
err = "[Discord] discord_token is required"
|
||||||
|
logger.error(err)
|
||||||
|
self.report_startup_error(err)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
import discord
|
||||||
|
except ImportError:
|
||||||
|
err = (
|
||||||
|
"[Discord] discord.py is not installed. "
|
||||||
|
"Run: pip install discord.py"
|
||||||
|
)
|
||||||
|
logger.error(err)
|
||||||
|
self.report_startup_error(err)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Run the asyncio event loop in a dedicated thread so the sync cow body
|
||||||
|
# is untouched.
|
||||||
|
self._loop = asyncio.new_event_loop()
|
||||||
|
|
||||||
|
def _run_loop():
|
||||||
|
asyncio.set_event_loop(self._loop)
|
||||||
|
try:
|
||||||
|
self._loop.run_until_complete(self._async_main(discord))
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[Discord] event loop crashed: {e}", exc_info=True)
|
||||||
|
self.report_startup_error(str(e))
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
self._loop.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
logger.info("[Discord] event loop exited")
|
||||||
|
|
||||||
|
self._loop_thread = threading.Thread(target=_run_loop, daemon=True, name="discord-loop")
|
||||||
|
self._loop_thread.start()
|
||||||
|
# Block startup() until the loop thread exits, matching other channels'
|
||||||
|
# behaviour (startup is a blocking call).
|
||||||
|
self._loop_thread.join()
|
||||||
|
|
||||||
|
async def _async_main(self, discord):
|
||||||
|
"""Build the discord client, register handlers, and connect to the Gateway."""
|
||||||
|
# message_content is a privileged intent; it must be enabled in the
|
||||||
|
# Developer Portal (Bot -> Privileged Gateway Intents) to read text.
|
||||||
|
intents = discord.Intents.default()
|
||||||
|
intents.message_content = True
|
||||||
|
client = discord.Client(intents=intents)
|
||||||
|
self._client = client
|
||||||
|
|
||||||
|
channel = self
|
||||||
|
|
||||||
|
@client.event
|
||||||
|
async def on_ready():
|
||||||
|
channel.bot_user_id = str(client.user.id)
|
||||||
|
channel.bot_username = client.user.name or ""
|
||||||
|
channel.name = channel.bot_user_id # ChatChannel uses self.name to strip @-mention
|
||||||
|
logger.info(f"[Discord] Bot logged in as {client.user} (id={client.user.id})")
|
||||||
|
channel.report_startup_success()
|
||||||
|
logger.info("[Discord] ✅ Discord bot ready, listening for messages")
|
||||||
|
|
||||||
|
@client.event
|
||||||
|
async def on_message(message):
|
||||||
|
await channel._on_message(message)
|
||||||
|
|
||||||
|
# Connect to the Gateway; discord.py auto-reconnects on transient errors.
|
||||||
|
logger.info("[Discord] Connecting to Gateway...")
|
||||||
|
|
||||||
|
# client.start() handles login + Gateway connection and runs until
|
||||||
|
# close(); it is the standard entrypoint across discord.py versions.
|
||||||
|
runner_task = asyncio.create_task(client.start(self.bot_token))
|
||||||
|
|
||||||
|
# Block until stop()
|
||||||
|
try:
|
||||||
|
while not self._stop_event.is_set():
|
||||||
|
if runner_task.done():
|
||||||
|
# Surface a startup/connection failure (e.g. bad token)
|
||||||
|
exc = runner_task.exception()
|
||||||
|
if exc:
|
||||||
|
logger.error(f"[Discord] client stopped: {exc}", exc_info=exc)
|
||||||
|
self.report_startup_error(str(exc))
|
||||||
|
break
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
if not client.is_closed():
|
||||||
|
await client.close()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[Discord] shutdown error: {e}")
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
logger.info("[Discord] stop() called")
|
||||||
|
self._stop_event.set()
|
||||||
|
if self._loop_thread and self._loop_thread.is_alive():
|
||||||
|
try:
|
||||||
|
self._loop_thread.join(timeout=10)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
logger.info("[Discord] stop() completed")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Inbound: discord message -> ChatMessage -> ChatChannel.produce
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def _on_message(self, message):
|
||||||
|
"""Discord message entry: parse -> build ChatMessage -> produce()."""
|
||||||
|
try:
|
||||||
|
# Ignore our own messages and other bots. self._client.user may be
|
||||||
|
# None until on_ready completes, so guard against that.
|
||||||
|
if self._client and self._client.user and message.author.id == self._client.user.id:
|
||||||
|
return
|
||||||
|
if message.author.bot:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Idempotent dedup
|
||||||
|
msg_uid = f"{message.channel.id}:{message.id}"
|
||||||
|
if self._received_msgs.get(msg_uid):
|
||||||
|
return
|
||||||
|
self._received_msgs[msg_uid] = True
|
||||||
|
|
||||||
|
# guild is None for DMs
|
||||||
|
is_group = message.guild is not None
|
||||||
|
|
||||||
|
# Guild trigger gate (silently drop if not triggered)
|
||||||
|
if is_group and not self._should_reply_in_guild(message):
|
||||||
|
logger.debug(f"[Discord] guild message not triggered (need @mention or reply), skip")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Parse message type + download attachments if needed.
|
||||||
|
ctype, content, caption = await self._parse_message(message)
|
||||||
|
if ctype is None:
|
||||||
|
logger.debug(f"[Discord] unsupported message type, skip. msg_id={message.id}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Strip the bot mention from guild text/caption
|
||||||
|
if is_group:
|
||||||
|
if ctype == ContextType.TEXT and content:
|
||||||
|
content = self._strip_at_mention(content)
|
||||||
|
if caption:
|
||||||
|
caption = self._strip_at_mention(caption)
|
||||||
|
|
||||||
|
dc_msg = DiscordMessage(
|
||||||
|
message,
|
||||||
|
is_group=is_group,
|
||||||
|
bot_user_id=self.bot_user_id,
|
||||||
|
ctype=ctype,
|
||||||
|
content=content,
|
||||||
|
)
|
||||||
|
dc_msg.is_at = is_group # if we reached here in a guild, bot is mentioned/replied
|
||||||
|
|
||||||
|
from channel.file_cache import get_file_cache
|
||||||
|
file_cache = get_file_cache()
|
||||||
|
session_id = self._compute_session_id(message, is_group)
|
||||||
|
|
||||||
|
# Media + caption together: treat as a complete query and bypass the cache
|
||||||
|
if ctype in (ContextType.IMAGE, ContextType.FILE) and caption:
|
||||||
|
tag = "image" if ctype == ContextType.IMAGE else "file"
|
||||||
|
merged_text = f"{caption}\n[{tag}: {content}]"
|
||||||
|
dc_msg.ctype = ContextType.TEXT
|
||||||
|
dc_msg.content = merged_text
|
||||||
|
ctype = ContextType.TEXT
|
||||||
|
logger.info(f"[Discord] Media+caption merged for session {session_id}")
|
||||||
|
# fallthrough to the TEXT branch below
|
||||||
|
|
||||||
|
elif ctype == ContextType.IMAGE:
|
||||||
|
file_cache.add(session_id, content, file_type="image")
|
||||||
|
logger.info(f"[Discord] Image cached for session {session_id}, waiting for query...")
|
||||||
|
return
|
||||||
|
elif ctype == ContextType.FILE:
|
||||||
|
file_cache.add(session_id, content, file_type="file")
|
||||||
|
logger.info(f"[Discord] File cached for session {session_id}: {content}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if ctype == ContextType.TEXT:
|
||||||
|
# Fast-path: /cancel mirrors Web channel behaviour
|
||||||
|
if (content or "").strip().lower() in ("/cancel", "cancel"):
|
||||||
|
await self._do_cancel(session_id, message)
|
||||||
|
return
|
||||||
|
|
||||||
|
cached_files = file_cache.get(session_id)
|
||||||
|
if cached_files:
|
||||||
|
refs = []
|
||||||
|
for fi in cached_files:
|
||||||
|
ftype = fi["type"]
|
||||||
|
tag = ftype if ftype in ("image", "video") else "file"
|
||||||
|
refs.append(f"[{tag}: {fi['path']}]")
|
||||||
|
dc_msg.content = (dc_msg.content or "") + "\n" + "\n".join(refs)
|
||||||
|
file_cache.clear(session_id)
|
||||||
|
logger.info(f"[Discord] Attached {len(cached_files)} cached file(s) to query")
|
||||||
|
|
||||||
|
context = self._compose_context(
|
||||||
|
dc_msg.ctype,
|
||||||
|
dc_msg.content,
|
||||||
|
isgroup=is_group,
|
||||||
|
msg=dc_msg,
|
||||||
|
# Replies use Discord's reply mechanism, no manual @mention needed
|
||||||
|
no_need_at=True,
|
||||||
|
)
|
||||||
|
if context:
|
||||||
|
context["session_id"] = session_id
|
||||||
|
context["receiver"] = str(message.channel.id)
|
||||||
|
context["discord_channel_id"] = message.channel.id
|
||||||
|
context["discord_reply_to_msg_id"] = message.id if is_group else None
|
||||||
|
self.produce(context)
|
||||||
|
logger.debug(f"[Discord] received: type={ctype}, content={str(dc_msg.content)[:80]}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[Discord] _on_message error: {e}", exc_info=True)
|
||||||
|
|
||||||
|
async def _do_cancel(self, session_id: str, message):
|
||||||
|
"""Fast-path: /cancel calls cancel_session directly without going through agent."""
|
||||||
|
try:
|
||||||
|
from agent.protocol import get_cancel_registry
|
||||||
|
cancelled = get_cancel_registry().cancel_session(session_id)
|
||||||
|
text = "Current task cancelled." if cancelled else "No running task to cancel."
|
||||||
|
await message.channel.send(text)
|
||||||
|
logger.info(f"[Discord] /cancel session={session_id}, cancelled={cancelled}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[Discord] /cancel error: {e}", exc_info=True)
|
||||||
|
|
||||||
|
async def _parse_message(self, message):
|
||||||
|
"""Parse a discord message and return (ctype, content, caption).
|
||||||
|
|
||||||
|
- content is text for ContextType.TEXT, otherwise the local file path
|
||||||
|
- caption is the optional text accompanying an attachment; empty for plain text
|
||||||
|
"""
|
||||||
|
text = (message.content or "").strip()
|
||||||
|
attachments = message.attachments or []
|
||||||
|
|
||||||
|
if attachments:
|
||||||
|
# Handle the first attachment; caption is the accompanying message text
|
||||||
|
att = attachments[0]
|
||||||
|
content_type = (att.content_type or "").lower()
|
||||||
|
name = att.filename or str(att.id)
|
||||||
|
path = await self._download_attachment(att, name)
|
||||||
|
if not path:
|
||||||
|
return (None, None, "")
|
||||||
|
is_image = content_type.startswith("image/") or name.lower().endswith(
|
||||||
|
(".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp")
|
||||||
|
)
|
||||||
|
if is_image:
|
||||||
|
return (ContextType.IMAGE, path, text)
|
||||||
|
return (ContextType.FILE, path, text)
|
||||||
|
|
||||||
|
if text:
|
||||||
|
return (ContextType.TEXT, text, "")
|
||||||
|
|
||||||
|
return (None, None, "")
|
||||||
|
|
||||||
|
async def _download_attachment(self, attachment, name: str):
|
||||||
|
"""Download a discord attachment into the local tmp dir; return path or None."""
|
||||||
|
try:
|
||||||
|
tmp_dir = DiscordMessage.get_tmp_dir()
|
||||||
|
safe_name = re.sub(r"[^\w.\-]", "_", name)
|
||||||
|
# Prefix with attachment id to avoid name collisions
|
||||||
|
local_path = os.path.join(tmp_dir, f"{attachment.id}_{safe_name}")
|
||||||
|
await attachment.save(local_path)
|
||||||
|
logger.debug(f"[Discord] downloaded {name} -> {local_path}")
|
||||||
|
return local_path
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[Discord] download_attachment failed ({name}): {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Guild trigger logic
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _should_reply_in_guild(self, message) -> bool:
|
||||||
|
"""Decide whether to reply to a guild channel message based on configuration."""
|
||||||
|
mode = conf().get("discord_group_trigger", "mention_or_reply")
|
||||||
|
if mode == "all":
|
||||||
|
return True
|
||||||
|
|
||||||
|
# self._client.user may be None until on_ready completes
|
||||||
|
if not self._client or not self._client.user:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 1) Mentioned (direct @bot, not @everyone / @role)
|
||||||
|
if self._client.user in message.mentions:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 2) Reply to a bot message
|
||||||
|
if mode == "mention_or_reply":
|
||||||
|
ref = message.reference
|
||||||
|
resolved = getattr(ref, "resolved", None) if ref else None
|
||||||
|
if resolved and getattr(resolved, "author", None):
|
||||||
|
if resolved.author.id == self._client.user.id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _strip_at_mention(self, content: str) -> str:
|
||||||
|
"""Strip <@BOT_ID> / <@!BOT_ID> from guild text."""
|
||||||
|
if not content or not self.bot_user_id:
|
||||||
|
return content
|
||||||
|
pattern = re.compile(r"<@!?" + re.escape(self.bot_user_id) + r">")
|
||||||
|
return pattern.sub("", content).strip()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _compute_session_id(message, is_group: bool) -> str:
|
||||||
|
channel_id = message.channel.id
|
||||||
|
user_id = message.author.id
|
||||||
|
if is_group:
|
||||||
|
if conf().get("group_shared_session", True):
|
||||||
|
return f"discord_channel_{channel_id}"
|
||||||
|
return f"discord_channel_{channel_id}_{user_id}"
|
||||||
|
return f"discord_user_{user_id}"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Override _compose_context: skip the parent's group whitelist/at checks
|
||||||
|
# (already handled via _should_reply_in_guild). Same idea as telegram / slack.
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _compose_context(self, ctype: ContextType, content, **kwargs):
|
||||||
|
context = Context(ctype, content)
|
||||||
|
context.kwargs = kwargs
|
||||||
|
if "channel_type" not in context:
|
||||||
|
context["channel_type"] = self.channel_type
|
||||||
|
if "origin_ctype" not in context:
|
||||||
|
context["origin_ctype"] = ctype
|
||||||
|
|
||||||
|
cmsg = context["msg"]
|
||||||
|
if cmsg.is_group:
|
||||||
|
if conf().get("group_shared_session", True):
|
||||||
|
context["session_id"] = cmsg.other_user_id
|
||||||
|
else:
|
||||||
|
context["session_id"] = f"{cmsg.from_user_id}:{cmsg.other_user_id}"
|
||||||
|
else:
|
||||||
|
context["session_id"] = cmsg.from_user_id
|
||||||
|
context["receiver"] = cmsg.other_user_id
|
||||||
|
|
||||||
|
if ctype == ContextType.TEXT:
|
||||||
|
img_match_prefix = check_prefix(content, conf().get("image_create_prefix"))
|
||||||
|
if img_match_prefix:
|
||||||
|
content = content.replace(img_match_prefix, "", 1)
|
||||||
|
context.type = ContextType.IMAGE_CREATE
|
||||||
|
else:
|
||||||
|
context.type = ContextType.TEXT
|
||||||
|
context.content = (content or "").strip()
|
||||||
|
if "desire_rtype" not in context and conf().get("always_reply_voice"):
|
||||||
|
context["desire_rtype"] = ReplyType.VOICE
|
||||||
|
elif ctype == ContextType.VOICE:
|
||||||
|
if "desire_rtype" not in context and (
|
||||||
|
conf().get("voice_reply_voice") or conf().get("always_reply_voice")
|
||||||
|
):
|
||||||
|
context["desire_rtype"] = ReplyType.VOICE
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Outbound: ChatChannel.send -> Discord Gateway/REST
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def send(self, reply: Reply, context: Context):
|
||||||
|
"""Called from cow's sync main thread; marshal the coroutine onto the loop thread."""
|
||||||
|
if self._loop is None or self._client is None:
|
||||||
|
logger.warning("[Discord] client not ready, drop reply")
|
||||||
|
return
|
||||||
|
|
||||||
|
channel_id = context.get("discord_channel_id")
|
||||||
|
if channel_id is None:
|
||||||
|
logger.warning("[Discord] no discord_channel_id in context, drop reply")
|
||||||
|
return
|
||||||
|
|
||||||
|
coro = self._async_send(reply, channel_id)
|
||||||
|
try:
|
||||||
|
future = asyncio.run_coroutine_threadsafe(coro, self._loop)
|
||||||
|
future.result(timeout=180)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[Discord] send failed: {e}")
|
||||||
|
|
||||||
|
async def _async_send(self, reply: Reply, channel_id):
|
||||||
|
try:
|
||||||
|
import discord
|
||||||
|
|
||||||
|
channel = self._client.get_channel(channel_id)
|
||||||
|
if channel is None:
|
||||||
|
# Not in cache (e.g. DM channel); fetch it explicitly
|
||||||
|
channel = await self._client.fetch_channel(channel_id)
|
||||||
|
|
||||||
|
rtype = reply.type
|
||||||
|
content = reply.content
|
||||||
|
|
||||||
|
if rtype in (ReplyType.TEXT, ReplyType.INFO, ReplyType.ERROR):
|
||||||
|
text = str(content) if content is not None else ""
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
for chunk in _split_text(text, DISCORD_MSG_LIMIT):
|
||||||
|
await channel.send(chunk)
|
||||||
|
|
||||||
|
elif rtype == ReplyType.IMAGE:
|
||||||
|
# Already a local BytesIO; send it directly
|
||||||
|
content.seek(0)
|
||||||
|
await channel.send(file=discord.File(content, filename="image.png"))
|
||||||
|
|
||||||
|
elif rtype == ReplyType.IMAGE_URL:
|
||||||
|
url = str(content)
|
||||||
|
if url.startswith("file://"):
|
||||||
|
local = url[7:]
|
||||||
|
await channel.send(file=discord.File(local))
|
||||||
|
else:
|
||||||
|
# Post the URL as text; Discord will unfurl it as an image preview
|
||||||
|
await channel.send(url)
|
||||||
|
|
||||||
|
elif rtype in (ReplyType.VOICE, ReplyType.FILE):
|
||||||
|
local = content[7:] if isinstance(content, str) and content.startswith("file://") else content
|
||||||
|
caption = getattr(reply, "text_content", None) or None
|
||||||
|
await channel.send(content=caption, file=discord.File(local))
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Fallback: send as plain text
|
||||||
|
await channel.send(str(content))
|
||||||
|
|
||||||
|
logger.info(f"[Discord] sent reply (type={rtype}, channel={channel_id})")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[Discord] _async_send error: {e}", exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _split_text(text: str, limit: int):
|
||||||
|
"""Split long text preferring line breaks to keep markdown structure intact."""
|
||||||
|
if len(text) <= limit:
|
||||||
|
yield text
|
||||||
|
return
|
||||||
|
buf = []
|
||||||
|
size = 0
|
||||||
|
for line in text.splitlines(keepends=True):
|
||||||
|
if size + len(line) > limit and buf:
|
||||||
|
yield "".join(buf)
|
||||||
|
buf, size = [], 0
|
||||||
|
# Hard-split single lines that exceed the limit
|
||||||
|
while len(line) > limit:
|
||||||
|
yield line[:limit]
|
||||||
|
line = line[limit:]
|
||||||
|
buf.append(line)
|
||||||
|
size += len(line)
|
||||||
|
if buf:
|
||||||
|
yield "".join(buf)
|
||||||
60
channel/discord/discord_message.py
Normal file
60
channel/discord/discord_message.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
"""
|
||||||
|
Discord message adapter.
|
||||||
|
|
||||||
|
Convert a discord.py Message into cow's unified ChatMessage.
|
||||||
|
File downloads are NOT performed here; the channel layer downloads
|
||||||
|
attachments on demand inside the async event loop.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
|
||||||
|
from bridge.context import ContextType
|
||||||
|
from channel.chat_message import ChatMessage
|
||||||
|
from common.utils import expand_path
|
||||||
|
from config import conf
|
||||||
|
|
||||||
|
|
||||||
|
class DiscordMessage(ChatMessage):
|
||||||
|
"""Wrap a discord.py Message into the unified ChatMessage."""
|
||||||
|
|
||||||
|
def __init__(self, message, is_group: bool = False, bot_user_id: str = "",
|
||||||
|
ctype: ContextType = ContextType.TEXT, content: str = ""):
|
||||||
|
super().__init__(message)
|
||||||
|
# Basic fields
|
||||||
|
self.msg_id = str(message.id)
|
||||||
|
self.create_time = int(message.created_at.timestamp()) if message.created_at else 0
|
||||||
|
self.ctype = ctype
|
||||||
|
self.content = content
|
||||||
|
|
||||||
|
author = message.author
|
||||||
|
channel = message.channel
|
||||||
|
|
||||||
|
# Sender / chat info
|
||||||
|
from_user_id = str(author.id)
|
||||||
|
from_user_nick = getattr(author, "display_name", None) or getattr(author, "name", None) or from_user_id
|
||||||
|
self.from_user_id = from_user_id
|
||||||
|
self.from_user_nickname = from_user_nick
|
||||||
|
self.to_user_id = bot_user_id or "discord_bot"
|
||||||
|
self.to_user_nickname = bot_user_id or "discord_bot"
|
||||||
|
|
||||||
|
self.is_group = is_group
|
||||||
|
if is_group:
|
||||||
|
# Guild channel: other_user_id = channel_id, actual_user_id = sender id
|
||||||
|
self.other_user_id = str(channel.id)
|
||||||
|
self.other_user_nickname = getattr(channel, "name", None) or str(channel.id)
|
||||||
|
self.actual_user_id = from_user_id
|
||||||
|
self.actual_user_nickname = from_user_nick
|
||||||
|
else:
|
||||||
|
# DM: use channel_id so replies go back to the same DM channel
|
||||||
|
self.other_user_id = str(channel.id)
|
||||||
|
self.other_user_nickname = from_user_nick
|
||||||
|
|
||||||
|
# Whether the bot was triggered by @-mention (set by channel layer)
|
||||||
|
self.is_at = False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_tmp_dir() -> str:
|
||||||
|
"""Local download directory, aligned with other channels (agent_workspace/tmp)."""
|
||||||
|
workspace_root = expand_path(conf().get("agent_workspace", "~/cow"))
|
||||||
|
tmp_dir = os.path.join(workspace_root, "tmp")
|
||||||
|
os.makedirs(tmp_dir, exist_ok=True)
|
||||||
|
return tmp_dir
|
||||||
@@ -2952,6 +2952,14 @@ class ChannelsHandler:
|
|||||||
{"key": "slack_app_token", "label": "App Token (xapp-)", "type": "secret"},
|
{"key": "slack_app_token", "label": "App Token (xapp-)", "type": "secret"},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
("discord", {
|
||||||
|
"label": {"zh": "Discord", "en": "Discord"},
|
||||||
|
"icon": "fa-discord",
|
||||||
|
"color": "indigo",
|
||||||
|
"fields": [
|
||||||
|
{"key": "discord_token", "label": "Bot Token", "type": "secret"},
|
||||||
|
],
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -247,3 +247,4 @@ WEIXIN = "weixin"
|
|||||||
WECHAT_KF = "wechat_kf"
|
WECHAT_KF = "wechat_kf"
|
||||||
TELEGRAM = "telegram"
|
TELEGRAM = "telegram"
|
||||||
SLACK = "slack"
|
SLACK = "slack"
|
||||||
|
DISCORD = "discord"
|
||||||
|
|||||||
364
config.py
364
config.py
@@ -8,211 +8,215 @@ import pickle
|
|||||||
|
|
||||||
from common.log import logger
|
from common.log import logger
|
||||||
|
|
||||||
# 将所有可用的配置项写在字典里, 请使用小写字母
|
# All available config keys are listed in this dict (use lowercase keys).
|
||||||
# 此处的配置值无实际意义,程序不会读取此处的配置,仅用于提示格式,请将配置加入到config.json中
|
# The values here are placeholders only; the program does NOT read them.
|
||||||
|
# They merely document the expected format — put real values in config.json.
|
||||||
available_setting = {
|
available_setting = {
|
||||||
# openai api配置
|
# openai api config
|
||||||
"open_ai_api_key": "", # openai api key
|
"open_ai_api_key": "", # openai api key
|
||||||
# openai apibase,当use_azure_chatgpt为true时,需要设置对应的api base
|
# openai api base; when use_azure_chatgpt is true, set the matching api base
|
||||||
"open_ai_api_base": "https://api.openai.com/v1",
|
"open_ai_api_base": "https://api.openai.com/v1",
|
||||||
"claude_api_base": "https://api.anthropic.com/v1", # claude api base
|
"claude_api_base": "https://api.anthropic.com/v1", # claude api base
|
||||||
"gemini_api_base": "https://generativelanguage.googleapis.com", # gemini api base
|
"gemini_api_base": "https://generativelanguage.googleapis.com", # gemini api base
|
||||||
"custom_api_key": "", # custom OpenAI-compatible provider api key (used when bot_type is "custom")
|
"custom_api_key": "", # custom OpenAI-compatible provider api key (used when bot_type is "custom")
|
||||||
"custom_api_base": "", # custom OpenAI-compatible provider api base (used when bot_type is "custom")
|
"custom_api_base": "", # custom OpenAI-compatible provider api base (used when bot_type is "custom")
|
||||||
"proxy": "", # openai使用的代理
|
"proxy": "", # proxy used by openai
|
||||||
# chatgpt模型, 当use_azure_chatgpt为true时,其名称为Azure上model deployment名称
|
# chatgpt model; when use_azure_chatgpt is true, this is the Azure model deployment name
|
||||||
"model": "gpt-3.5-turbo", # 可选择: gpt-4o, pt-4o-mini, gpt-4-turbo, claude-3-sonnet, wenxin, moonshot, qwen-turbo, xunfei, glm-4, minimax, gemini等模型,全部可选模型详见common/const.py文件
|
"model": "gpt-3.5-turbo", # options: gpt-4o, gpt-4o-mini, gpt-4-turbo, claude-3-sonnet, wenxin, moonshot, qwen-turbo, xunfei, glm-4, minimax, gemini, etc. See common/const.py for the full list
|
||||||
"bot_type": "", # 可选配置,使用兼容openai格式的三方服务时候,需填"openai"或"custom"(custom模式下切换模型不会自动切换bot_type)。bot具体名称详见common/const.py文件,如不填根据model名称判断
|
"bot_type": "", # optional; for OpenAI-compatible third-party services set "openai" or "custom" (in custom mode switching model won't auto-switch bot_type). See common/const.py for bot names; inferred from model name if left empty
|
||||||
"use_azure_chatgpt": False, # 是否使用azure的chatgpt
|
"use_azure_chatgpt": False, # whether to use Azure chatgpt
|
||||||
"azure_deployment_id": "", # azure 模型部署名称
|
"azure_deployment_id": "", # azure model deployment name
|
||||||
"azure_api_version": "", # azure api版本
|
"azure_api_version": "", # azure api version
|
||||||
# Bot触发配置
|
# Bot trigger config
|
||||||
"single_chat_prefix": ["bot", "@bot"], # 私聊时文本需要包含该前缀才能触发机器人回复
|
"single_chat_prefix": ["bot", "@bot"], # text must contain this prefix to trigger a reply in single chat
|
||||||
"single_chat_reply_prefix": "[bot] ", # 私聊时自动回复的前缀,用于区分真人
|
"single_chat_reply_prefix": "[bot] ", # auto-reply prefix in single chat, used to distinguish from a real person
|
||||||
"single_chat_reply_suffix": "", # 私聊时自动回复的后缀,\n 可以换行
|
"single_chat_reply_suffix": "", # auto-reply suffix in single chat; \n inserts a line break
|
||||||
"group_chat_prefix": ["@bot"], # 群聊时包含该前缀则会触发机器人回复
|
"group_chat_prefix": ["@bot"], # messages containing this prefix trigger a reply in group chat
|
||||||
"no_need_at": False, # 群聊回复时是否不需要艾特
|
"no_need_at": False, # whether replying in group chat does not require an @mention
|
||||||
"group_chat_reply_prefix": "", # 群聊时自动回复的前缀
|
"group_chat_reply_prefix": "", # auto-reply prefix in group chat
|
||||||
"group_chat_reply_suffix": "", # 群聊时自动回复的后缀,\n 可以换行
|
"group_chat_reply_suffix": "", # auto-reply suffix in group chat; \n inserts a line break
|
||||||
"group_chat_keyword": [], # 群聊时包含该关键词则会触发机器人回复
|
"group_chat_keyword": [], # messages containing this keyword trigger a reply in group chat
|
||||||
"group_at_off": False, # 是否关闭群聊时@bot的触发
|
"group_at_off": False, # whether to disable @bot triggering in group chat
|
||||||
"group_name_white_list": ["ChatGPT测试群", "ChatGPT测试群2"], # 开启自动回复的群名称列表
|
"group_name_white_list": ["group1", "group2"], # group names where auto-reply is enabled
|
||||||
"group_name_keyword_white_list": [], # 开启自动回复的群名称关键词列表
|
"group_name_keyword_white_list": [], # group-name keywords where auto-reply is enabled
|
||||||
"group_chat_in_one_session": ["ChatGPT测试群"], # 支持会话上下文共享的群名称
|
"group_chat_in_one_session": ["group1"], # group names that share conversation context
|
||||||
"group_shared_session": False, # 群聊是否共享会话上下文(所有成员共享)。False时每个用户在群内有独立会话
|
"group_shared_session": False, # whether group chat shares conversation context (all members share). When False each user has an independent session in the group
|
||||||
"nick_name_black_list": [], # 用户昵称黑名单
|
"nick_name_black_list": [], # user nickname blacklist
|
||||||
"group_welcome_msg": "", # 配置新人进群固定欢迎语,不配置则使用随机风格欢迎
|
"group_welcome_msg": "", # fixed welcome message for new group members; uses a random style when empty
|
||||||
"trigger_by_self": False, # 是否允许机器人触发
|
"trigger_by_self": False, # whether the bot can be triggered by itself
|
||||||
"text_to_image": "dall-e-2", # 图片生成模型,可选 dall-e-2, dall-e-3
|
"text_to_image": "dall-e-2", # image generation model, options: dall-e-2, dall-e-3
|
||||||
# Azure OpenAI dall-e-3 配置
|
# Azure OpenAI dall-e-3 config
|
||||||
"dalle3_image_style": "vivid", # 图片生成dalle3的风格,可选有 vivid, natural
|
"dalle3_image_style": "vivid", # dalle3 image style, options: vivid, natural
|
||||||
"dalle3_image_quality": "hd", # 图片生成dalle3的质量,可选有 standard, hd
|
"dalle3_image_quality": "hd", # dalle3 image quality, options: standard, hd
|
||||||
# Azure OpenAI DALL-E API 配置, 当use_azure_chatgpt为true时,用于将文字回复的资源和Dall-E的资源分开.
|
# Azure OpenAI DALL-E API config; when use_azure_chatgpt is true, separates the text-reply resource from the DALL-E resource
|
||||||
"azure_openai_dalle_api_base": "", # [可选] azure openai 用于回复图片的资源 endpoint,默认使用 open_ai_api_base
|
"azure_openai_dalle_api_base": "", # [optional] azure openai endpoint for image replies; defaults to open_ai_api_base
|
||||||
"azure_openai_dalle_api_key": "", # [可选] azure openai 用于回复图片的资源 key,默认使用 open_ai_api_key
|
"azure_openai_dalle_api_key": "", # [optional] azure openai key for image replies; defaults to open_ai_api_key
|
||||||
"azure_openai_dalle_deployment_id":"", # [可选] azure openai 用于回复图片的资源 deployment id,默认使用 text_to_image
|
"azure_openai_dalle_deployment_id":"", # [optional] azure openai deployment id for image replies; defaults to text_to_image
|
||||||
"image_proxy": True, # 是否需要图片代理,国内访问LinkAI时需要
|
"image_proxy": True, # whether an image proxy is needed; required when accessing LinkAI from mainland China
|
||||||
"image_create_prefix": ["画", "看", "找"], # 开启图片回复的前缀
|
"image_create_prefix": ["画", "看", "找"], # prefixes that enable image replies
|
||||||
"concurrency_in_session": 1, # 同一会话最多有多少条消息在处理中,大于1可能乱序
|
"concurrency_in_session": 1, # max number of in-flight messages per session; values >1 may cause out-of-order replies
|
||||||
"image_create_size": "256x256", # 图片大小,可选有 256x256, 512x512, 1024x1024 (dall-e-3默认为1024x1024)
|
"image_create_size": "256x256", # image size, options: 256x256, 512x512, 1024x1024 (dall-e-3 defaults to 1024x1024)
|
||||||
"group_chat_exit_group": False,
|
"group_chat_exit_group": False,
|
||||||
# chatgpt会话参数
|
# chatgpt session params
|
||||||
"expires_in_seconds": 3600, # 无操作会话的过期时间
|
"expires_in_seconds": 3600, # idle session expiry time
|
||||||
# 人格描述
|
# persona description (only used in chat mode)
|
||||||
"character_desc": "你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。",
|
"character_desc": "You are a helpful AI assistant. You aim to answer and solve any questions people have, and can communicate in multiple languages.",
|
||||||
"conversation_max_tokens": 1000, # 支持上下文记忆的最多字符数
|
"conversation_max_tokens": 1000, # max characters of context memory
|
||||||
# chatgpt限流配置
|
# chatgpt rate limit config
|
||||||
"rate_limit_chatgpt": 20, # chatgpt的调用频率限制
|
"rate_limit_chatgpt": 20, # chatgpt call rate limit
|
||||||
"rate_limit_dalle": 50, # openai dalle的调用频率限制
|
"rate_limit_dalle": 50, # openai dalle call rate limit
|
||||||
# chatgpt api参数 参考https://platform.openai.com/docs/api-reference/chat/create
|
# chatgpt api params, see https://platform.openai.com/docs/api-reference/chat/create
|
||||||
"temperature": 0.9,
|
"temperature": 0.9,
|
||||||
"top_p": 1,
|
"top_p": 1,
|
||||||
"frequency_penalty": 0,
|
"frequency_penalty": 0,
|
||||||
"presence_penalty": 0,
|
"presence_penalty": 0,
|
||||||
"request_timeout": 180, # chatgpt请求超时时间,openai接口默认设置为600,对于难问题一般需要较长时间
|
"request_timeout": 180, # chatgpt request timeout; the openai api defaults to 600, hard questions usually need longer
|
||||||
"timeout": 120, # chatgpt重试超时时间,在这个时间内,将会自动重试
|
"timeout": 120, # chatgpt retry timeout; will auto-retry within this window
|
||||||
# Baidu 文心一言参数
|
# Baidu Wenxin (ERNIE) params
|
||||||
"baidu_wenxin_model": "eb-instant", # 默认使用ERNIE-Bot-turbo模型
|
"baidu_wenxin_model": "eb-instant", # defaults to the ERNIE-Bot-turbo model
|
||||||
"baidu_wenxin_api_key": "", # Baidu api key
|
"baidu_wenxin_api_key": "", # Baidu api key
|
||||||
"baidu_wenxin_secret_key": "", # Baidu secret key
|
"baidu_wenxin_secret_key": "", # Baidu secret key
|
||||||
"baidu_wenxin_prompt_enabled": False, # Enable prompt if you are using ernie character model
|
"baidu_wenxin_prompt_enabled": False, # Enable prompt if you are using ernie character model
|
||||||
# Baidu Qianfan / ERNIE OpenAI-compatible API
|
# Baidu Qianfan / ERNIE OpenAI-compatible API
|
||||||
"qianfan_api_key": "", # Baidu Qianfan API key in bce-v3 format
|
"qianfan_api_key": "", # Baidu Qianfan API key in bce-v3 format
|
||||||
"qianfan_api_base": "https://qianfan.baidubce.com/v2", # Qianfan OpenAI-compatible API base
|
"qianfan_api_base": "https://qianfan.baidubce.com/v2", # Qianfan OpenAI-compatible API base
|
||||||
# 讯飞星火API
|
# Xunfei Spark API
|
||||||
"xunfei_app_id": "", # 讯飞应用ID
|
"xunfei_app_id": "", # Xunfei app id
|
||||||
"xunfei_api_key": "", # 讯飞 API key
|
"xunfei_api_key": "", # Xunfei API key
|
||||||
"xunfei_api_secret": "", # 讯飞 API secret
|
"xunfei_api_secret": "", # Xunfei API secret
|
||||||
"xunfei_domain": "", # 讯飞模型对应的domain参数,Spark4.0 Ultra为 4.0Ultra,其他模型详见: https://www.xfyun.cn/doc/spark/Web.html
|
"xunfei_domain": "", # Xunfei model domain param; for Spark4.0 Ultra it is 4.0Ultra, see https://www.xfyun.cn/doc/spark/Web.html for others
|
||||||
"xunfei_spark_url": "", # 讯飞模型对应的请求地址,Spark4.0 Ultra为 wss://spark-api.xf-yun.com/v4.0/chat,其他模型参考详见: https://www.xfyun.cn/doc/spark/Web.html
|
"xunfei_spark_url": "", # Xunfei model request url; for Spark4.0 Ultra it is wss://spark-api.xf-yun.com/v4.0/chat, see https://www.xfyun.cn/doc/spark/Web.html for others
|
||||||
# claude 配置
|
# claude config
|
||||||
"claude_api_cookie": "",
|
"claude_api_cookie": "",
|
||||||
"claude_uuid": "",
|
"claude_uuid": "",
|
||||||
# claude api key
|
# claude api key
|
||||||
"claude_api_key": "",
|
"claude_api_key": "",
|
||||||
# 通义千问API, 获取方式查看文档 https://help.aliyun.com/document_detail/2587494.html
|
# Tongyi Qianwen API, see https://help.aliyun.com/document_detail/2587494.html for how to obtain
|
||||||
"qwen_access_key_id": "",
|
"qwen_access_key_id": "",
|
||||||
"qwen_access_key_secret": "",
|
"qwen_access_key_secret": "",
|
||||||
"qwen_agent_key": "",
|
"qwen_agent_key": "",
|
||||||
"qwen_app_id": "",
|
"qwen_app_id": "",
|
||||||
"qwen_node_id": "", # 流程编排模型用到的id,如果没有用到qwen_node_id,请务必保持为空字符串
|
"qwen_node_id": "", # id used by workflow-orchestration models; keep it an empty string if qwen_node_id is unused
|
||||||
# 阿里灵积(通义新版sdk)模型api key
|
# Alibaba Lingji (Tongyi new sdk) model api key
|
||||||
"dashscope_api_key": "",
|
"dashscope_api_key": "",
|
||||||
# Google Gemini Api Key
|
# Google Gemini Api Key
|
||||||
"gemini_api_key": "",
|
"gemini_api_key": "",
|
||||||
# Embedding 模型设置
|
# Embedding model config
|
||||||
"embedding_provider": "", # 显式指定厂商:openai / linkai / dashscope / doubao / zhipu (与 bot_type 命名一致)
|
"embedding_provider": "", # explicitly set the provider: openai / linkai / dashscope / doubao / zhipu (aligned with bot_type naming)
|
||||||
"embedding_model": "", # 留空使用厂商默认 model
|
"embedding_model": "", # leave empty to use the provider's default model
|
||||||
"embedding_dimensions": 0, # 留空/0 使用厂商默认维度(推荐统一 1024)
|
"embedding_dimensions": 0, # leave empty/0 to use the provider's default dimension (1024 recommended for consistency)
|
||||||
# 语音设置
|
# voice config
|
||||||
"speech_recognition": True, # 是否开启语音识别
|
"speech_recognition": True, # whether to enable speech recognition
|
||||||
"group_speech_recognition": False, # 是否开启群组语音识别
|
"group_speech_recognition": False, # whether to enable group speech recognition
|
||||||
"voice_reply_voice": False, # 是否使用语音回复语音,需要设置对应语音合成引擎的api key
|
"voice_reply_voice": False, # whether to reply to voice with voice; requires the matching TTS engine api key
|
||||||
"always_reply_voice": False, # 是否一直使用语音回复
|
"always_reply_voice": False, # whether to always reply with voice
|
||||||
"voice_to_text": "openai", # 语音识别引擎,支持openai,baidu,google,azure,xunfei,ali
|
"voice_to_text": "openai", # speech recognition engine: openai,baidu,google,azure,xunfei,ali
|
||||||
"text_to_voice": "openai", # 语音合成引擎,支持openai,baidu,google,azure,xunfei,ali,pytts(offline),elevenlabs,edge(online)
|
"text_to_voice": "openai", # TTS engine: openai,baidu,google,azure,xunfei,ali,pytts(offline),elevenlabs,edge(online)
|
||||||
"text_to_voice_model": "tts-1",
|
"text_to_voice_model": "tts-1",
|
||||||
"tts_voice_id": "alloy",
|
"tts_voice_id": "alloy",
|
||||||
# baidu 语音api配置, 使用百度语音识别和语音合成时需要
|
# baidu voice api config; required when using Baidu speech recognition and TTS
|
||||||
"baidu_app_id": "",
|
"baidu_app_id": "",
|
||||||
"baidu_api_key": "",
|
"baidu_api_key": "",
|
||||||
"baidu_secret_key": "",
|
"baidu_secret_key": "",
|
||||||
# 1536普通话(支持简单的英文识别) 1737英语 1637粤语 1837四川话 1936普通话远场
|
# 1536 Mandarin (with basic English) 1737 English 1637 Cantonese 1837 Sichuanese 1936 Mandarin far-field
|
||||||
"baidu_dev_pid": 1536,
|
"baidu_dev_pid": 1536,
|
||||||
# azure 语音api配置, 使用azure语音识别和语音合成时需要
|
# azure voice api config; required when using Azure speech recognition and TTS
|
||||||
"azure_voice_api_key": "",
|
"azure_voice_api_key": "",
|
||||||
"azure_voice_region": "japaneast",
|
"azure_voice_region": "japaneast",
|
||||||
# elevenlabs 语音api配置
|
# elevenlabs voice api config
|
||||||
"xi_api_key": "", # 获取ap的方法可以参考https://docs.elevenlabs.io/api-reference/quick-start/authentication
|
"xi_api_key": "", # see https://docs.elevenlabs.io/api-reference/quick-start/authentication for how to obtain the api key
|
||||||
"xi_voice_id": "", # ElevenLabs提供了9种英式、美式等英语发音id,分别是“Adam/Antoni/Arnold/Bella/Domi/Elli/Josh/Rachel/Sam”
|
"xi_voice_id": "", # ElevenLabs offers 9 English voice ids: Adam/Antoni/Arnold/Bella/Domi/Elli/Josh/Rachel/Sam
|
||||||
# 服务时间限制
|
# service time limit
|
||||||
"chat_time_module": False, # 是否开启服务时间限制
|
"chat_time_module": False, # whether to enable service-time limiting
|
||||||
"chat_start_time": "00:00", # 服务开始时间
|
"chat_start_time": "00:00", # service start time
|
||||||
"chat_stop_time": "24:00", # 服务结束时间
|
"chat_stop_time": "24:00", # service stop time
|
||||||
# 翻译api
|
# translation api
|
||||||
"translate": "baidu", # 翻译api,支持baidu, youdao
|
"translate": "baidu", # translation api: baidu, youdao
|
||||||
# baidu翻译api的配置
|
# baidu translation api config
|
||||||
"baidu_translate_app_id": "", # 百度翻译api的appid
|
"baidu_translate_app_id": "", # baidu translation api appid
|
||||||
"baidu_translate_app_key": "", # 百度翻译api的秘钥
|
"baidu_translate_app_key": "", # baidu translation api secret key
|
||||||
# youdao翻译api的配置
|
# youdao translation api config
|
||||||
"youdao_translate_app_key": "", # 有道翻译api的应用ID
|
"youdao_translate_app_key": "", # youdao translation api app id
|
||||||
"youdao_translate_app_secret": "", # 有道翻译api的应用密钥
|
"youdao_translate_app_secret": "", # youdao translation api app secret
|
||||||
# wechatmp的配置
|
# wechatmp config
|
||||||
"wechatmp_token": "", # 微信公众平台的Token
|
"wechatmp_token": "", # WeChat Official Account token
|
||||||
"wechatmp_port": 8080, # 微信公众平台的端口,需要端口转发到80或443
|
"wechatmp_port": 8080, # WeChat Official Account port; needs port forwarding to 80 or 443
|
||||||
"wechatmp_app_id": "", # 微信公众平台的appID
|
"wechatmp_app_id": "", # WeChat Official Account appID
|
||||||
"wechatmp_app_secret": "", # 微信公众平台的appsecret
|
"wechatmp_app_secret": "", # WeChat Official Account appsecret
|
||||||
"wechatmp_aes_key": "", # 微信公众平台的EncodingAESKey,加密模式需要
|
"wechatmp_aes_key": "", # WeChat Official Account EncodingAESKey; required in encrypted mode
|
||||||
# wechatcom的通用配置
|
# wechatcom shared config
|
||||||
"wechatcom_corp_id": "", # 企业微信公司的corpID
|
"wechatcom_corp_id": "", # WeCom corp id
|
||||||
# wechatcomapp的配置
|
# wechatcomapp config
|
||||||
"wechatcomapp_token": "", # 企业微信app的token
|
"wechatcomapp_token": "", # WeCom app token
|
||||||
"wechatcomapp_port": 9898, # 企业微信app的服务端口,不需要端口转发
|
"wechatcomapp_port": 9898, # WeCom app service port; no port forwarding needed
|
||||||
"wechatcomapp_secret": "", # 企业微信app的secret
|
"wechatcomapp_secret": "", # WeCom app secret
|
||||||
"wechatcomapp_agent_id": "", # 企业微信app的agent_id
|
"wechatcomapp_agent_id": "", # WeCom app agent_id
|
||||||
"wechatcomapp_aes_key": "", # 企业微信app的aes_key
|
"wechatcomapp_aes_key": "", # WeCom app aes_key
|
||||||
# 微信客服(wechat_kf)的配置
|
# WeCom Customer Service (wechat_kf) config
|
||||||
"wechat_kf_corp_id": "", # 微信客服所在企业的corp_id
|
"wechat_kf_corp_id": "", # corp_id of the company the WeCom Customer Service belongs to
|
||||||
"wechat_kf_token": "", # 微信客服回调token
|
"wechat_kf_token": "", # WeCom Customer Service callback token
|
||||||
"wechat_kf_port": 9888, # 微信客服回调服务端口
|
"wechat_kf_port": 9888, # WeCom Customer Service callback service port
|
||||||
"wechat_kf_secret": "", # 微信客服应用的secret
|
"wechat_kf_secret": "", # WeCom Customer Service app secret
|
||||||
"wechat_kf_aes_key": "", # 微信客服回调aes_key
|
"wechat_kf_aes_key": "", # WeCom Customer Service callback aes_key
|
||||||
"wechat_kf_cursor_path": "~/.wechat_kf_cursors.json", # 微信客服sync_msg游标持久化文件路径
|
"wechat_kf_cursor_path": "~/.wechat_kf_cursors.json", # path for persisting the WeCom Customer Service sync_msg cursor
|
||||||
# 飞书配置
|
# Feishu config
|
||||||
"feishu_port": 80, # 飞书bot监听端口,仅webhook模式需要
|
"feishu_port": 80, # Feishu bot listening port; only needed in webhook mode
|
||||||
"feishu_app_id": "", # 飞书机器人应用APP Id
|
"feishu_app_id": "", # Feishu bot app id
|
||||||
"feishu_app_secret": "", # 飞书机器人APP secret
|
"feishu_app_secret": "", # Feishu bot app secret
|
||||||
"feishu_token": "", # 飞书 verification token,仅webhook模式需要
|
"feishu_token": "", # Feishu verification token; only needed in webhook mode
|
||||||
"feishu_event_mode": "websocket", # 飞书事件接收模式: webhook(HTTP服务器) 或 websocket(长连接)
|
"feishu_event_mode": "websocket", # Feishu event mode: webhook(HTTP server) or websocket(long connection)
|
||||||
# 飞书流式回复(基于官方 cardkit 流式卡片 API,需要机器人开通 cardkit:card:write 权限,且飞书客户端 7.20+)
|
# Feishu streaming reply (based on the official cardkit streaming-card API; requires the cardkit:card:write permission and Feishu client 7.20+)
|
||||||
"feishu_stream_reply": True, # 是否开启流式回复(打字机效果)。失败/老客户端自动降级为非流式或升级提示
|
"feishu_stream_reply": True, # whether to enable streaming reply (typewriter effect); auto-downgrades to non-streaming or shows an upgrade prompt on failure/old clients
|
||||||
# 钉钉配置
|
# DingTalk config
|
||||||
"dingtalk_client_id": "", # 钉钉机器人Client ID
|
"dingtalk_client_id": "", # DingTalk bot Client ID
|
||||||
"dingtalk_client_secret": "", # 钉钉机器人Client Secret
|
"dingtalk_client_secret": "", # DingTalk bot Client Secret
|
||||||
"dingtalk_card_enabled": False,
|
"dingtalk_card_enabled": False,
|
||||||
# 企微智能机器人配置(长连接模式)
|
# WeCom smart bot config (long connection mode)
|
||||||
"wecom_bot_id": "", # 企微智能机器人BotID
|
"wecom_bot_id": "", # WeCom smart bot BotID
|
||||||
"wecom_bot_secret": "", # 企微智能机器人长连接Secret
|
"wecom_bot_secret": "", # WeCom smart bot long-connection secret
|
||||||
# Telegram 配置
|
# Telegram config
|
||||||
"telegram_token": "", # 从 @BotFather 申请的 bot token
|
"telegram_token": "", # Bot token from @BotFather
|
||||||
"telegram_proxy": "", # 可选的 HTTP/SOCKS5 代理,例如 http://127.0.0.1:7890 或 socks5://127.0.0.1:1080(留空则走系统环境变量)
|
"telegram_proxy": "", # Optional HTTP/SOCKS5 proxy, e.g. http://127.0.0.1:7890 or socks5://127.0.0.1:1080 (empty falls back to env vars)
|
||||||
"telegram_group_trigger": "mention_or_reply", # 群聊触发方式: mention_or_reply(@或回复触发,推荐) | mention_only(仅@) | all(所有消息)
|
"telegram_group_trigger": "mention_or_reply", # Group trigger: mention_or_reply(@ or reply, recommended) | mention_only(@ only) | all(every message)
|
||||||
"telegram_register_commands": True, # 启动时是否自动向 BotFather 注册命令菜单(与 web 端 slash 命令一致)
|
"telegram_register_commands": True, # Auto-register the BotFather command menu on startup (aligned with web slash commands)
|
||||||
# Slack 配置(Socket Mode,无需公网 IP)
|
# Slack config (Socket Mode, no public IP required)
|
||||||
"slack_bot_token": "", # Bot User OAuth Token,形如 xoxb-...
|
"slack_bot_token": "", # Bot User OAuth Token, like xoxb-...
|
||||||
"slack_app_token": "", # App-Level Token(开启 Socket Mode 后生成),形如 xapp-...
|
"slack_app_token": "", # App-Level Token (generated after enabling Socket Mode), like xapp-...
|
||||||
"slack_group_trigger": "mention_or_reply", # 频道触发方式: mention_or_reply(@或线程内回复,推荐) | mention_only(仅@) | all(所有消息)
|
"slack_group_trigger": "mention_or_reply", # Channel trigger: mention_or_reply(@ or reply in thread, recommended) | mention_only(@ only) | all(every message)
|
||||||
# 微信配置
|
# Discord config (Gateway connection, no public IP required)
|
||||||
"weixin_token": "", # 微信登录后获取的bot_token,留空则启动时自动扫码登录
|
"discord_token": "", # Discord Bot Token (generated on the Bot page of the Developer Portal)
|
||||||
|
"discord_group_trigger": "mention_or_reply", # Channel trigger: mention_or_reply(@ or reply to bot, recommended) | mention_only(@ only) | all(every message)
|
||||||
|
# WeChat config
|
||||||
|
"weixin_token": "", # bot_token obtained after WeChat login; leave empty to auto scan-login on startup
|
||||||
"weixin_base_url": "https://ilinkai.weixin.qq.com", # Weixin ilink API base URL
|
"weixin_base_url": "https://ilinkai.weixin.qq.com", # Weixin ilink API base URL
|
||||||
"weixin_cdn_base_url": "https://novac2c.cdn.weixin.qq.com/c2c", # CDN base URL
|
"weixin_cdn_base_url": "https://novac2c.cdn.weixin.qq.com/c2c", # CDN base URL
|
||||||
"weixin_credentials_path": "~/.weixin_cow_credentials.json", # credentials file path
|
"weixin_credentials_path": "~/.weixin_cow_credentials.json", # credentials file path
|
||||||
# chatgpt指令自定义触发词
|
# custom trigger words for chatgpt commands
|
||||||
"clear_memory_commands": ["#清除记忆"], # 重置会话指令,必须以#开头
|
"clear_memory_commands": ["#清除记忆"], # session-reset command; must start with #
|
||||||
# channel配置
|
# channel config
|
||||||
"channel_type": "", # 通道类型,支持多渠道同时运行。单个: "feishu",多个: "feishu, dingtalk" 或 ["feishu", "dingtalk"]。可选值: web,feishu,dingtalk,wecom_bot,weixin,wechatmp,wechatmp_service,wechatcom_app,wechat_kf,telegram,slack
|
"channel_type": "", # channel type; supports running multiple channels at once. Single: "feishu", multiple: "feishu, dingtalk" or ["feishu", "dingtalk"]. Options: web,feishu,dingtalk,wecom_bot,weixin,wechatmp,wechatmp_service,wechatcom_app,wechat_kf,telegram,slack,discord
|
||||||
"web_console": True, # 是否自动启动Web控制台(默认启动)。设为False可禁用
|
"web_console": True, # whether to auto-start the Web console (on by default). Set False to disable
|
||||||
"subscribe_msg": "", # 订阅消息, 支持: wechatmp, wechatmp_service, wechatcom_app
|
"subscribe_msg": "", # subscribe message; supported by: wechatmp, wechatmp_service, wechatcom_app
|
||||||
"debug": False, # 是否开启debug模式,开启后会打印更多日志
|
"debug": False, # whether to enable debug mode; prints more logs when on
|
||||||
"appdata_dir": "", # 数据目录
|
"appdata_dir": "", # data directory
|
||||||
# 插件配置
|
# plugin config
|
||||||
"plugin_trigger_prefix": "$", # 规范插件提供聊天相关指令的前缀,建议不要和管理员指令前缀"#"冲突
|
"plugin_trigger_prefix": "$", # prefix for plugin chat commands; avoid clashing with the admin command prefix "#"
|
||||||
# 是否使用全局插件配置
|
# whether to use the global plugin config
|
||||||
"use_global_plugin_config": False,
|
"use_global_plugin_config": False,
|
||||||
"max_media_send_count": 3, # 单次最大发送媒体资源的个数
|
"max_media_send_count": 3, # max number of media resources sent at once
|
||||||
"media_send_interval": 1, # 发送图片的事件间隔,单位秒
|
"media_send_interval": 1, # interval between sending images, in seconds
|
||||||
# 智谱AI 平台配置
|
# Zhipu AI platform config
|
||||||
"zhipu_ai_api_key": "",
|
"zhipu_ai_api_key": "",
|
||||||
"zhipu_ai_api_base": "https://open.bigmodel.cn/api/paas/v4",
|
"zhipu_ai_api_base": "https://open.bigmodel.cn/api/paas/v4",
|
||||||
"moonshot_api_key": "",
|
"moonshot_api_key": "",
|
||||||
"moonshot_base_url": "https://api.moonshot.cn/v1",
|
"moonshot_base_url": "https://api.moonshot.cn/v1",
|
||||||
# 豆包(火山方舟) 平台配置
|
# Doubao (Volcano Ark) platform config
|
||||||
"ark_api_key": "",
|
"ark_api_key": "",
|
||||||
"ark_base_url": "https://ark.cn-beijing.volces.com/api/v3",
|
"ark_base_url": "https://ark.cn-beijing.volces.com/api/v3",
|
||||||
# 魔搭社区 平台配置
|
# ModelScope community platform config
|
||||||
"modelscope_api_key": "",
|
"modelscope_api_key": "",
|
||||||
"modelscope_base_url": "https://api-inference.modelscope.cn/v1/chat/completions",
|
"modelscope_base_url": "https://api-inference.modelscope.cn/v1/chat/completions",
|
||||||
# LinkAI平台配置
|
# LinkAI platform config
|
||||||
"use_linkai": False,
|
"use_linkai": False,
|
||||||
"linkai_api_key": "",
|
"linkai_api_key": "",
|
||||||
"linkai_app_code": "",
|
"linkai_app_code": "",
|
||||||
@@ -225,7 +229,7 @@ available_setting = {
|
|||||||
"Minimax_base_url": "",
|
"Minimax_base_url": "",
|
||||||
"deepseek_api_key": "",
|
"deepseek_api_key": "",
|
||||||
"deepseek_api_base": "https://api.deepseek.com/v1",
|
"deepseek_api_base": "https://api.deepseek.com/v1",
|
||||||
# 小米 MiMo 大模型
|
# Xiaomi MiMo LLM
|
||||||
"mimo_api_key": "",
|
"mimo_api_key": "",
|
||||||
"mimo_api_base": "https://api.xiaomimimo.com/v1",
|
"mimo_api_base": "https://api.xiaomimimo.com/v1",
|
||||||
"web_host": "", # Web console bind address; empty means auto
|
"web_host": "", # Web console bind address; empty means auto
|
||||||
@@ -233,14 +237,14 @@ available_setting = {
|
|||||||
"web_password": "", # Web console password; empty means no authentication required
|
"web_password": "", # Web console password; empty means no authentication required
|
||||||
"web_session_expire_days": 30, # Auth session expiry in days
|
"web_session_expire_days": 30, # Auth session expiry in days
|
||||||
"web_file_serve_root": "~", # Root dir the /api/file endpoint may serve; "/" allows the whole filesystem
|
"web_file_serve_root": "~", # Root dir the /api/file endpoint may serve; "/" allows the whole filesystem
|
||||||
"agent": True, # 是否开启Agent模式
|
"agent": True, # whether to enable Agent mode
|
||||||
"agent_workspace": "~/cow", # agent工作空间路径,用于存储skills、memory等
|
"agent_workspace": "~/cow", # agent workspace path, used to store skills, memory, etc.
|
||||||
"agent_max_context_tokens": 50000, # Agent模式下最大上下文tokens
|
"agent_max_context_tokens": 50000, # max context tokens in Agent mode
|
||||||
"agent_max_context_turns": 20, # Agent模式下最大上下文记忆轮次
|
"agent_max_context_turns": 20, # max context memory turns in Agent mode
|
||||||
"agent_max_steps": 20, # Agent模式下单次运行最大决策步数
|
"agent_max_steps": 20, # max decision steps per run in Agent mode
|
||||||
"enable_thinking": False, # Enable deep-thinking mode for thinking-capable models
|
"enable_thinking": False, # Enable deep-thinking mode for thinking-capable models
|
||||||
"reasoning_effort": "high", # Reasoning depth under thinking mode: "high" or "max"
|
"reasoning_effort": "high", # Reasoning depth under thinking mode: "high" or "max"
|
||||||
"knowledge": True, # 是否开启知识库功能
|
"knowledge": True, # whether to enable the knowledge base feature
|
||||||
"skill": {}, # Per-skill runtime config; nested keys flatten to SKILL_<NAME>_<KEY> env vars at startup
|
"skill": {}, # Per-skill runtime config; nested keys flatten to SKILL_<NAME>_<KEY> env vars at startup
|
||||||
"mcp_servers": [], # MCP server list; each entry supports type "stdio" (local process) or "sse" (remote URL)
|
"mcp_servers": [], # MCP server list; each entry supports type "stdio" (local process) or "sse" (remote URL)
|
||||||
}
|
}
|
||||||
@@ -253,7 +257,7 @@ class Config(dict):
|
|||||||
d = {}
|
d = {}
|
||||||
for k, v in d.items():
|
for k, v in d.items():
|
||||||
self[k] = v
|
self[k] = v
|
||||||
# user_datas: 用户数据,key为用户名,value为用户数据,也是dict
|
# user_datas: per-user data; key is the username, value is the user's data (also a dict)
|
||||||
self.user_datas = {}
|
self.user_datas = {}
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
@@ -263,11 +267,11 @@ class Config(dict):
|
|||||||
return super().__setitem__(key, value)
|
return super().__setitem__(key, value)
|
||||||
|
|
||||||
def get(self, key, default=None):
|
def get(self, key, default=None):
|
||||||
# 跳过以下划线开头的注释字段
|
# skip comment fields starting with an underscore
|
||||||
if key.startswith("_"):
|
if key.startswith("_"):
|
||||||
return super().get(key, default)
|
return super().get(key, default)
|
||||||
|
|
||||||
# 如果key不在available_setting中,直接走dict的get,返回config.json中实际加载的值(如不存在则返回default)
|
# if the key is not in available_setting, fall back to dict.get and return the value actually loaded from config.json (or default if absent)
|
||||||
if key not in available_setting:
|
if key not in available_setting:
|
||||||
return super().get(key, default)
|
return super().get(key, default)
|
||||||
|
|
||||||
@@ -334,7 +338,7 @@ def drag_sensitive(config):
|
|||||||
def load_config():
|
def load_config():
|
||||||
global config
|
global config
|
||||||
|
|
||||||
# 打印 ASCII Logo
|
# print ASCII logo
|
||||||
logger.info(" ____ _ _ ")
|
logger.info(" ____ _ _ ")
|
||||||
logger.info(" / ___|_____ __ / \\ __ _ ___ _ __ | |_ ")
|
logger.info(" / ___|_____ __ / \\ __ _ ___ _ __ | |_ ")
|
||||||
logger.info("| | / _ \\ \\ /\\ / // _ \\ / _` |/ _ \\ '_ \\| __|")
|
logger.info("| | / _ \\ \\ /\\ / // _ \\ / _` |/ _ \\ '_ \\| __|")
|
||||||
@@ -344,13 +348,13 @@ def load_config():
|
|||||||
logger.info("")
|
logger.info("")
|
||||||
config_path = "./config.json"
|
config_path = "./config.json"
|
||||||
if not os.path.exists(config_path):
|
if not os.path.exists(config_path):
|
||||||
logger.info("配置文件不存在,将使用config-template.json模板")
|
logger.info("config file not found, falling back to config-template.json")
|
||||||
config_path = "./config-template.json"
|
config_path = "./config-template.json"
|
||||||
|
|
||||||
config_str = read_file(config_path)
|
config_str = read_file(config_path)
|
||||||
logger.debug("[INIT] config str: {}".format(drag_sensitive(config_str)))
|
logger.debug("[INIT] config str: {}".format(drag_sensitive(config_str)))
|
||||||
|
|
||||||
# 将json字符串反序列化为dict类型。
|
# Deserialize the json string into a dict.
|
||||||
# `object_pairs_hook` lets us catch users who accidentally typed the
|
# `object_pairs_hook` lets us catch users who accidentally typed the
|
||||||
# same key twice (e.g. two `"tools"` blocks) — json.loads would
|
# same key twice (e.g. two `"tools"` blocks) — json.loads would
|
||||||
# otherwise silently drop all but the last occurrence.
|
# otherwise silently drop all but the last occurrence.
|
||||||
@@ -367,7 +371,7 @@ def load_config():
|
|||||||
# Some online deployment platforms (e.g. Railway) deploy project from github directly. So you shouldn't put your secrets like api key in a config file, instead use environment variables to override the default config.
|
# Some online deployment platforms (e.g. Railway) deploy project from github directly. So you shouldn't put your secrets like api key in a config file, instead use environment variables to override the default config.
|
||||||
for name, value in os.environ.items():
|
for name, value in os.environ.items():
|
||||||
name = name.lower()
|
name = name.lower()
|
||||||
# 跳过以下划线开头的注释字段
|
# skip comment fields starting with an underscore
|
||||||
if name.startswith("_"):
|
if name.startswith("_"):
|
||||||
continue
|
continue
|
||||||
if name in available_setting:
|
if name in available_setting:
|
||||||
@@ -388,19 +392,19 @@ def load_config():
|
|||||||
|
|
||||||
logger.info("[INIT] load config: {}".format(drag_sensitive(config)))
|
logger.info("[INIT] load config: {}".format(drag_sensitive(config)))
|
||||||
|
|
||||||
# 打印系统初始化信息
|
# print system initialization info
|
||||||
logger.info("[INIT] ========================================")
|
logger.info("[INIT] ========================================")
|
||||||
logger.info("[INIT] System Initialization")
|
logger.info("[INIT] System Initialization")
|
||||||
logger.info("[INIT] ========================================")
|
logger.info("[INIT] ========================================")
|
||||||
logger.info("[INIT] Channel: {}".format(config.get("channel_type", "unknown")))
|
logger.info("[INIT] Channel: {}".format(config.get("channel_type", "unknown")))
|
||||||
logger.info("[INIT] Model: {}".format(config.get("model", "unknown")))
|
logger.info("[INIT] Model: {}".format(config.get("model", "unknown")))
|
||||||
|
|
||||||
# Agent模式信息
|
# Agent mode info
|
||||||
if config.get("agent", True):
|
if config.get("agent", True):
|
||||||
workspace = config.get("agent_workspace", "~/cow")
|
workspace = config.get("agent_workspace", "~/cow")
|
||||||
logger.info("[INIT] Mode: Agent (workspace: {})".format(workspace))
|
logger.info("[INIT] Mode: Agent (workspace: {})".format(workspace))
|
||||||
else:
|
else:
|
||||||
logger.info("[INIT] Mode: Chat (在config.json中设置 \"agent\":true 可启用Agent模式)")
|
logger.info("[INIT] Mode: Chat (set \"agent\":true in config.json to enable Agent mode)")
|
||||||
|
|
||||||
logger.info("[INIT] Debug: {}".format(config.get("debug", False)))
|
logger.info("[INIT] Debug: {}".format(config.get("debug", False)))
|
||||||
logger.info("[INIT] ========================================")
|
logger.info("[INIT] ========================================")
|
||||||
@@ -609,8 +613,8 @@ plugin_config = {}
|
|||||||
|
|
||||||
def write_plugin_config(pconf: dict):
|
def write_plugin_config(pconf: dict):
|
||||||
"""
|
"""
|
||||||
写入插件全局配置
|
Write the global plugin config.
|
||||||
:param pconf: 全量插件配置
|
:param pconf: the full plugin config
|
||||||
"""
|
"""
|
||||||
global plugin_config
|
global plugin_config
|
||||||
for k in pconf:
|
for k in pconf:
|
||||||
@@ -618,8 +622,8 @@ def write_plugin_config(pconf: dict):
|
|||||||
|
|
||||||
def remove_plugin_config(name: str):
|
def remove_plugin_config(name: str):
|
||||||
"""
|
"""
|
||||||
移除待重新加载的插件全局配置
|
Remove the global config of a plugin pending reload.
|
||||||
:param name: 待重载的插件名
|
:param name: name of the plugin to reload
|
||||||
"""
|
"""
|
||||||
global plugin_config
|
global plugin_config
|
||||||
plugin_config.pop(name.lower(), None)
|
plugin_config.pop(name.lower(), None)
|
||||||
@@ -627,12 +631,12 @@ def remove_plugin_config(name: str):
|
|||||||
|
|
||||||
def pconf(plugin_name: str) -> dict:
|
def pconf(plugin_name: str) -> dict:
|
||||||
"""
|
"""
|
||||||
根据插件名称获取配置
|
Get the config for a plugin by name.
|
||||||
:param plugin_name: 插件名称
|
:param plugin_name: plugin name
|
||||||
:return: 该插件的配置项
|
:return: the plugin's config
|
||||||
"""
|
"""
|
||||||
return plugin_config.get(plugin_name.lower())
|
return plugin_config.get(plugin_name.lower())
|
||||||
|
|
||||||
|
|
||||||
# 全局配置,用于存放全局生效的状态
|
# global config holding globally-effective state
|
||||||
global_config = {"admin_users": []}
|
global_config = {"admin_users": []}
|
||||||
|
|||||||
93
docs/channels/discord.mdx
Normal file
93
docs/channels/discord.mdx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
---
|
||||||
|
title: Discord
|
||||||
|
description: 将 CowAgent 接入 Discord Bot
|
||||||
|
---
|
||||||
|
|
||||||
|
> 通过 Discord Bot 的 **Gateway 长连接** 接入 CowAgent,支持私聊(DM)与服务器频道(@机器人 / 回复机器人触发)。Gateway 基于 WebSocket 长连接,无需公网 IP 与回调地址,开箱即用。
|
||||||
|
|
||||||
|
## 一、接入步骤
|
||||||
|
|
||||||
|
### 步骤一:创建 Discord 应用与 Bot
|
||||||
|
|
||||||
|
1. 打开 [Discord 开发者后台](https://discord.com/developers/applications),点击 **New Application**,填写名称(如 `CowAgent`)并创建。
|
||||||
|
2. 左侧菜单进入 **Bot** 页面,点击 **Reset Token** 生成 Bot Token,复制并妥善保存(仅显示一次)。
|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
这个 Token 等同于 Bot 的密码,请勿泄露。若意外泄漏,在 Bot 页面再次点击 **Reset Token** 重置即可。
|
||||||
|
</Tip>
|
||||||
|
|
||||||
|
### 步骤二:开启 Message Content Intent
|
||||||
|
|
||||||
|
私聊与频道读取文本均依赖该权限。
|
||||||
|
|
||||||
|
1. 在 **Bot** 页面下方找到 **Privileged Gateway Intents**。
|
||||||
|
2. 打开 **Message Content Intent** 开关并保存。
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
未开启该开关时,机器人收到的消息内容会为空,导致无响应。
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
### 步骤三:邀请 Bot 进入服务器
|
||||||
|
|
||||||
|
1. 左侧菜单进入 **OAuth2 → URL Generator**。
|
||||||
|
2. **Scopes** 勾选 `bot`。
|
||||||
|
3. **Bot Permissions** 至少勾选:`Send Messages`、`Read Message History`、`Attach Files`、`View Channels`。
|
||||||
|
4. 复制底部生成的授权链接,在浏览器打开,选择目标服务器完成授权。
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
仅需私聊(DM)可跳过此步,但仍需先在任意共同服务器中与机器人建立 DM 通道,或由用户主动私聊机器人。
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
### 步骤四:接入 CowAgent
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Web 控制台(推荐)">
|
||||||
|
打开 Web 控制台(本地链接:http://127.0.0.1:9899 ),选择 **通道** 菜单,点击 **接入通道**,选择 **Discord**,填入 Bot Token,点击接入即可。
|
||||||
|
</Tab>
|
||||||
|
<Tab title="配置文件">
|
||||||
|
在 `config.json` 中添加以下配置后启动:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"channel_type": "discord",
|
||||||
|
"discord_token": "your-discord-bot-token",
|
||||||
|
"discord_group_trigger": "mention_or_reply"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 参数 | 说明 | 默认值 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `discord_token` | 开发者后台 Bot 页面生成的 Bot Token | - |
|
||||||
|
| `discord_group_trigger` | 频道触发方式:`mention_or_reply`(@或回复机器人)/ `mention_only`(仅@) / `all`(所有消息) | `mention_or_reply` |
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
启动 Cow 后,日志中出现以下输出即表示接入成功:
|
||||||
|
|
||||||
|
```
|
||||||
|
[Discord] Bot logged in as CowAgent#1234 (id=123456789)
|
||||||
|
[Discord] ✅ Discord bot ready, listening for messages
|
||||||
|
```
|
||||||
|
|
||||||
|
## 二、功能说明
|
||||||
|
|
||||||
|
| 功能 | 支持情况 |
|
||||||
|
| --- | --- |
|
||||||
|
| 私聊(DM) | ✅ |
|
||||||
|
| 服务器频道(@机器人 / 回复机器人) | ✅ |
|
||||||
|
| 文本消息 | ✅ 收发 |
|
||||||
|
| 图片消息 | ✅ 收发 |
|
||||||
|
| 文件消息 | ✅ 收发(PDF / Word / Excel 等) |
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Discord 单条消息上限为 2000 字符,超长回复会自动按换行拆分为多条发送。
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
## 三、使用
|
||||||
|
|
||||||
|
完成接入后:
|
||||||
|
|
||||||
|
- **私聊(DM)**:在服务器成员列表中找到你的机器人,点击头像直接发消息对话。
|
||||||
|
- **频道**:在已邀请机器人的频道中,使用 `@你的机器人 你好` 或 **回复机器人的某条消息** 触发对话。
|
||||||
|
|
||||||
|
发送图片或文件时,可以在附件的输入框中 **添加文字说明**(描述/问题)一并发送,机器人会结合附件回答。也支持先发附件再发问题,两条消息会自动合并提问。
|
||||||
@@ -21,6 +21,7 @@ CowAgent 支持接入多种聊天通道,启动时通过 `channel_type` 切换
|
|||||||
| [公众号](/channels/wechatmp) | ✅ | ✅ | | ✅ | |
|
| [公众号](/channels/wechatmp) | ✅ | ✅ | | ✅ | |
|
||||||
| [Telegram](/channels/telegram) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
| [Telegram](/channels/telegram) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
| [Slack](/channels/slack) | ✅ | ✅ | ✅ | | ✅ |
|
| [Slack](/channels/slack) | ✅ | ✅ | ✅ | | ✅ |
|
||||||
|
| [Discord](/channels/discord) | ✅ | ✅ | ✅ | | ✅ |
|
||||||
|
|
||||||
- **图片 / 文件 / 语音**列表示通道支持收发对应消息类型,具体细节详见各通道文档
|
- **图片 / 文件 / 语音**列表示通道支持收发对应消息类型,具体细节详见各通道文档
|
||||||
- **群聊**列指可识别并响应群消息
|
- **群聊**列指可识别并响应群消息
|
||||||
@@ -41,3 +42,4 @@ CowAgent 支持接入多种聊天通道,启动时通过 `channel_type` 切换
|
|||||||
- [公众号](/channels/wechatmp) — 微信公众号(订阅号 / 服务号)
|
- [公众号](/channels/wechatmp) — 微信公众号(订阅号 / 服务号)
|
||||||
- [Telegram](/channels/telegram) — 海外 IM,5 分钟接入,无需公网 IP
|
- [Telegram](/channels/telegram) — 海外 IM,5 分钟接入,无需公网 IP
|
||||||
- [Slack](/channels/slack) — 团队协作 IM,Socket Mode 接入,无需公网 IP
|
- [Slack](/channels/slack) — 团队协作 IM,Socket Mode 接入,无需公网 IP
|
||||||
|
- [Discord](/channels/discord) — 社区 IM,Gateway 长连接接入,无需公网 IP
|
||||||
|
|||||||
@@ -199,7 +199,8 @@
|
|||||||
"channels/wechat-kf",
|
"channels/wechat-kf",
|
||||||
"channels/wechatmp",
|
"channels/wechatmp",
|
||||||
"channels/telegram",
|
"channels/telegram",
|
||||||
"channels/slack"
|
"channels/slack",
|
||||||
|
"channels/discord"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -398,6 +399,7 @@
|
|||||||
"en/channels/web",
|
"en/channels/web",
|
||||||
"en/channels/telegram",
|
"en/channels/telegram",
|
||||||
"en/channels/slack",
|
"en/channels/slack",
|
||||||
|
"en/channels/discord",
|
||||||
"en/channels/weixin",
|
"en/channels/weixin",
|
||||||
"en/channels/feishu",
|
"en/channels/feishu",
|
||||||
"en/channels/dingtalk",
|
"en/channels/dingtalk",
|
||||||
@@ -611,7 +613,8 @@
|
|||||||
"ja/channels/wechat-kf",
|
"ja/channels/wechat-kf",
|
||||||
"ja/channels/wechatmp",
|
"ja/channels/wechatmp",
|
||||||
"ja/channels/telegram",
|
"ja/channels/telegram",
|
||||||
"ja/channels/slack"
|
"ja/channels/slack",
|
||||||
|
"ja/channels/discord"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
93
docs/en/channels/discord.mdx
Normal file
93
docs/en/channels/discord.mdx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
---
|
||||||
|
title: Discord
|
||||||
|
description: Integrate CowAgent with a Discord Bot
|
||||||
|
---
|
||||||
|
|
||||||
|
> Integrate CowAgent into Discord via a Discord Bot using the **Gateway** (persistent WebSocket). Supports direct messages (DM) and server channels (triggered by @mention or replying to the bot). The Gateway uses a persistent WebSocket connection — no public IP or callback URL required, works out of the box.
|
||||||
|
|
||||||
|
## 1. Setup
|
||||||
|
|
||||||
|
### Step 1: Create a Discord Application and Bot
|
||||||
|
|
||||||
|
1. Open the [Discord Developer Portal](https://discord.com/developers/applications), click **New Application**, enter a name (e.g. `CowAgent`), and create it.
|
||||||
|
2. Go to the **Bot** page in the left sidebar, click **Reset Token** to generate a Bot Token, then copy and store it safely (shown only once).
|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
This token is your bot's password — keep it secret. If it leaks, click **Reset Token** again on the Bot page to regenerate it.
|
||||||
|
</Tip>
|
||||||
|
|
||||||
|
### Step 2: Enable the Message Content Intent
|
||||||
|
|
||||||
|
Reading message text in both DMs and channels depends on this privileged intent.
|
||||||
|
|
||||||
|
1. On the **Bot** page, find **Privileged Gateway Intents**.
|
||||||
|
2. Turn on **Message Content Intent** and save.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Without this intent enabled, incoming message content will be empty and the bot will not respond.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
### Step 3: Invite the Bot to a Server
|
||||||
|
|
||||||
|
1. Go to **OAuth2 → URL Generator** in the left sidebar.
|
||||||
|
2. Under **Scopes**, check `bot`.
|
||||||
|
3. Under **Bot Permissions**, check at least: `Send Messages`, `Read Message History`, `Attach Files`, `View Channels`.
|
||||||
|
4. Copy the generated authorization URL at the bottom, open it in a browser, and authorize it for your target server.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
You can skip this step if you only need DMs, but you still need a DM channel with the bot (e.g. the user messages the bot directly).
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
### Step 4: Connect to CowAgent
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Web Console (Recommended)">
|
||||||
|
Open the Web Console (default `http://127.0.0.1:9899`), go to **Channels**, click **Add Channel**, choose **Discord**, paste the Bot Token, and click connect.
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Config File">
|
||||||
|
Add the following to `config.json` and start Cow:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"channel_type": "discord",
|
||||||
|
"discord_token": "your-discord-bot-token",
|
||||||
|
"discord_group_trigger": "mention_or_reply"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Key | Description | Default |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `discord_token` | Bot Token generated on the Bot page of the Developer Portal | - |
|
||||||
|
| `discord_group_trigger` | Channel trigger: `mention_or_reply` (@ or reply to bot) / `mention_only` (@ only) / `all` (all messages) | `mention_or_reply` |
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
The integration is ready when you see logs like:
|
||||||
|
|
||||||
|
```
|
||||||
|
[Discord] Bot logged in as CowAgent#1234 (id=123456789)
|
||||||
|
[Discord] ✅ Discord bot ready, listening for messages
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Capabilities
|
||||||
|
|
||||||
|
| Feature | Support |
|
||||||
|
| --- | --- |
|
||||||
|
| Direct message (DM) | ✅ |
|
||||||
|
| Server channel (@bot / reply to bot) | ✅ |
|
||||||
|
| Text messages | ✅ send / receive |
|
||||||
|
| Image messages | ✅ send / receive |
|
||||||
|
| File messages | ✅ send / receive (PDF / Word / Excel, etc.) |
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
A single Discord message is capped at 2000 characters; long replies are automatically split across multiple messages by line breaks.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
## 3. Usage
|
||||||
|
|
||||||
|
Once connected:
|
||||||
|
|
||||||
|
- **Direct message (DM)**: find your bot in the server member list, click its avatar, and message it directly.
|
||||||
|
- **Channel**: in a channel where the bot is invited, trigger it with `@your-bot hello` or by **replying to one of the bot's messages**.
|
||||||
|
|
||||||
|
When sending an image or file, you can **add a text caption** (description / question) in the attachment input — the bot will answer based on both. Sending an attachment first and then a follow-up question also works; the two messages are merged automatically.
|
||||||
@@ -21,6 +21,7 @@ The table below summarizes the inbound message types, bot reply types, and group
|
|||||||
| [Official Account](/en/channels/wechatmp) | ✅ | ✅ | | ✅ | |
|
| [Official Account](/en/channels/wechatmp) | ✅ | ✅ | | ✅ | |
|
||||||
| [Telegram](/en/channels/telegram) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
| [Telegram](/en/channels/telegram) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
| [Slack](/en/channels/slack) | ✅ | ✅ | ✅ | | ✅ |
|
| [Slack](/en/channels/slack) | ✅ | ✅ | ✅ | | ✅ |
|
||||||
|
| [Discord](/en/channels/discord) | ✅ | ✅ | ✅ | | ✅ |
|
||||||
|
|
||||||
- The **Image / File / Voice** columns indicate that the channel can send and receive the corresponding message types; see each channel's docs for details
|
- The **Image / File / Voice** columns indicate that the channel can send and receive the corresponding message types; see each channel's docs for details
|
||||||
- The **Group Chat** column indicates the ability to recognize and respond to group messages
|
- The **Group Chat** column indicates the ability to recognize and respond to group messages
|
||||||
@@ -41,3 +42,4 @@ The table below summarizes the inbound message types, bot reply types, and group
|
|||||||
- [Official Account](/en/channels/wechatmp) — WeChat Official Account (subscription / service)
|
- [Official Account](/en/channels/wechatmp) — WeChat Official Account (subscription / service)
|
||||||
- [Telegram](/en/channels/telegram) — global IM, 5-minute setup, no public IP needed
|
- [Telegram](/en/channels/telegram) — global IM, 5-minute setup, no public IP needed
|
||||||
- [Slack](/en/channels/slack) — team collaboration IM, Socket Mode integration, no public IP needed
|
- [Slack](/en/channels/slack) — team collaboration IM, Socket Mode integration, no public IP needed
|
||||||
|
- [Discord](/en/channels/discord) — community IM, Gateway connection, no public IP needed
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ CowAgent は主要な LLM プロバイダーすべてに対応しています。
|
|||||||
| [WeChat 公式アカウント](https://docs.cowagent.ai/ja/channels/wechatmp) | ✅ | ✅ | | ✅ | |
|
| [WeChat 公式アカウント](https://docs.cowagent.ai/ja/channels/wechatmp) | ✅ | ✅ | | ✅ | |
|
||||||
| [Telegram](https://docs.cowagent.ai/ja/channels/telegram) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
| [Telegram](https://docs.cowagent.ai/ja/channels/telegram) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
| [Slack](https://docs.cowagent.ai/ja/channels/slack) | ✅ | ✅ | ✅ | | ✅ |
|
| [Slack](https://docs.cowagent.ai/ja/channels/slack) | ✅ | ✅ | ✅ | | ✅ |
|
||||||
|
| [Discord](https://docs.cowagent.ai/ja/channels/discord) | ✅ | ✅ | ✅ | | ✅ |
|
||||||
|
|
||||||
> Feishu と WeCom Bot は **Web コンソール内で QR コードをスキャンするだけで接続**できます — パブリック IP は不要です。詳細は [チャネル概要](https://docs.cowagent.ai/ja/channels/index) を参照してください。
|
> Feishu と WeCom Bot は **Web コンソール内で QR コードをスキャンするだけで接続**できます — パブリック IP は不要です。詳細は [チャネル概要](https://docs.cowagent.ai/ja/channels/index) を参照してください。
|
||||||
|
|
||||||
|
|||||||
93
docs/ja/channels/discord.mdx
Normal file
93
docs/ja/channels/discord.mdx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
---
|
||||||
|
title: Discord
|
||||||
|
description: Discord Bot 経由で CowAgent を接続
|
||||||
|
---
|
||||||
|
|
||||||
|
> Discord Bot の **Gateway 常時接続** を通じて CowAgent を接続します。ダイレクトメッセージ(DM)およびサーバーチャンネル(@メンションまたはボットへの返信で起動)に対応。Gateway は WebSocket の常時接続を使うため公開 IP やコールバック URL は不要で、すぐに利用できます。
|
||||||
|
|
||||||
|
## 1. 接続手順
|
||||||
|
|
||||||
|
### ステップ 1: Discord アプリと Bot を作成
|
||||||
|
|
||||||
|
1. [Discord 開発者ポータル](https://discord.com/developers/applications) を開き、**New Application** をクリックして名前(例: `CowAgent`)を入力し作成します。
|
||||||
|
2. 左メニューの **Bot** ページで **Reset Token** をクリックして Bot Token を生成し、コピーして安全に保管します(一度だけ表示されます)。
|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
この Token は Bot のパスワードに相当します。漏洩しないようにしてください。万一漏洩した場合は Bot ページで再度 **Reset Token** をクリックして再生成できます。
|
||||||
|
</Tip>
|
||||||
|
|
||||||
|
### ステップ 2: Message Content Intent を有効化
|
||||||
|
|
||||||
|
DM・チャンネルいずれもテキスト読み取りにこの権限が必要です。
|
||||||
|
|
||||||
|
1. **Bot** ページの **Privileged Gateway Intents** を探します。
|
||||||
|
2. **Message Content Intent** をオンにして保存します。
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
この権限を有効にしないと、受信メッセージの本文が空になり、ボットが応答しません。
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
### ステップ 3: Bot をサーバーに招待
|
||||||
|
|
||||||
|
1. 左メニューの **OAuth2 → URL Generator** を開きます。
|
||||||
|
2. **Scopes** で `bot` をチェックします。
|
||||||
|
3. **Bot Permissions** で最低限以下をチェックします: `Send Messages`、`Read Message History`、`Attach Files`、`View Channels`。
|
||||||
|
4. 下部に生成された認証 URL をコピーしてブラウザで開き、対象のサーバーを選択して認証を完了します。
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
DM のみを利用する場合はこのステップを省略できますが、ボットとの DM チャンネルが必要です(ユーザーがボットに直接メッセージを送るなど)。
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
### ステップ 4: CowAgent に接続
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Web コンソール(推奨)">
|
||||||
|
Web コンソール(既定 `http://127.0.0.1:9899`)を開き、**チャネル** メニュー → **チャネルを追加** → **Discord** を選択し、Bot Token を貼り付けて接続をクリックします。
|
||||||
|
</Tab>
|
||||||
|
<Tab title="設定ファイル">
|
||||||
|
`config.json` に以下を追加して Cow を起動します:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"channel_type": "discord",
|
||||||
|
"discord_token": "your-discord-bot-token",
|
||||||
|
"discord_group_trigger": "mention_or_reply"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| パラメータ | 説明 | 既定値 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `discord_token` | 開発者ポータルの Bot ページで生成した Bot Token | - |
|
||||||
|
| `discord_group_trigger` | チャンネルのトリガー方式: `mention_or_reply`(@ またはボットへの返信)/ `mention_only`(@ のみ)/ `all`(全メッセージ) | `mention_or_reply` |
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
ログに以下のような出力が表示されれば接続成功です:
|
||||||
|
|
||||||
|
```
|
||||||
|
[Discord] Bot logged in as CowAgent#1234 (id=123456789)
|
||||||
|
[Discord] ✅ Discord bot ready, listening for messages
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 機能
|
||||||
|
|
||||||
|
| 機能 | 対応状況 |
|
||||||
|
| --- | --- |
|
||||||
|
| ダイレクトメッセージ(DM) | ✅ |
|
||||||
|
| サーバーチャンネル(@bot / ボットへの返信) | ✅ |
|
||||||
|
| テキストメッセージ | ✅ 送受信 |
|
||||||
|
| 画像メッセージ | ✅ 送受信 |
|
||||||
|
| ファイルメッセージ | ✅ 送受信(PDF / Word / Excel など) |
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Discord の 1 メッセージは最大 2000 文字です。長い返信は改行単位で自動的に複数メッセージに分割して送信されます。
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
## 3. 使い方
|
||||||
|
|
||||||
|
接続が完了したら:
|
||||||
|
|
||||||
|
- **ダイレクトメッセージ(DM)**: サーバーのメンバー一覧からボットを見つけ、アイコンをクリックして直接メッセージを送ります。
|
||||||
|
- **チャンネル**: ボットを招待したチャンネルで、`@your-bot こんにちは` または **ボットのメッセージへの返信** で起動します。
|
||||||
|
|
||||||
|
画像やファイルを送るときは、添付の入力欄に **テキスト説明**(説明・質問)を書いて一緒に送信できます。Bot は添付ファイルと説明を合わせて回答します。先に添付を送り、その後に質問を送る形でも、2 つのメッセージは自動でまとめて処理されます。
|
||||||
@@ -11,6 +11,9 @@ CowAgent は複数のチャットチャネルへの接続に対応しており
|
|||||||
|
|
||||||
| チャネル | テキスト | 画像 | ファイル | 音声 | グループチャット |
|
| チャネル | テキスト | 画像 | ファイル | 音声 | グループチャット |
|
||||||
| --- | :-: | :-: | :-: | :-: | :-: |
|
| --- | :-: | :-: | :-: | :-: | :-: |
|
||||||
|
| [Telegram](/ja/channels/telegram) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| [Slack](/ja/channels/slack) | ✅ | ✅ | ✅ | | ✅ |
|
||||||
|
| [Discord](/ja/channels/discord) | ✅ | ✅ | ✅ | | ✅ |
|
||||||
| [WeChat](/ja/channels/weixin) | ✅ | ✅ | ✅ | ✅ | |
|
| [WeChat](/ja/channels/weixin) | ✅ | ✅ | ✅ | ✅ | |
|
||||||
| [Web コンソール](/ja/channels/web) | ✅ | ✅ | ✅ | ✅ | |
|
| [Web コンソール](/ja/channels/web) | ✅ | ✅ | ✅ | ✅ | |
|
||||||
| [Feishu](/ja/channels/feishu) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
| [Feishu](/ja/channels/feishu) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
@@ -19,8 +22,6 @@ CowAgent は複数のチャットチャネルへの接続に対応しており
|
|||||||
| [QQ](/ja/channels/qq) | ✅ | ✅ | ✅ | | ✅ |
|
| [QQ](/ja/channels/qq) | ✅ | ✅ | ✅ | | ✅ |
|
||||||
| [WeCom アプリ](/ja/channels/wecom) | ✅ | ✅ | ✅ | ✅ | |
|
| [WeCom アプリ](/ja/channels/wecom) | ✅ | ✅ | ✅ | ✅ | |
|
||||||
| [WeChat 公式アカウント](/ja/channels/wechatmp) | ✅ | ✅ | | ✅ | |
|
| [WeChat 公式アカウント](/ja/channels/wechatmp) | ✅ | ✅ | | ✅ | |
|
||||||
| [Telegram](/ja/channels/telegram) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
||||||
| [Slack](/ja/channels/slack) | ✅ | ✅ | ✅ | | ✅ |
|
|
||||||
|
|
||||||
- **画像 / ファイル / 音声**列は対応するメッセージタイプの送受信に対応していることを示します。詳細は各チャネルのドキュメントを参照してください
|
- **画像 / ファイル / 音声**列は対応するメッセージタイプの送受信に対応していることを示します。詳細は各チャネルのドキュメントを参照してください
|
||||||
- **グループチャット**列はグループメッセージを認識して応答できることを示します
|
- **グループチャット**列はグループメッセージを認識して応答できることを示します
|
||||||
@@ -41,3 +42,4 @@ CowAgent は複数のチャットチャネルへの接続に対応しており
|
|||||||
- [WeChat 公式アカウント](/ja/channels/wechatmp) — WeChat 公式アカウント(購読アカウント / サービスアカウント)
|
- [WeChat 公式アカウント](/ja/channels/wechatmp) — WeChat 公式アカウント(購読アカウント / サービスアカウント)
|
||||||
- [Telegram](/ja/channels/telegram) — グローバル IM、5 分で接続、公開 IP 不要
|
- [Telegram](/ja/channels/telegram) — グローバル IM、5 分で接続、公開 IP 不要
|
||||||
- [Slack](/ja/channels/slack) — チームコラボレーション IM、Socket Mode 接続、公開 IP 不要
|
- [Slack](/ja/channels/slack) — チームコラボレーション IM、Socket Mode 接続、公開 IP 不要
|
||||||
|
- [Discord](/ja/channels/discord) — コミュニティ IM、Gateway 接続、公開 IP 不要
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ CowAgent 支持国内外主流厂商的大语言模型。**文本对话、图像
|
|||||||
| [微信公众号](https://docs.cowagent.ai/channels/wechatmp) | ✅ | ✅ | | ✅ | |
|
| [微信公众号](https://docs.cowagent.ai/channels/wechatmp) | ✅ | ✅ | | ✅ | |
|
||||||
| [Telegram](https://docs.cowagent.ai/channels/telegram) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
| [Telegram](https://docs.cowagent.ai/channels/telegram) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
| [Slack](https://docs.cowagent.ai/channels/slack) | ✅ | ✅ | ✅ | | ✅ |
|
| [Slack](https://docs.cowagent.ai/channels/slack) | ✅ | ✅ | ✅ | | ✅ |
|
||||||
|
| [Discord](https://docs.cowagent.ai/channels/discord) | ✅ | ✅ | ✅ | | ✅ |
|
||||||
|
|
||||||
> 飞书、企微智能机器人支持在 Web 控制台内**扫码一键接入**,无需公网 IP。详见 [通道概览](https://docs.cowagent.ai/channels)。
|
> 飞书、企微智能机器人支持在 Web 控制台内**扫码一键接入**,无需公网 IP。详见 [通道概览](https://docs.cowagent.ai/channels)。
|
||||||
|
|
||||||
|
|||||||
@@ -31,3 +31,5 @@ pycryptodome
|
|||||||
python-telegram-bot
|
python-telegram-bot
|
||||||
# slack bot
|
# slack bot
|
||||||
slack_bolt
|
slack_bolt
|
||||||
|
# discord bot
|
||||||
|
discord.py
|
||||||
Reference in New Issue
Block a user