diff --git a/README.md b/README.md index 0a162ca..e08d6c9 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [中文] | [English]
-**CowAgent** 是基于大模型的超级AI助理,能够主动思考和任务规划、操作计算机和外部资源、创造和执行Skills、拥有长期记忆并不断成长。CowAgent 支持灵活切换多种模型,能处理文本、语音、图片、文件等多模态消息,可接入网页、飞书、钉钉、企微智能机器人、企业微信应用、微信公众号中使用,7*24小时运行于你的个人电脑或服务器中。 +**CowAgent** 是基于大模型的超级AI助理,能够主动思考和任务规划、操作计算机和外部资源、创造和执行Skills、拥有长期记忆并不断成长。CowAgent 支持灵活切换多种模型,能处理文本、语音、图片、文件等多模态消息,可接入网页、飞书、钉钉、企微智能机器人、QQ、企微自建应用、微信公众号中使用,7*24小时运行于你的个人电脑或服务器中。
🌐 官网 ·
@@ -143,7 +143,7 @@ pip3 install -r requirements-optional.txt
```bash
# config.json 文件内容示例
{
- "channel_type": "web", # 接入渠道类型,默认为web,支持修改为:feishu,dingtalk,wecom_bot,wechatcom_app,wechatmp_service,wechatmp,terminal
+ "channel_type": "web", # 接入渠道类型,默认为web,支持修改为:feishu,dingtalk,wecom_bot,qq,wechatcom_app,wechatmp_service,wechatmp,terminal
"model": "MiniMax-M2.5", # 模型名称
"minimax_api_key": "", # MiniMax API Key
"zhipu_ai_api_key": "", # 智谱GLM API Key
@@ -702,7 +702,23 @@ API Key创建:在 [控制台](https://aistudio.google.com/app/apikey?hl=zh-cn)
5. WeCom App - 企业微信应用
+5. QQ - QQ 机器人
+
+QQ 机器人使用 WebSocket 长连接模式,无需公网 IP 和域名,支持 QQ 单聊、群聊和频道消息:
+
+```json
+{
+ "channel_type": "qq",
+ "qq_app_id": "YOUR_APP_ID",
+ "qq_app_secret": "YOUR_APP_SECRET"
+}
+```
+详细步骤和参数说明参考 [QQ 机器人接入](https://docs.cowagent.ai/channels/qq)
+
+6. WeCom App - 企业微信应用
企业微信自建应用接入需在后台创建应用并启用消息回调,配置示例:
@@ -722,7 +738,7 @@ API Key创建:在 [控制台](https://aistudio.google.com/app/apikey?hl=zh-cn)
6. WeChat MP - 微信公众号
+7. WeChat MP - 微信公众号
本项目支持订阅号和服务号两种公众号,通过服务号(`wechatmp_service`)体验更佳。
@@ -757,7 +773,7 @@ API Key创建:在 [控制台](https://aistudio.google.com/app/apikey?hl=zh-cn)
7. Terminal - 终端
+8. Terminal - 终端
修改 `config.json` 中的 `channel_type` 字段:
diff --git a/agent/tools/scheduler/integration.py b/agent/tools/scheduler/integration.py
index 7d43236..949c9ff 100644
--- a/agent/tools/scheduler/integration.py
+++ b/agent/tools/scheduler/integration.py
@@ -237,6 +237,8 @@ def _execute_send_message(task: dict, agent_bridge):
logger.warning(f"[Scheduler] Task {task['id']}: DingTalk single chat message missing sender_staff_id")
elif channel_type == "wecom_bot":
context["msg"] = None
+ elif channel_type == "qq":
+ context["msg"] = None
# Create reply
reply = Reply(ReplyType.TEXT, content)
diff --git a/channel/qq/qq_channel.py b/channel/qq/qq_channel.py
index e88138b..d3a1e0f 100644
--- a/channel/qq/qq_channel.py
+++ b/channel/qq/qq_channel.py
@@ -130,7 +130,7 @@ class QQChannel(ChatChannel):
self._access_token = data.get("access_token", "")
expires_in = int(data.get("expires_in", 7200))
self._token_expires_at = time.time() + expires_in - 60
- logger.info(f"[QQ] Access token refreshed, expires_in={expires_in}s")
+ logger.debug(f"[QQ] Access token refreshed, expires_in={expires_in}s")
except Exception as e:
logger.error(f"[QQ] Failed to refresh access_token: {e}")
@@ -159,7 +159,7 @@ class QQChannel(ChatChannel):
)
resp.raise_for_status()
url = resp.json().get("url", "")
- logger.info(f"[QQ] Gateway URL: {url}")
+ logger.debug(f"[QQ] Gateway URL: {url}")
return url
except Exception as e:
logger.error(f"[QQ] Failed to get gateway URL: {e}")
@@ -173,7 +173,7 @@ class QQChannel(ChatChannel):
return
def _on_open(ws):
- logger.info("[QQ] WebSocket connected, waiting for Hello...")
+ logger.debug("[QQ] WebSocket connected, waiting for Hello...")
def _on_message(ws, raw):
try:
@@ -242,7 +242,7 @@ class QQChannel(ChatChannel):
},
},
})
- logger.info(f"[QQ] Identify sent with intents={DEFAULT_INTENTS}")
+ logger.debug(f"[QQ] Identify sent with intents={DEFAULT_INTENTS}")
def _send_resume(self):
self._ws_send({
@@ -253,7 +253,7 @@ class QQChannel(ChatChannel):
"seq": self._last_seq,
},
})
- logger.info(f"[QQ] Resume sent: session_id={self._session_id}, seq={self._last_seq}")
+ logger.debug(f"[QQ] Resume sent: session_id={self._session_id}, seq={self._last_seq}")
def _start_heartbeat(self, interval_ms: int):
if self._heartbeat_thread and self._heartbeat_thread.is_alive():
@@ -291,7 +291,7 @@ class QQChannel(ChatChannel):
if op == OP_HELLO:
heartbeat_interval = d.get("heartbeat_interval", 45000) if d else 45000
- logger.info(f"[QQ] Received Hello, heartbeat_interval={heartbeat_interval}ms")
+ logger.debug(f"[QQ] Received Hello, heartbeat_interval={heartbeat_interval}ms")
self._heartbeat_interval = heartbeat_interval
if self._can_resume and self._session_id:
self._send_resume()
@@ -321,8 +321,8 @@ class QQChannel(ChatChannel):
if t == "READY":
self._session_id = d.get("session_id", "")
user = d.get("user", {})
- logger.info(f"[QQ] Ready: session_id={self._session_id}, "
- f"bot={user.get('username', '')}")
+ bot_name = user.get('username', '')
+ logger.info(f"[QQ] ✅ Connected successfully (bot={bot_name})")
self._connected = True
self._can_resume = False
self._start_heartbeat(self._heartbeat_interval)
@@ -445,8 +445,13 @@ class QQChannel(ChatChannel):
def send(self, reply: Reply, context: Context):
msg = context.get("msg")
+ is_group = context.get("isgroup", False)
+ receiver = context.get("receiver", "")
+
if not msg:
- logger.warning("[QQ] No msg in context, cannot send reply")
+ # Active send (e.g. scheduled tasks), no original message to reply to
+ self._active_send_text(reply.content if reply.type == ReplyType.TEXT else str(reply.content),
+ receiver, is_group)
return
event_type = getattr(msg, "event_type", "")
@@ -521,6 +526,26 @@ class QQChannel(ChatChannel):
except Exception as e:
logger.error(f"[QQ] Send message error: {e}")
+ # ------------------------------------------------------------------
+ # Active send (no original message, e.g. scheduled tasks)
+ # ------------------------------------------------------------------
+
+ def _active_send_text(self, content: str, receiver: str, is_group: bool):
+ """Send text without an original message (active push). QQ limits active messages to 4/month per user."""
+ if not receiver:
+ logger.warning("[QQ] No receiver for active send")
+ return
+ if is_group:
+ url = f"{QQ_API_BASE}/v2/groups/{receiver}/messages"
+ else:
+ url = f"{QQ_API_BASE}/v2/users/{receiver}/messages"
+ body = {
+ "content": content,
+ "msg_type": 0,
+ }
+ event_label = "GROUP_ACTIVE" if is_group else "C2C_ACTIVE"
+ self._post_message(url, body, event_label)
+
# ------------------------------------------------------------------
# Send text
# ------------------------------------------------------------------
diff --git a/channel/web/web_channel.py b/channel/web/web_channel.py
index d980df9..6327a79 100644
--- a/channel/web/web_channel.py
+++ b/channel/web/web_channel.py
@@ -615,6 +615,15 @@ class ChannelsHandler:
{"key": "wecom_bot_secret", "label": "Secret", "type": "secret"},
],
}),
+ ("qq", {
+ "label": {"zh": "QQ 机器人", "en": "QQ Bot"},
+ "icon": "fa-comment",
+ "color": "blue",
+ "fields": [
+ {"key": "qq_app_id", "label": "App ID", "type": "text"},
+ {"key": "qq_app_secret", "label": "App Secret", "type": "secret"},
+ ],
+ }),
("wechatcom_app", {
"label": {"zh": "企微自建应用", "en": "WeCom App"},
"icon": "fa-building",
diff --git a/common/cloud_client.py b/common/cloud_client.py
index 7e21228..6ad1bcb 100644
--- a/common/cloud_client.py
+++ b/common/cloud_client.py
@@ -26,6 +26,8 @@ CHANNEL_ACTIONS = {"channel_create", "channel_update", "channel_delete"}
CREDENTIAL_MAP = {
"feishu": ("feishu_app_id", "feishu_app_secret"),
"dingtalk": ("dingtalk_client_id", "dingtalk_client_secret"),
+ "wecom_bot": ("wecom_bot_id", "wecom_bot_secret"),
+ "qq": ("qq_app_id", "qq_app_secret"),
"wechatmp": ("wechatmp_app_id", "wechatmp_app_secret"),
"wechatmp_service": ("wechatmp_app_id", "wechatmp_app_secret"),
"wechatcom_app": ("wechatcomapp_agent_id", "wechatcomapp_secret"),
@@ -669,6 +671,12 @@ def _build_config():
elif current_channel_type in ("wechatmp", "wechatmp_service"):
config["app_id"] = local_conf.get("wechatmp_app_id")
config["app_secret"] = local_conf.get("wechatmp_app_secret")
+ elif current_channel_type == "wecom_bot":
+ config["app_id"] = local_conf.get("wecom_bot_id")
+ config["app_secret"] = local_conf.get("wecom_bot_secret")
+ elif current_channel_type == "qq":
+ config["app_id"] = local_conf.get("qq_app_id")
+ config["app_secret"] = local_conf.get("qq_app_secret")
elif current_channel_type == "wechatcom_app":
config["app_id"] = local_conf.get("wechatcomapp_agent_id")
config["app_secret"] = local_conf.get("wechatcomapp_secret")
diff --git a/config.py b/config.py
index 757d7f5..93e3d6e 100644
--- a/config.py
+++ b/config.py
@@ -381,6 +381,8 @@ def load_config():
"wechatmp_app_secret": "WECHATMP_APP_SECRET",
"wechatcomapp_agent_id": "WECHATCOMAPP_AGENT_ID",
"wechatcomapp_secret": "WECHATCOMAPP_SECRET",
+ "qq_app_id": "QQ_APP_ID",
+ "qq_app_secret": "QQ_APP_SECRET"
}
injected = 0
for conf_key, env_key in _CONFIG_TO_ENV.items():
diff --git a/docs/agent.md b/docs/agent.md
index 34c889b..9fcdbe4 100644
--- a/docs/agent.md
+++ b/docs/agent.md
@@ -179,5 +179,7 @@ Agent支持在多种渠道中使用,只需修改 `config.json` 中的 `channel
- **飞书接入**:[飞书接入文档](https://docs.link-ai.tech/cow/multi-platform/feishu)
- **钉钉接入**:[钉钉接入文档](https://docs.link-ai.tech/cow/multi-platform/dingtalk)
- **企业微信应用接入**:[企微应用文档](https://docs.link-ai.tech/cow/multi-platform/wechat-com)
+- **企微智能机器人**:[企微智能机器人文档](https://docs.link-ai.tech/cow/multi-platform/wecom-bot)
+- **QQ机器人**:[QQ机器人文档](https://docs.link-ai.tech/cow/multi-platform/qq)
更多渠道配置参考:[通道说明](../README.md#通道说明)
diff --git a/docs/channels/qq.mdx b/docs/channels/qq.mdx
new file mode 100644
index 0000000..3b7554a
--- /dev/null
+++ b/docs/channels/qq.mdx
@@ -0,0 +1,88 @@
+---
+title: QQ 机器人
+description: 将 CowAgent 接入 QQ 机器人(WebSocket 长连接模式)
+---
+
+> 通过 QQ 开放平台的机器人接口接入 CowAgent,支持 QQ 单聊、QQ 群聊(@机器人)、频道消息和频道私信,无需公网 IP,使用 WebSocket 长连接模式。
+
+
+
+2.填写机器人名称、头像等基本信息,完成创建:
+
+
+
+3.点击进入机器人配置页面,选择**开发管理**菜单,完成以下步骤:
+
+ - 复制并记录 **AppID**(机器人ID)
+ - 生成并记录 **AppSecret**(机器人秘钥)
+
+
+
+## 二、配置和运行
+
+### 方式一:Web 控制台接入
+
+启动 Cow项目后打开 Web 控制台 (本地链接为: http://127.0.0.1:9899/ ),选择 **通道** 菜单,点击 **接入通道**,选择 **QQ 机器人**,填写上一步保存的 AppID 和 AppSecret,点击接入即可。
+
+
+
+### 方式二:配置文件接入
+
+在 `config.json` 中添加以下配置:
+
+```json
+{
+ "channel_type": "qq",
+ "qq_app_id": "YOUR_APP_ID",
+ "qq_app_secret": "YOUR_APP_SECRET"
+}
+```
+
+| 参数 | 说明 |
+| --- | --- |
+| `qq_app_id` | QQ 机器人的 AppID,在开放平台开发管理中获取 |
+| `qq_app_secret` | QQ 机器人的 AppSecret,在开放平台开发管理中获取 |
+
+配置完成后启动程序,日志显示 `[QQ] ✅ Connected successfully` 即表示连接成功。
+
+
+## 三、使用
+
+在 QQ开放平台 - 管理 - **使用范围和人员** 菜单中,使用QQ客户端扫描 "添加到群和消息列表" 的二维码,即可开始与QQ机器人的聊天:
+
+
+
+对话效果:
+
+
+## 四、功能说明
+
+> 注意:若需在群聊及频道中使用QQ机器人,需完成发布上架审核并在使用范围配置权限使用范围。
+
+| 功能 | 支持情况 |
+| --- | --- |
+| QQ 单聊 | ✅ |
+| QQ 群聊(@机器人) | ✅ |
+| 频道消息(@机器人) | ✅ |
+| 频道私信 | ✅ |
+| 文本消息 | ✅ 收发 |
+| 图片消息 | ✅ 收发(群聊和单聊) |
+| 文件消息 | ✅ 发送(群聊和单聊) |
+| 定时任务 | ✅ 主动推送(每月每用户限 4 条) |
+
+
+## 五、注意事项
+
+- **被动消息限制**:QQ 单聊被动消息有效期为 60 分钟,每条消息最多回复 5 次;QQ 群聊被动消息有效期为 5 分钟。
+- **主动消息限制**:单聊和群聊每月主动消息上限为 4 条,在使用定时任务功能时需要注意这个限制
+- **事件权限**:默认订阅 `GROUP_AND_C2C_EVENT`(QQ群/单聊)和 `PUBLIC_GUILD_MESSAGES`(频道公域消息),如需其他事件类型请在开放平台申请权限。
diff --git a/docs/channels/wecom-bot.mdx b/docs/channels/wecom-bot.mdx
index 47c46dd..bcdac98 100644
--- a/docs/channels/wecom-bot.mdx
+++ b/docs/channels/wecom-bot.mdx
@@ -29,7 +29,7 @@ description: 将 CowAgent 接入企业微信智能机器人(长连接模式)
### 方式一:Web 控制台接入
-启动程序后打开 Web 控制台 (本地连接为: http://127.0.0.1:9899/ ),选择 **通道** 菜单,点击 **接入通道**,选择 **企微智能机器人**,填写上一步保存的 Bot ID 和 Secret,点击接入即可。
+启动Cow项目后打开 Web 控制台 (本地链接为: http://127.0.0.1:9899/ ),选择 **通道** 菜单,点击 **接入通道**,选择 **企微智能机器人**,填写上一步保存的 Bot ID 和 Secret,点击接入即可。
diff --git a/docs/docs.json b/docs/docs.json
index f82f8c7..ebfe877 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -157,6 +157,7 @@
"channels/feishu",
"channels/dingtalk",
"channels/wecom-bot",
+ "channels/qq",
"channels/wecom",
"channels/wechatmp"
]
@@ -300,6 +301,7 @@
"en/channels/feishu",
"en/channels/dingtalk",
"en/channels/wecom-bot",
+ "en/channels/qq",
"en/channels/wecom",
"en/channels/wechatmp"
]
diff --git a/docs/en/channels/qq.mdx b/docs/en/channels/qq.mdx
new file mode 100644
index 0000000..a7f0859
--- /dev/null
+++ b/docs/en/channels/qq.mdx
@@ -0,0 +1,88 @@
+---
+title: QQ Bot
+description: Connect CowAgent to QQ Bot (WebSocket long connection)
+---
+
+> Connect CowAgent via QQ Open Platform's bot API, supporting QQ direct messages, group chats (@bot), guild channel messages, and guild DMs. No public IP required — uses WebSocket long connection.
+
+
+
+2.Fill in the bot name, avatar, and other basic information to complete the creation:
+
+
+
+3.Enter the bot configuration page, go to **Development Management**, and complete the following steps:
+
+ - Copy and save the **AppID** (Bot ID)
+ - Generate and save the **AppSecret** (Bot Secret)
+
+
+
+## 2. Configuration and Running
+
+### Option A: Web Console
+
+Start the program and open the Web console (local access: http://127.0.0.1:9899/). Go to the **Channels** tab, click **Connect Channel**, select **QQ Bot**, fill in the AppID and AppSecret from the previous step, and click Connect.
+
+
+
+### Option B: Config File
+
+Add the following to your `config.json`:
+
+```json
+{
+ "channel_type": "qq",
+ "qq_app_id": "YOUR_APP_ID",
+ "qq_app_secret": "YOUR_APP_SECRET"
+}
+```
+
+| Parameter | Description |
+| --- | --- |
+| `qq_app_id` | AppID of the QQ Bot, found in Development Management on the open platform |
+| `qq_app_secret` | AppSecret of the QQ Bot, found in Development Management on the open platform |
+
+After configuration, start the program. The log message `[QQ] ✅ Connected successfully` indicates a successful connection.
+
+
+## 3. Usage
+
+In the QQ Open Platform, go to **Management → Usage Scope & Members**, scan the "Add to group and message list" QR code with your QQ client to start chatting with the bot:
+
+
+
+Chat example:
+
+
+## 4. Supported Features
+
+> Note: To use the QQ bot in group chats and guild channels, you need to complete the publishing review and configure usage scope permissions.
+
+| Feature | Status |
+| --- | --- |
+| QQ Direct Messages | ✅ |
+| QQ Group Chat (@bot) | ✅ |
+| Guild Channel (@bot) | ✅ |
+| Guild DM | ✅ |
+| Text Messages | ✅ Send & Receive |
+| Image Messages | ✅ Send & Receive (group & direct) |
+| File Messages | ✅ Send (group & direct) |
+| Scheduled Tasks | ✅ Active push (4 per user per month) |
+
+
+## 5. Notes
+
+- **Passive message limits**: QQ direct message replies are valid for 60 minutes (max 5 replies per message); group chat replies are valid for 5 minutes.
+- **Active message limits**: Both direct and group chats have a monthly limit of 4 active messages. Keep this in mind when using the scheduled tasks feature.
+- **Event permissions**: By default, `GROUP_AND_C2C_EVENT` (QQ group/direct) and `PUBLIC_GUILD_MESSAGES` (guild public messages) are subscribed. Apply for additional permissions on the open platform if needed.
diff --git a/run.sh b/run.sh
index da34abc..34ac2bf 100644
--- a/run.sh
+++ b/run.sh
@@ -409,19 +409,21 @@ select_channel() {
echo -e "${CYAN}${BOLD}=========================================${NC}"
echo -e "${YELLOW}1) Feishu (飞书)${NC}"
echo -e "${YELLOW}2) DingTalk (钉钉)${NC}"
- echo -e "${YELLOW}3) WeCom (企微应用)${NC}"
- echo -e "${YELLOW}4) Web (网页)${NC}"
+ echo -e "${YELLOW}3) WeCom Bot (企微智能机器人)${NC}"
+ echo -e "${YELLOW}4) QQ (QQ 机器人)${NC}"
+ echo -e "${YELLOW}5) WeCom App (企微自建应用)${NC}"
+ echo -e "${YELLOW}6) Web (网页)${NC}"
echo ""
while true; do
read -p "Enter your choice [press Enter for default: 1 - Feishu]: " channel_choice
channel_choice=${channel_choice:-1}
case "$channel_choice" in
- 1|2|3|4)
+ 1|2|3|4|5|6)
break
;;
*)
- echo -e "${RED}Invalid choice. Please enter 1-4.${NC}"
+ echo -e "${RED}Invalid choice. Please enter 1-6.${NC}"
;;
esac
done
@@ -456,9 +458,31 @@ configure_channel() {
ACCESS_INFO="DingTalk channel configured"
;;
3)
- # WeCom
+ # WeCom Bot
+ CHANNEL_TYPE="wecom_bot"
+ echo -e "${GREEN}Configure WeCom Bot...${NC}"
+ read -p "Enter WeCom Bot ID: " wecom_bot_id
+ read -p "Enter WeCom Bot Secret: " wecom_bot_secret
+
+ WECOM_BOT_ID="$wecom_bot_id"
+ WECOM_BOT_SECRET="$wecom_bot_secret"
+ ACCESS_INFO="WeCom Bot channel configured"
+ ;;
+ 4)
+ # QQ
+ CHANNEL_TYPE="qq"
+ echo -e "${GREEN}Configure QQ Bot...${NC}"
+ read -p "Enter QQ App ID: " qq_app_id
+ read -p "Enter QQ App Secret: " qq_app_secret
+
+ QQ_APP_ID="$qq_app_id"
+ QQ_APP_SECRET="$qq_app_secret"
+ ACCESS_INFO="QQ Bot channel configured"
+ ;;
+ 5)
+ # WeCom App
CHANNEL_TYPE="wechatcom_app"
- echo -e "${GREEN}Configure WeCom...${NC}"
+ echo -e "${GREEN}Configure WeCom App...${NC}"
read -p "Enter WeChat Corp ID: " corp_id
read -p "Enter WeChat Com App Token: " com_token
read -p "Enter WeChat Com App Secret: " com_secret
@@ -473,9 +497,9 @@ configure_channel() {
WECHATCOM_AGENT_ID="$com_agent_id"
WECHATCOM_AES_KEY="$com_aes_key"
WECHATCOM_PORT="$com_port"
- ACCESS_INFO="WeCom channel configured on port ${com_port}"
+ ACCESS_INFO="WeCom App channel configured on port ${com_port}"
;;
- 4)
+ 6)
# Web
CHANNEL_TYPE="web"
read -p "Enter web port [press Enter for default: 9899]: " web_port
@@ -600,6 +624,72 @@ EOF
"agent_max_context_turns": 30,
"agent_max_steps": 15
}
+EOF
+ ;;
+ wecom_bot)
+ cat > config.json <