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 长连接模式。 + + + QQ 机器人通过 QQ 开放平台创建,使用 WebSocket 长连接接收消息,通过 OpenAPI 发送消息,无需公网 IP 和域名。 + + +## 一、创建 QQ 机器人 + +> 进入[QQ 开放平台](https://q.qq.com),QQ扫码登录,如果未注册开放平台账号,请先完成[账号注册](https://q.qq.com/#/register)。 + +1.在 [QQ开放平台-机器人列表页](https://q.qq.com/#/apps),点击创建机器人: + + + +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. + + + QQ Bot is created through the QQ Open Platform. It uses WebSocket long connection to receive messages and OpenAPI to send messages. No public IP or domain is required. + + +## 1. Create a QQ Bot + +> Visit the [QQ Open Platform](https://q.qq.com), sign in with QQ. If you haven't registered, please complete [account registration](https://q.qq.com/#/register) first. + +1.Go to the [QQ Open Platform - Bot List](https://q.qq.com/#/apps), and click **Create Bot**: + + + +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 < config.json <