mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-06-02 09:48:22 +08:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5958b69ec9 | ||
|
|
7d4e2cb39a | ||
|
|
a483ec0cea | ||
|
|
c1421e0874 | ||
|
|
ce89869c3c | ||
|
|
b8b57e34ff | ||
|
|
bc7f627253 | ||
|
|
652156e398 | ||
|
|
9febb071c6 | ||
|
|
7d0e1568ac | ||
|
|
b4e711f411 | ||
|
|
1b5be1b981 | ||
|
|
49d8707c58 | ||
|
|
9192f6f7f7 | ||
|
|
05022e3745 | ||
|
|
5356e9ddeb | ||
|
|
52acf76e2c | ||
|
|
40cdbd3b45 | ||
|
|
5487c0befe |
70
README.md
70
README.md
@@ -4,10 +4,10 @@
|
||||
<a href="https://github.com/zhayujie/chatgpt-on-wechat/releases/latest"><img src="https://img.shields.io/github/v/release/zhayujie/chatgpt-on-wechat" alt="Latest release"></a>
|
||||
<a href="https://github.com/zhayujie/chatgpt-on-wechat/blob/master/LICENSE"><img src="https://img.shields.io/github/license/zhayujie/chatgpt-on-wechat" alt="License: MIT"></a>
|
||||
<a href="https://github.com/zhayujie/chatgpt-on-wechat"><img src="https://img.shields.io/github/stars/zhayujie/chatgpt-on-wechat?style=flat-square" alt="Stars"></a> <br/>
|
||||
[中文] | [<a href="docs/en/README.md">English</a>]
|
||||
[中文] | [<a href="docs/en/README.md">English</a>] | [<a href="docs/ja/README.md">日本語</a>]
|
||||
</p>
|
||||
|
||||
**CowAgent** 是基于大模型的超级AI助理,能够主动思考和任务规划、操作计算机和外部资源、创造和执行Skills、拥有长期记忆并不断成长。CowAgent 支持灵活切换多种模型,能处理文本、语音、图片、文件等多模态消息,可接入网页、飞书、钉钉、企微智能机器人、QQ、企微自建应用、微信公众号中使用,7*24小时运行于你的个人电脑或服务器中。
|
||||
**CowAgent** 是基于大模型的超级AI助理,能够主动思考和任务规划、操作计算机和外部资源、创造和执行Skills、拥有长期记忆并不断成长,比OpenClaw更轻量和便捷。CowAgent 支持灵活切换多种模型,能处理文本、语音、图片、文件等多模态消息,可接入微信、飞书、钉钉、企微智能机器人、QQ、企微自建应用、微信公众号、网页中使用,7*24小时运行于你的个人电脑或服务器中。
|
||||
|
||||
<p align="center">
|
||||
<a href="https://cowagent.ai/">🌐 官网</a> ·
|
||||
@@ -17,7 +17,6 @@
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
# 简介
|
||||
|
||||
> 该项目既是一个可以开箱即用的超级AI助理,也是一个支持高扩展的Agent框架,可以通过为项目扩展大模型接口、接入渠道、内置工具、Skills系统来灵活实现各种定制需求。核心能力如下:
|
||||
@@ -27,7 +26,7 @@
|
||||
- ✅ **技能系统:** 实现了Skills创建和运行的引擎,内置多种技能,并支持通过自然语言对话完成自定义Skills开发
|
||||
- ✅ **多模态消息:** 支持对文本、图片、语音、文件等多类型消息进行解析、处理、生成、发送等操作
|
||||
- ✅ **多模型接入:** 支持OpenAI, Claude, Gemini, DeepSeek, MiniMax、GLM、Qwen、Kimi、Doubao等国内外主流模型厂商
|
||||
- ✅ **多端部署:** 支持运行在本地计算机或服务器,可集成到飞书、钉钉、企业微信、QQ、微信公众号、网页中使用
|
||||
- ✅ **多端部署:** 支持运行在本地计算机或服务器,可集成到微信、飞书、钉钉、企业微信、QQ、微信公众号、网页中使用
|
||||
|
||||
## 声明
|
||||
|
||||
@@ -67,6 +66,8 @@
|
||||
|
||||
# 🏷 更新日志
|
||||
|
||||
>**2026.03.22:** [2.0.4版本](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/2.0.4),新增个人微信通道(微信扫码即用)、新增 MiniMax-M2.7 和 GLM-5-Turbo 模型、run.sh 脚本重构、日文文档及多项修复。
|
||||
|
||||
>**2026.03.18:** [2.0.3版本](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/2.0.3),新增企微智能机器人和 QQ 通道、支持Coding Plan、新增多个模型、Web端文件处理、记忆系统升级。
|
||||
|
||||
>**2026.02.27:** [2.0.2版本](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/2.0.2),Web 控制台全面升级(流式对话、模型/技能/记忆/通道/定时任务/日志管理)、支持多通道同时运行、会话持久化存储、新增多个模型。
|
||||
@@ -75,10 +76,6 @@
|
||||
|
||||
>**2026.02.03:** [2.0.0版本](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/2.0.0),正式升级为超级Agent助理,支持多轮任务决策、具备长期记忆、实现多种系统工具、支持Skills框架,新增多种模型并优化了接入渠道。
|
||||
|
||||
>**2025.05.23:** [1.7.6版本](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/1.7.6) 优化web网页channel、新增 [AgentMesh](https://github.com/zhayujie/chatgpt-on-wechat/blob/master/plugins/agent/README.md)多智能体插件、百度语音合成优化、企微应用`access_token`获取优化、支持`claude-4-sonnet`和`claude-4-opus`模型
|
||||
|
||||
>**2025.04.11:** [1.7.5版本](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/1.7.5) 新增支持 [wechatferry](https://github.com/zhayujie/chatgpt-on-wechat/pull/2562) 协议、新增 deepseek 模型、新增支持腾讯云语音能力、新增支持 ModelScope 和 Gitee-AI API接口
|
||||
|
||||
更多更新历史请查看: [更新日志](https://docs.cowagent.ai/releases)
|
||||
|
||||
<br/>
|
||||
@@ -102,7 +99,7 @@ bash <(curl -fsSL https://cdn.link-ai.tech/code/cow/run.sh)
|
||||
|
||||
项目支持国内外主流厂商的模型接口,可选模型及配置说明参考:[模型说明](#模型说明)。
|
||||
|
||||
> 注:Agent模式下推荐使用以下模型,可根据效果及成本综合选择:MiniMax-M2.5、glm-5、kimi-k2.5、qwen3.5-plus、claude-sonnet-4-6、gemini-3.1-pro-preview、gpt-5.4、gpt-5.4-mini
|
||||
> 注:Agent模式下推荐使用以下模型,可根据效果及成本综合选择:MiniMax-M2.7、glm-5-turbo、kimi-k2.5、qwen3.5-plus、claude-sonnet-4-6、gemini-3.1-pro-preview、gpt-5.4、gpt-5.4-mini
|
||||
|
||||
同时支持使用 **LinkAI平台** 接口,支持上述全部模型,并支持知识库、工作流、插件等Agent技能,参考 [接口文档](https://docs.link-ai.tech/platform/api)。
|
||||
|
||||
@@ -147,8 +144,8 @@ pip3 install -r requirements-optional.txt
|
||||
```bash
|
||||
# config.json 文件内容示例
|
||||
{
|
||||
"channel_type": "web", # 接入渠道类型,默认为web,支持修改为:feishu,dingtalk,wecom_bot,qq,wechatcom_app,wechatmp_service,wechatmp,terminal
|
||||
"model": "MiniMax-M2.5", # 模型名称
|
||||
"channel_type": "weixin", # 接入渠道类型,默认为weixin, 支持修改为 feishu,dingtalk,wecom_bot,qq,wechatcom_app,wechatmp_service,wechatmp,terminal
|
||||
"model": "MiniMax-M2.7", # 模型名称
|
||||
"minimax_api_key": "", # MiniMax API Key
|
||||
"zhipu_ai_api_key": "", # 智谱GLM API Key
|
||||
"moonshot_api_key": "", # Kimi/Moonshot API Key
|
||||
@@ -187,7 +184,7 @@ pip3 install -r requirements-optional.txt
|
||||
<details>
|
||||
<summary>2. 其他配置</summary>
|
||||
|
||||
+ `model`: 模型名称,Agent模式下推荐使用 `MiniMax-M2.5`、`glm-5`、`kimi-k2.5`、`qwen3.5-plus`、`claude-sonnet-4-6`、`gemini-3.1-pro-preview`,全部模型名称参考[common/const.py](https://github.com/zhayujie/chatgpt-on-wechat/blob/master/common/const.py)文件
|
||||
+ `model`: 模型名称,Agent模式下推荐使用 `MiniMax-M2.7`、`glm-5-turbo`、`kimi-k2.5`、`qwen3.5-plus`、`claude-sonnet-4-6`、`gemini-3.1-pro-preview`,全部模型名称参考[common/const.py](https://github.com/zhayujie/chatgpt-on-wechat/blob/master/common/const.py)文件
|
||||
+ `character_desc`:普通对话模式下的机器人系统提示词。在Agent模式下该配置不生效,由工作空间中的文件内容构成。
|
||||
+ `subscribe_msg`:订阅消息,公众号和企业微信channel中请填写,当被订阅时会自动回复, 可使用特殊占位符。目前支持的占位符有{trigger_prefix},在程序中它会自动替换成bot的触发词。
|
||||
</details>
|
||||
@@ -312,24 +309,24 @@ sudo docker logs -f chatgpt-on-wechat
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "MiniMax-M2.5",
|
||||
"model": "MiniMax-M2.7",
|
||||
"minimax_api_key": ""
|
||||
}
|
||||
```
|
||||
- `model`: 可填写 `MiniMax-M2.5、MiniMax-M2.1、MiniMax-M2.1-lightning、MiniMax-M2、abab6.5-chat` 等
|
||||
- `model`: 可填写 `MiniMax-M2.7、MiniMax-M2.5、MiniMax-M2.1、MiniMax-M2.1-lightning、MiniMax-M2、abab6.5-chat` 等
|
||||
- `minimax_api_key`:MiniMax平台的API-KEY,在 [控制台](https://platform.minimaxi.com/user-center/basic-information/interface-key) 创建
|
||||
|
||||
方式二:OpenAI兼容方式接入,配置如下:
|
||||
```json
|
||||
{
|
||||
"bot_type": "openai",
|
||||
"model": "MiniMax-M2.5",
|
||||
"model": "MiniMax-M2.7",
|
||||
"open_ai_api_base": "https://api.minimaxi.com/v1",
|
||||
"open_ai_api_key": ""
|
||||
}
|
||||
```
|
||||
- `bot_type`: OpenAI兼容方式
|
||||
- `model`: 可填 `MiniMax-M2.5、MiniMax-M2.1、MiniMax-M2.1-lightning、MiniMax-M2`,参考[API文档](https://platform.minimaxi.com/document/%E5%AF%B9%E8%AF%9D?key=66701d281d57f38758d581d0#QklxsNSbaf6kM4j6wjO5eEek)
|
||||
- `model`: 可填 `MiniMax-M2.7、MiniMax-M2.5、MiniMax-M2.1、MiniMax-M2.1-lightning、MiniMax-M2`,参考[API文档](https://platform.minimaxi.com/document/%E5%AF%B9%E8%AF%9D?key=66701d281d57f38758d581d0#QklxsNSbaf6kM4j6wjO5eEek)
|
||||
- `open_ai_api_base`: MiniMax平台API的 BASE URL
|
||||
- `open_ai_api_key`: MiniMax平台的API-KEY
|
||||
</details>
|
||||
@@ -341,24 +338,24 @@ sudo docker logs -f chatgpt-on-wechat
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "glm-5",
|
||||
"model": "glm-5-turbo",
|
||||
"zhipu_ai_api_key": ""
|
||||
}
|
||||
```
|
||||
- `model`: 可填 `glm-5、glm-4.7、glm-4-plus、glm-4-flash、glm-4-air、glm-4-airx、glm-4-long` 等, 参考 [glm系列模型编码](https://bigmodel.cn/dev/api/normal-model/glm-4)
|
||||
- `model`: 可填 `glm-5-turbo、glm-5、glm-4.7、glm-4-plus、glm-4-flash、glm-4-air、glm-4-airx、glm-4-long` 等, 参考 [glm系列模型编码](https://bigmodel.cn/dev/api/normal-model/glm-4)
|
||||
- `zhipu_ai_api_key`: 智谱AI平台的 API KEY,在 [控制台](https://www.bigmodel.cn/usercenter/proj-mgmt/apikeys) 创建
|
||||
|
||||
方式二:OpenAI兼容方式接入,配置如下:
|
||||
```json
|
||||
{
|
||||
"bot_type": "openai",
|
||||
"model": "glm-5",
|
||||
"model": "glm-5-turbo",
|
||||
"open_ai_api_base": "https://open.bigmodel.cn/api/paas/v4",
|
||||
"open_ai_api_key": ""
|
||||
}
|
||||
```
|
||||
- `bot_type`: OpenAI兼容方式
|
||||
- `model`: 可填 `glm-5、glm-4.7、glm-4-plus、glm-4-flash、glm-4-air、glm-4-airx、glm-4-long` 等
|
||||
- `model`: 可填 `glm-5-turbo、glm-5、glm-4.7、glm-4-plus、glm-4-flash、glm-4-air、glm-4-airx、glm-4-long` 等
|
||||
- `open_ai_api_base`: 智谱AI平台的 BASE URL
|
||||
- `open_ai_api_key`: 智谱AI平台的 API KEY
|
||||
</details>
|
||||
@@ -628,7 +625,24 @@ Coding Plan 是各厂商推出的编程包月套餐,所有厂商均可通过 O
|
||||
支持同时可接入多个通道,配置时可通过逗号进行分割,例如 `"channel_type": "feishu,dingtalk"`。
|
||||
|
||||
<details>
|
||||
<summary>1. Web</summary>
|
||||
<summary>1. Weixin - 微信</summary>
|
||||
|
||||
接入个人微信,扫码登录即可使用,支持文本、图片、语音、文件等消息收发。
|
||||
|
||||
```json
|
||||
{
|
||||
"channel_type": "weixin"
|
||||
}
|
||||
```
|
||||
|
||||
启动后终端会显示二维码,使用微信扫码授权即可,也可以在 Web 控制台的「通道」页面中扫码接入。登录凭证会自动保存至 `~/.weixin_cow_credentials.json`,下次启动无需重新扫码,如需重新登录删除该文件后重启即可。
|
||||
|
||||
详细步骤和参数说明参考 [微信接入](https://docs.cowagent.ai/channels/weixin)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>2. Web</summary>
|
||||
|
||||
项目启动后会默认运行Web控制台,配置如下:
|
||||
|
||||
@@ -645,7 +659,7 @@ Coding Plan 是各厂商推出的编程包月套餐,所有厂商均可通过 O
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>2. Feishu - 飞书</summary>
|
||||
<summary>3. Feishu - 飞书</summary>
|
||||
|
||||
飞书支持两种事件接收模式:WebSocket 长连接(推荐)和 Webhook。
|
||||
|
||||
@@ -681,7 +695,7 @@ Coding Plan 是各厂商推出的编程包月套餐,所有厂商均可通过 O
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>3. DingTalk - 钉钉</summary>
|
||||
<summary>4. DingTalk - 钉钉</summary>
|
||||
|
||||
钉钉需要在开放平台创建智能机器人应用,将以下配置填入 `config.json`:
|
||||
|
||||
@@ -696,7 +710,7 @@ Coding Plan 是各厂商推出的编程包月套餐,所有厂商均可通过 O
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>4. WeCom Bot - 企微智能机器人</summary>
|
||||
<summary>5. WeCom Bot - 企微智能机器人</summary>
|
||||
|
||||
企微智能机器人使用 WebSocket 长连接模式,无需公网 IP 和域名,配置简单:
|
||||
|
||||
@@ -712,7 +726,7 @@ Coding Plan 是各厂商推出的编程包月套餐,所有厂商均可通过 O
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>5. QQ - QQ 机器人</summary>
|
||||
<summary>6. QQ - QQ 机器人</summary>
|
||||
|
||||
QQ 机器人使用 WebSocket 长连接模式,无需公网 IP 和域名,支持 QQ 单聊、群聊和频道消息:
|
||||
|
||||
@@ -728,7 +742,7 @@ QQ 机器人使用 WebSocket 长连接模式,无需公网 IP 和域名,支
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>6. WeCom App - 企业微信应用</summary>
|
||||
<summary>7. WeCom App - 企业微信应用</summary>
|
||||
|
||||
企业微信自建应用接入需在后台创建应用并启用消息回调,配置示例:
|
||||
|
||||
@@ -748,7 +762,7 @@ QQ 机器人使用 WebSocket 长连接模式,无需公网 IP 和域名,支
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>7. WeChat MP - 微信公众号</summary>
|
||||
<summary>8. WeChat MP - 微信公众号</summary>
|
||||
|
||||
本项目支持订阅号和服务号两种公众号,通过服务号(`wechatmp_service`)体验更佳。
|
||||
|
||||
@@ -783,7 +797,7 @@ QQ 机器人使用 WebSocket 长连接模式,无需公网 IP 和域名,支
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>8. Terminal - 终端</summary>
|
||||
<summary>9. Terminal - 终端</summary>
|
||||
|
||||
修改 `config.json` 中的 `channel_type` 字段:
|
||||
|
||||
|
||||
@@ -44,6 +44,11 @@ class ChatService:
|
||||
if agent is None:
|
||||
raise RuntimeError("Failed to initialise agent for the session")
|
||||
|
||||
# Pass context metadata to model for downstream API requests
|
||||
if hasattr(agent, 'model'):
|
||||
agent.model.channel_type = channel_type or ""
|
||||
agent.model.session_id = session_id or ""
|
||||
|
||||
# State shared between the event callback and this method
|
||||
state = _StreamState()
|
||||
|
||||
|
||||
@@ -32,18 +32,21 @@ class EmbeddingProvider(ABC):
|
||||
class OpenAIEmbeddingProvider(EmbeddingProvider):
|
||||
"""OpenAI embedding provider using REST API"""
|
||||
|
||||
def __init__(self, model: str = "text-embedding-3-small", api_key: Optional[str] = None, api_base: Optional[str] = None):
|
||||
def __init__(self, model: str = "text-embedding-3-small", api_key: Optional[str] = None,
|
||||
api_base: Optional[str] = None, extra_headers: Optional[dict] = None):
|
||||
"""
|
||||
Initialize OpenAI embedding provider
|
||||
|
||||
|
||||
Args:
|
||||
model: Model name (text-embedding-3-small or text-embedding-3-large)
|
||||
api_key: OpenAI API key
|
||||
api_base: Optional API base URL
|
||||
extra_headers: Optional extra headers to include in API requests
|
||||
"""
|
||||
self.model = model
|
||||
self.api_key = api_key
|
||||
self.api_base = api_base or "https://api.openai.com/v1"
|
||||
self.extra_headers = extra_headers or {}
|
||||
|
||||
# Validate API key
|
||||
if not self.api_key or self.api_key in ["", "YOUR API KEY", "YOUR_API_KEY"]:
|
||||
@@ -59,7 +62,8 @@ class OpenAIEmbeddingProvider(EmbeddingProvider):
|
||||
url = f"{self.api_base}/embeddings"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self.api_key}"
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
**self.extra_headers,
|
||||
}
|
||||
data = {
|
||||
"input": input_data,
|
||||
@@ -134,7 +138,8 @@ def create_embedding_provider(
|
||||
provider: str = "openai",
|
||||
model: Optional[str] = None,
|
||||
api_key: Optional[str] = None,
|
||||
api_base: Optional[str] = None
|
||||
api_base: Optional[str] = None,
|
||||
extra_headers: Optional[dict] = None
|
||||
) -> EmbeddingProvider:
|
||||
"""
|
||||
Factory function to create embedding provider
|
||||
@@ -147,10 +152,11 @@ def create_embedding_provider(
|
||||
model: Model name (default: text-embedding-3-small)
|
||||
api_key: API key (required)
|
||||
api_base: API base URL
|
||||
|
||||
extra_headers: Optional extra headers to include in API requests
|
||||
|
||||
Returns:
|
||||
EmbeddingProvider instance
|
||||
|
||||
|
||||
Raises:
|
||||
ValueError: If provider is unsupported or api_key is missing
|
||||
"""
|
||||
@@ -158,4 +164,4 @@ def create_embedding_provider(
|
||||
raise ValueError(f"Unsupported embedding provider: {provider}. Use 'openai' or 'linkai'.")
|
||||
|
||||
model = model or "text-embedding-3-small"
|
||||
return OpenAIEmbeddingProvider(model=model, api_key=api_key, api_base=api_base)
|
||||
return OpenAIEmbeddingProvider(model=model, api_key=api_key, api_base=api_base, extra_headers=extra_headers)
|
||||
|
||||
@@ -76,11 +76,15 @@ class MemoryManager:
|
||||
linkai_key = os.environ.get('LINKAI_API_KEY')
|
||||
linkai_base = os.environ.get('LINKAI_API_BASE', 'https://api.link-ai.tech')
|
||||
if linkai_key:
|
||||
from common.utils import get_cloud_headers
|
||||
cloud_headers = get_cloud_headers(linkai_key)
|
||||
cloud_headers.pop("Authorization", None)
|
||||
self.embedding_provider = create_embedding_provider(
|
||||
provider="linkai",
|
||||
model=self.config.embedding_model,
|
||||
api_key=linkai_key,
|
||||
api_base=f"{linkai_base}/v1"
|
||||
api_base=f"{linkai_base}/v1",
|
||||
extra_headers=cloud_headers,
|
||||
)
|
||||
except Exception as e:
|
||||
from common.log import logger
|
||||
|
||||
@@ -18,6 +18,107 @@ from typing import Dict, List, Set
|
||||
|
||||
from common.log import logger
|
||||
|
||||
_SYNTH_TOOL_ERR = (
|
||||
"Error: Missing tool_result adjacent to tool_use (session repair). "
|
||||
"The conversation history was inconsistent; continue from here."
|
||||
)
|
||||
|
||||
|
||||
def _repair_tool_use_adjacency(messages: List[Dict]) -> int:
|
||||
"""
|
||||
Anthropic requires: after assistant content with tool_use, the next message
|
||||
must be user content listing tool_result for every tool_use id (same user msg).
|
||||
|
||||
Valid histories satisfy this at every such assistant; the loop only mutates
|
||||
when that condition fails (broken persistence, bad trims, etc.).
|
||||
"""
|
||||
|
||||
def _synth_block(tid: str) -> Dict:
|
||||
return {
|
||||
"type": "tool_result",
|
||||
"tool_use_id": tid,
|
||||
"content": _SYNTH_TOOL_ERR,
|
||||
"is_error": True,
|
||||
}
|
||||
|
||||
repairs = 0
|
||||
i = 0
|
||||
while i < len(messages):
|
||||
msg = messages[i]
|
||||
if msg.get("role") != "assistant":
|
||||
i += 1
|
||||
continue
|
||||
|
||||
content = msg.get("content", [])
|
||||
if not isinstance(content, list):
|
||||
i += 1
|
||||
continue
|
||||
|
||||
required = [
|
||||
b.get("id")
|
||||
for b in content
|
||||
if isinstance(b, dict) and b.get("type") == "tool_use" and b.get("id")
|
||||
]
|
||||
if not required:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
req_set = set(required)
|
||||
if i + 1 >= len(messages):
|
||||
messages.append({
|
||||
"role": "user",
|
||||
"content": [_synth_block(tid) for tid in required],
|
||||
})
|
||||
logger.warning(
|
||||
"⚠️ Appended synthetic tool_result after trailing assistant tool_use"
|
||||
)
|
||||
repairs += 1
|
||||
break
|
||||
|
||||
nxt = messages[i + 1]
|
||||
if nxt.get("role") != "user":
|
||||
messages.insert(
|
||||
i + 1,
|
||||
{"role": "user", "content": [_synth_block(tid) for tid in required]},
|
||||
)
|
||||
logger.warning(
|
||||
"⚠️ Inserted synthetic tool_result user after tool_use "
|
||||
f"(next role={nxt.get('role')!r})"
|
||||
)
|
||||
repairs += 1
|
||||
i += 2
|
||||
continue
|
||||
|
||||
nc = nxt.get("content", [])
|
||||
if not isinstance(nc, list):
|
||||
messages.insert(
|
||||
i + 1,
|
||||
{"role": "user", "content": [_synth_block(tid) for tid in required]},
|
||||
)
|
||||
repairs += 1
|
||||
i += 2
|
||||
continue
|
||||
|
||||
present = {
|
||||
b.get("tool_use_id")
|
||||
for b in nc
|
||||
if isinstance(b, dict) and b.get("type") == "tool_result" and b.get("tool_use_id")
|
||||
}
|
||||
if req_set <= present:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
missing = [tid for tid in required if tid not in present]
|
||||
nxt["content"] = [_synth_block(tid) for tid in missing] + nc
|
||||
logger.warning(
|
||||
"⚠️ Prepended synthetic tool_result for Anthropic adjacency "
|
||||
f"(missing_ids={missing})"
|
||||
)
|
||||
repairs += len(missing)
|
||||
i += 1
|
||||
|
||||
return repairs
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Claude-format sanitizer (used by agent_stream)
|
||||
@@ -28,33 +129,21 @@ def sanitize_claude_messages(messages: List[Dict]) -> int:
|
||||
Validate and fix a Claude-format message list **in-place**.
|
||||
|
||||
Fixes handled:
|
||||
- Trailing assistant message with tool_use but no following tool_result
|
||||
- Anthropic adjacency: assistant tool_use must be immediately followed by
|
||||
user message(s) containing matching tool_result blocks
|
||||
- Leading orphaned tool_result user messages
|
||||
- Mid-list tool_result blocks whose tool_use_id has no matching
|
||||
tool_use in any preceding assistant message
|
||||
|
||||
Returns the number of messages / blocks removed.
|
||||
Returns: number of removals plus adjacency repair operations (inserts/prepends).
|
||||
"""
|
||||
if not messages:
|
||||
return 0
|
||||
|
||||
removed = 0
|
||||
|
||||
# 1. Remove trailing incomplete tool_use assistant messages
|
||||
while messages:
|
||||
last = messages[-1]
|
||||
if last.get("role") != "assistant":
|
||||
break
|
||||
content = last.get("content", [])
|
||||
if isinstance(content, list) and any(
|
||||
isinstance(b, dict) and b.get("type") == "tool_use"
|
||||
for b in content
|
||||
):
|
||||
logger.warning("⚠️ Removing trailing incomplete tool_use assistant message")
|
||||
messages.pop()
|
||||
removed += 1
|
||||
else:
|
||||
break
|
||||
# 1. Adjacency repair (Anthropic: tool_result must be in the next user message)
|
||||
adj_repairs = _repair_tool_use_adjacency(messages)
|
||||
|
||||
# 2. Remove leading orphaned tool_result user messages
|
||||
while messages:
|
||||
@@ -136,9 +225,15 @@ def sanitize_claude_messages(messages: List[Dict]) -> int:
|
||||
if pass_removed == 0:
|
||||
break
|
||||
|
||||
# 4. Removals above can break adjacency; re-run repair only if something was removed.
|
||||
if removed:
|
||||
adj_repairs += _repair_tool_use_adjacency(messages)
|
||||
|
||||
if removed:
|
||||
logger.info(f"🔧 Message validation: removed {removed} broken message(s)")
|
||||
return removed
|
||||
if adj_repairs:
|
||||
logger.info(f"🔧 Message validation: adjacency repairs={adj_repairs}")
|
||||
return removed + adj_repairs
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@@ -82,7 +82,7 @@ class Vision(BaseTool):
|
||||
if not question:
|
||||
return ToolResult.fail("Error: 'question' parameter is required")
|
||||
|
||||
api_key, api_base = self._resolve_provider()
|
||||
api_key, api_base, extra_headers = self._resolve_provider()
|
||||
if not api_key:
|
||||
return ToolResult.fail(
|
||||
"Error: No API key configured for Vision.\n"
|
||||
@@ -98,7 +98,7 @@ class Vision(BaseTool):
|
||||
return ToolResult.fail(f"Error: {e}")
|
||||
|
||||
try:
|
||||
return self._call_api(api_key, api_base, model, question, image_content)
|
||||
return self._call_api(api_key, api_base, model, question, image_content, extra_headers)
|
||||
except requests.Timeout:
|
||||
return ToolResult.fail(f"Error: Vision API request timed out after {DEFAULT_TIMEOUT}s")
|
||||
except requests.ConnectionError:
|
||||
@@ -107,22 +107,26 @@ class Vision(BaseTool):
|
||||
logger.error(f"[Vision] Unexpected error: {e}", exc_info=True)
|
||||
return ToolResult.fail(f"Error: Vision API call failed - {e}")
|
||||
|
||||
def _resolve_provider(self) -> Tuple[Optional[str], str]:
|
||||
"""Resolve API key and base URL. Priority: conf() > env vars."""
|
||||
def _resolve_provider(self) -> Tuple[Optional[str], str, dict]:
|
||||
"""Resolve API key, base URL and extra headers. Priority: conf() > env vars."""
|
||||
api_key = conf().get("open_ai_api_key") or os.environ.get("OPENAI_API_KEY")
|
||||
if api_key:
|
||||
api_base = (conf().get("open_ai_api_base") or os.environ.get("OPENAI_API_BASE", "")).rstrip("/") \
|
||||
or "https://api.openai.com/v1"
|
||||
return api_key, self._ensure_v1(api_base)
|
||||
return api_key, self._ensure_v1(api_base), {}
|
||||
|
||||
api_key = conf().get("linkai_api_key") or os.environ.get("LINKAI_API_KEY")
|
||||
if api_key:
|
||||
api_base = (conf().get("linkai_api_base") or os.environ.get("LINKAI_API_BASE", "")).rstrip("/") \
|
||||
or "https://api.link-ai.tech"
|
||||
logger.debug("[Vision] Using LinkAI API (OPENAI_API_KEY not set)")
|
||||
return api_key, self._ensure_v1(api_base)
|
||||
from common.utils import get_cloud_headers
|
||||
extra = get_cloud_headers(api_key)
|
||||
extra.pop("Authorization", None)
|
||||
extra.pop("Content-Type", None)
|
||||
return api_key, self._ensure_v1(api_base), extra
|
||||
|
||||
return None, ""
|
||||
return None, "", {}
|
||||
|
||||
@staticmethod
|
||||
def _ensure_v1(api_base: str) -> str:
|
||||
@@ -197,7 +201,7 @@ class Vision(BaseTool):
|
||||
return path
|
||||
|
||||
def _call_api(self, api_key: str, api_base: str, model: str,
|
||||
question: str, image_content: dict) -> ToolResult:
|
||||
question: str, image_content: dict, extra_headers: dict = None) -> ToolResult:
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": [
|
||||
@@ -215,6 +219,7 @@ class Vision(BaseTool):
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
**(extra_headers or {}),
|
||||
}
|
||||
|
||||
resp = requests.post(
|
||||
|
||||
@@ -225,10 +225,8 @@ class WebSearch(BaseTool):
|
||||
api_base = conf().get("linkai_api_base", "https://api.link-ai.tech")
|
||||
url = f"{api_base.rstrip('/')}/v1/plugin/execute"
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {api_key}"
|
||||
}
|
||||
from common.utils import get_cloud_headers
|
||||
headers = get_cloud_headers(api_key)
|
||||
|
||||
payload = {
|
||||
"code": "web-search",
|
||||
|
||||
10
app.py
10
app.py
@@ -78,7 +78,13 @@ class ChannelManager:
|
||||
if first_start:
|
||||
PluginManager().load_plugins()
|
||||
|
||||
if conf().get("use_linkai"):
|
||||
# Cloud client is optional. It is only started when
|
||||
# use_linkai=True AND cloud_deployment_id is set.
|
||||
# By default neither is configured, so the app runs
|
||||
# entirely locally without any remote connection.
|
||||
if conf().get("use_linkai") and (
|
||||
os.environ.get("CLOUD_DEPLOYMENT_ID") or conf().get("cloud_deployment_id")
|
||||
):
|
||||
try:
|
||||
from common import cloud_client
|
||||
threading.Thread(
|
||||
@@ -229,6 +235,8 @@ def _clear_singleton_cache(channel_name: str):
|
||||
const.DINGTALK: "channel.dingtalk.dingtalk_channel.DingTalkChanel",
|
||||
const.WECOM_BOT: "channel.wecom_bot.wecom_bot_channel.WecomBotChannel",
|
||||
const.QQ: "channel.qq.qq_channel.QQChannel",
|
||||
const.WEIXIN: "channel.weixin.weixin_channel.WeixinChannel",
|
||||
"wx": "channel.weixin.weixin_channel.WeixinChannel",
|
||||
}
|
||||
module_path = cls_map.get(channel_name)
|
||||
if not module_path:
|
||||
|
||||
@@ -152,12 +152,20 @@ class AgentLLMModel(LLMModel):
|
||||
# Only pass max_tokens if it's explicitly set
|
||||
if request.max_tokens is not None:
|
||||
kwargs['max_tokens'] = request.max_tokens
|
||||
|
||||
|
||||
# Extract system prompt if present
|
||||
system_prompt = getattr(request, 'system', None)
|
||||
if system_prompt:
|
||||
kwargs['system'] = system_prompt
|
||||
|
||||
|
||||
# Pass context metadata to bot
|
||||
channel_type = getattr(self, 'channel_type', None)
|
||||
if channel_type:
|
||||
kwargs['channel_type'] = channel_type
|
||||
session_id = getattr(self, 'session_id', None)
|
||||
if session_id:
|
||||
kwargs['session_id'] = session_id
|
||||
|
||||
response = self.bot.call_with_tools(**kwargs)
|
||||
return self._format_response(response)
|
||||
else:
|
||||
@@ -195,10 +203,13 @@ class AgentLLMModel(LLMModel):
|
||||
if system_prompt:
|
||||
kwargs['system'] = system_prompt
|
||||
|
||||
# Pass channel_type for linkai tracking
|
||||
# Pass context metadata to bot
|
||||
channel_type = getattr(self, 'channel_type', None)
|
||||
if channel_type:
|
||||
kwargs['channel_type'] = channel_type
|
||||
session_id = getattr(self, 'session_id', None)
|
||||
if session_id:
|
||||
kwargs['session_id'] = session_id
|
||||
|
||||
stream = self.bot.call_with_tools(**kwargs)
|
||||
|
||||
@@ -375,9 +386,10 @@ class AgentBridge:
|
||||
logger.warning(f"[AgentBridge] Failed to attach context to scheduler: {e}")
|
||||
break
|
||||
|
||||
# Pass channel_type to model so linkai requests carry it
|
||||
# Pass context metadata to model for downstream API requests
|
||||
if context and hasattr(agent, 'model'):
|
||||
agent.model.channel_type = context.get("channel_type", "")
|
||||
agent.model.session_id = session_id or ""
|
||||
|
||||
# Store session_id on agent so executor can clear DB on fatal errors
|
||||
agent._current_session_id = session_id
|
||||
|
||||
@@ -39,6 +39,10 @@ def create_channel(channel_type) -> Channel:
|
||||
elif channel_type == const.QQ:
|
||||
from channel.qq.qq_channel import QQChannel
|
||||
ch = QQChannel()
|
||||
elif channel_type in (const.WEIXIN, "wx"):
|
||||
from channel.weixin.weixin_channel import WeixinChannel
|
||||
ch = WeixinChannel()
|
||||
channel_type = const.WEIXIN
|
||||
else:
|
||||
raise RuntimeError
|
||||
ch.channel_type = channel_type
|
||||
|
||||
@@ -454,7 +454,7 @@ class FeiShuChanel(ChatChannel):
|
||||
can_reply = is_group and msg and hasattr(msg, 'msg_id') and msg.msg_id
|
||||
|
||||
# Build content JSON
|
||||
content_json = json.dumps(reply_content) if content_key is None else json.dumps({content_key: reply_content})
|
||||
content_json = json.dumps(reply_content, ensure_ascii=False) if content_key is None else json.dumps({content_key: reply_content}, ensure_ascii=False)
|
||||
logger.debug(f"[FeiShu] Sending message: msg_type={msg_type}, content={content_json[:200]}")
|
||||
|
||||
if can_reply:
|
||||
|
||||
@@ -23,6 +23,7 @@ from channel.qq.qq_message import QQMessage
|
||||
from common.expired_dict import ExpiredDict
|
||||
from common.log import logger
|
||||
from common.singleton import singleton
|
||||
from common.ws_client_compat import websocket_app_run_forever
|
||||
from config import conf
|
||||
|
||||
# Rich media file_type constants
|
||||
@@ -210,7 +211,7 @@ class QQChannel(ChatChannel):
|
||||
|
||||
def run_forever():
|
||||
try:
|
||||
self._ws.run_forever(ping_interval=0, reconnect=0)
|
||||
websocket_app_run_forever(self._ws, ping_interval=0, reconnect=0)
|
||||
except (SystemExit, KeyboardInterrupt):
|
||||
logger.info("[QQ] WebSocket thread interrupted")
|
||||
except Exception as e:
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// =====================================================================
|
||||
// Version — update this before each release
|
||||
// =====================================================================
|
||||
const APP_VERSION = 'v2.0.3';
|
||||
const APP_VERSION = 'v2.0.4';
|
||||
|
||||
// =====================================================================
|
||||
// i18n
|
||||
@@ -51,6 +51,11 @@ const I18N = {
|
||||
channels_empty: '暂未接入任何通道', channels_empty_desc: '点击右上角「接入通道」按钮开始配置',
|
||||
channels_disconnect_confirm: '确认断开该通道?配置将保留但通道会停止运行。',
|
||||
channels_connected: '已接入', channels_connecting: '接入中...',
|
||||
weixin_scan_title: '微信扫码登录', weixin_scan_desc: '请使用微信扫描下方二维码',
|
||||
weixin_scan_loading: '正在获取二维码...', weixin_scan_waiting: '等待扫码...',
|
||||
weixin_scan_scanned: '已扫码,请在手机上确认', weixin_scan_expired: '二维码已过期,正在刷新...',
|
||||
weixin_scan_success: '登录成功,正在启动通道...', weixin_scan_fail: '获取二维码失败',
|
||||
weixin_qr_tip: '二维码约2分钟后过期',
|
||||
tasks_title: '定时任务', tasks_desc: '查看和管理定时任务',
|
||||
tasks_coming: '即将推出', tasks_coming_desc: '定时任务管理功能即将在此提供',
|
||||
logs_title: '日志', logs_desc: '实时日志输出 (run.log)',
|
||||
@@ -97,6 +102,11 @@ const I18N = {
|
||||
channels_empty: 'No channels connected', channels_empty_desc: 'Click the "Connect" button above to get started',
|
||||
channels_disconnect_confirm: 'Disconnect this channel? Config will be preserved but the channel will stop.',
|
||||
channels_connected: 'Connected', channels_connecting: 'Connecting...',
|
||||
weixin_scan_title: 'WeChat QR Login', weixin_scan_desc: 'Scan the QR code below with WeChat',
|
||||
weixin_scan_loading: 'Loading QR code...', weixin_scan_waiting: 'Waiting for scan...',
|
||||
weixin_scan_scanned: 'Scanned, please confirm on your phone', weixin_scan_expired: 'QR code expired, refreshing...',
|
||||
weixin_scan_success: 'Login successful, starting channel...', weixin_scan_fail: 'Failed to load QR code',
|
||||
weixin_qr_tip: 'QR code expires in ~2 minutes',
|
||||
tasks_title: 'Scheduled Tasks', tasks_desc: 'View and manage scheduled tasks',
|
||||
tasks_coming: 'Coming Soon', tasks_coming_desc: 'Scheduled task management will be available here',
|
||||
logs_title: 'Logs', logs_desc: 'Real-time log output (run.log)',
|
||||
@@ -1583,6 +1593,8 @@ function loadChannelsView() {
|
||||
}
|
||||
|
||||
function renderActiveChannels() {
|
||||
stopWeixinQrPoll();
|
||||
stopWeixinStatusPoll();
|
||||
const container = document.getElementById('channels-content');
|
||||
container.innerHTML = '';
|
||||
closeAddChannelPanel();
|
||||
@@ -1608,17 +1620,30 @@ function renderActiveChannels() {
|
||||
card.id = `channel-card-${ch.name}`;
|
||||
|
||||
const fieldsHtml = buildChannelFieldsHtml(ch.name, ch.fields || []);
|
||||
const hasFields = (ch.fields || []).length > 0;
|
||||
|
||||
const weixinWaiting = ch.name === 'weixin' && ch.login_status && ch.login_status !== 'logged_in';
|
||||
let statusDot, statusText;
|
||||
if (weixinWaiting) {
|
||||
statusDot = 'bg-amber-400 animate-pulse';
|
||||
statusText = ch.login_status === 'scanned'
|
||||
? `<span class="text-xs text-primary-500">${t('weixin_scan_scanned')}</span>`
|
||||
: `<span class="text-xs text-amber-500">${t('weixin_scan_waiting')}</span>`;
|
||||
} else {
|
||||
statusDot = 'bg-primary-400';
|
||||
statusText = `<span class="text-xs text-primary-500">${t('channels_connected')}</span>`;
|
||||
}
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="flex items-center gap-4 mb-5">
|
||||
<div class="flex items-center gap-4${hasFields || weixinWaiting ? ' mb-5' : ''}">
|
||||
<div class="w-10 h-10 rounded-xl bg-${ch.color}-50 dark:bg-${ch.color}-900/20 flex items-center justify-center flex-shrink-0">
|
||||
<i class="fas ${ch.icon} text-${ch.color}-500 text-base"></i>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-semibold text-slate-800 dark:text-slate-100">${escapeHtml(label)}</span>
|
||||
<span class="w-2 h-2 rounded-full bg-primary-400"></span>
|
||||
<span class="text-xs text-primary-500">${t('channels_connected')}</span>
|
||||
<span class="w-2 h-2 rounded-full ${statusDot}"></span>
|
||||
${statusText}
|
||||
</div>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5 font-mono">${escapeHtml(ch.name)}</p>
|
||||
</div>
|
||||
@@ -1630,7 +1655,14 @@ function renderActiveChannels() {
|
||||
${t('channels_disconnect')}
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
${weixinWaiting ? `<div id="weixin-active-qr" class="flex flex-col items-center py-2">
|
||||
<button onclick="showWeixinActiveQr()"
|
||||
class="px-4 py-2 rounded-lg bg-primary-500 hover:bg-primary-600 text-white text-sm font-medium
|
||||
cursor-pointer transition-colors duration-150">
|
||||
${t('weixin_scan_title')}
|
||||
</button>
|
||||
</div>` : ''}
|
||||
${hasFields ? `<div class="space-y-4">
|
||||
${fieldsHtml}
|
||||
<div class="flex items-center justify-end gap-3 pt-1">
|
||||
<span id="ch-status-${ch.name}" class="text-xs text-primary-500 opacity-0 transition-opacity duration-300"></span>
|
||||
@@ -1639,10 +1671,14 @@ function renderActiveChannels() {
|
||||
cursor-pointer transition-colors duration-150 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
id="ch-save-${ch.name}">${t('channels_save')}</button>
|
||||
</div>
|
||||
</div>`;
|
||||
</div>` : ''}`;
|
||||
|
||||
container.appendChild(card);
|
||||
bindSecretFieldEvents(card);
|
||||
|
||||
if (weixinWaiting) {
|
||||
startWeixinActiveStatusPoll();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1774,6 +1810,9 @@ function openAddChannelPanel() {
|
||||
const activeNames = new Set(channelsData.filter(c => c.active).map(c => c.name));
|
||||
const available = channelsData.filter(c => !activeNames.has(c.name));
|
||||
|
||||
const content = document.getElementById('channels-content');
|
||||
if (activeNames.size === 0 && content) content.classList.add('hidden');
|
||||
|
||||
if (available.length === 0) {
|
||||
panel.innerHTML = `<div class="bg-white dark:bg-[#1A1A1A] rounded-xl border border-slate-200 dark:border-white/10 p-6 text-center">
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">${currentLang === 'zh' ? '所有通道均已接入' : 'All channels are already connected'}</p>
|
||||
@@ -1828,14 +1867,18 @@ function openAddChannelPanel() {
|
||||
}
|
||||
|
||||
function closeAddChannelPanel() {
|
||||
stopWeixinQrPoll();
|
||||
const panel = document.getElementById('channels-add-panel');
|
||||
if (panel) {
|
||||
panel.classList.add('hidden');
|
||||
panel.innerHTML = '';
|
||||
}
|
||||
const content = document.getElementById('channels-content');
|
||||
if (content) content.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function onAddChannelSelect(chName) {
|
||||
stopWeixinQrPoll();
|
||||
const fieldsContainer = document.getElementById('add-channel-fields');
|
||||
const actions = document.getElementById('add-channel-actions');
|
||||
|
||||
@@ -1845,6 +1888,16 @@ function onAddChannelSelect(chName) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (chName === 'weixin') {
|
||||
actions.classList.add('hidden');
|
||||
fieldsContainer.innerHTML = `
|
||||
<div id="weixin-qr-panel" class="flex flex-col items-center py-4">
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400 mb-4">${t('weixin_scan_loading')}</p>
|
||||
</div>`;
|
||||
startWeixinQrLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
const ch = channelsData.find(c => c.name === chName);
|
||||
if (!ch) return;
|
||||
|
||||
@@ -1900,6 +1953,172 @@ function submitAddChannel() {
|
||||
});
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// WeChat QR Login
|
||||
// =====================================================================
|
||||
let _weixinQrPollTimer = null;
|
||||
let _weixinStatusPollTimer = null;
|
||||
|
||||
function stopWeixinStatusPoll() {
|
||||
if (_weixinStatusPollTimer) {
|
||||
clearTimeout(_weixinStatusPollTimer);
|
||||
_weixinStatusPollTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function startWeixinActiveStatusPoll() {
|
||||
stopWeixinStatusPoll();
|
||||
_weixinStatusPollTimer = setTimeout(() => {
|
||||
fetch('/api/channels').then(r => r.json()).then(data => {
|
||||
if (data.status !== 'success') return;
|
||||
const wx = (data.channels || []).find(c => c.name === 'weixin');
|
||||
if (!wx || !wx.active) return;
|
||||
if (wx.login_status === 'logged_in') {
|
||||
channelsData = data.channels;
|
||||
renderActiveChannels();
|
||||
} else {
|
||||
const ch = channelsData.find(c => c.name === 'weixin');
|
||||
if (ch) ch.login_status = wx.login_status;
|
||||
startWeixinActiveStatusPoll();
|
||||
}
|
||||
}).catch(() => { startWeixinActiveStatusPoll(); });
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function showWeixinActiveQr() {
|
||||
const container = document.getElementById('weixin-active-qr');
|
||||
if (!container) return;
|
||||
container.innerHTML = `
|
||||
<div id="weixin-qr-panel" class="flex flex-col items-center py-2">
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400 mb-4">${t('weixin_scan_loading')}</p>
|
||||
</div>`;
|
||||
stopWeixinStatusPoll();
|
||||
startWeixinQrLogin();
|
||||
}
|
||||
|
||||
function stopWeixinQrPoll() {
|
||||
if (_weixinQrPollTimer) {
|
||||
clearTimeout(_weixinQrPollTimer);
|
||||
_weixinQrPollTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function startWeixinQrLogin() {
|
||||
stopWeixinQrPoll();
|
||||
fetch('/api/weixin/qrlogin')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const panel = document.getElementById('weixin-qr-panel');
|
||||
if (!panel) return;
|
||||
if (data.status !== 'success') {
|
||||
panel.innerHTML = `<p class="text-sm text-red-500">${t('weixin_scan_fail')}: ${data.message || ''}</p>`;
|
||||
return;
|
||||
}
|
||||
renderWeixinQr(data.qr_image || data.qrcode_url, 'waiting');
|
||||
if (data.source === 'channel') {
|
||||
startWeixinActiveStatusPoll();
|
||||
} else {
|
||||
pollWeixinQrStatus();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
const panel = document.getElementById('weixin-qr-panel');
|
||||
if (panel) panel.innerHTML = `<p class="text-sm text-red-500">${t('weixin_scan_fail')}</p>`;
|
||||
});
|
||||
}
|
||||
|
||||
function renderWeixinQr(qrcodeUrl, status) {
|
||||
const panel = document.getElementById('weixin-qr-panel');
|
||||
if (!panel) return;
|
||||
|
||||
let statusText = t('weixin_scan_waiting');
|
||||
let statusColor = 'text-slate-500 dark:text-slate-400';
|
||||
if (status === 'scanned') {
|
||||
statusText = t('weixin_scan_scanned');
|
||||
statusColor = 'text-primary-500';
|
||||
} else if (status === 'expired') {
|
||||
statusText = t('weixin_scan_expired');
|
||||
statusColor = 'text-amber-500';
|
||||
} else if (status === 'confirmed') {
|
||||
statusText = t('weixin_scan_success');
|
||||
statusColor = 'text-primary-500';
|
||||
}
|
||||
|
||||
panel.innerHTML = `
|
||||
<div class="flex flex-col items-center">
|
||||
<p class="text-sm font-medium text-slate-700 dark:text-slate-200 mb-1">${t('weixin_scan_title')}</p>
|
||||
<p class="text-xs text-slate-400 dark:text-slate-500 mb-4">${t('weixin_scan_desc')}</p>
|
||||
<div class="bg-white p-3 rounded-xl shadow-sm border border-slate-100 dark:border-slate-700 mb-3">
|
||||
<img src="${escapeHtml(qrcodeUrl)}" alt="QR Code" class="w-52 h-52" style="image-rendering: pixelated;"/>
|
||||
</div>
|
||||
<p class="text-xs ${statusColor} mb-1">${statusText}</p>
|
||||
<p class="text-xs text-slate-400 dark:text-slate-500">${t('weixin_qr_tip')}</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function pollWeixinQrStatus() {
|
||||
_weixinQrPollTimer = setTimeout(() => {
|
||||
fetch('/api/weixin/qrlogin', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'poll' })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const panel = document.getElementById('weixin-qr-panel');
|
||||
if (!panel) { stopWeixinQrPoll(); return; }
|
||||
|
||||
if (data.status !== 'success') {
|
||||
pollWeixinQrStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
const qrStatus = data.qr_status;
|
||||
if (qrStatus === 'confirmed') {
|
||||
renderWeixinQr('', 'confirmed');
|
||||
panel.innerHTML = `
|
||||
<div class="flex flex-col items-center py-4">
|
||||
<div class="w-12 h-12 rounded-full bg-primary-50 dark:bg-primary-900/30 flex items-center justify-center mb-3">
|
||||
<i class="fas fa-check text-primary-500 text-lg"></i>
|
||||
</div>
|
||||
<p class="text-sm font-medium text-primary-600 dark:text-primary-400">${t('weixin_scan_success')}</p>
|
||||
</div>`;
|
||||
connectWeixinAfterQr();
|
||||
} else if (qrStatus === 'expired' && (data.qr_image || data.qrcode_url)) {
|
||||
renderWeixinQr(data.qr_image || data.qrcode_url, 'waiting');
|
||||
pollWeixinQrStatus();
|
||||
} else if (qrStatus === 'scaned') {
|
||||
const img = panel.querySelector('img');
|
||||
const currentSrc = img ? img.src : '';
|
||||
renderWeixinQr(currentSrc, 'scanned');
|
||||
pollWeixinQrStatus();
|
||||
} else {
|
||||
pollWeixinQrStatus();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
pollWeixinQrStatus();
|
||||
});
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function connectWeixinAfterQr() {
|
||||
fetch('/api/channels', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'connect', channel: 'weixin', config: {} })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
const ch = channelsData.find(c => c.name === 'weixin');
|
||||
if (ch) ch.active = true;
|
||||
setTimeout(() => renderActiveChannels(), 1500);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Scheduler View
|
||||
// =====================================================================
|
||||
|
||||
@@ -353,13 +353,15 @@ class WebChannel(ChatChannel):
|
||||
# 打印可用渠道类型提示
|
||||
logger.info(
|
||||
"[WebChannel] 全部可用通道如下,可修改 config.json 配置文件中的 channel_type 字段进行切换,多个通道用逗号分隔:")
|
||||
logger.info("[WebChannel] 1. web - 网页")
|
||||
logger.info("[WebChannel] 2. terminal - 终端")
|
||||
logger.info("[WebChannel] 3. feishu - 飞书")
|
||||
logger.info("[WebChannel] 4. dingtalk - 钉钉")
|
||||
logger.info("[WebChannel] 5. wechatcom_app - 企微自建应用")
|
||||
logger.info("[WebChannel] 6. wechatmp - 个人公众号")
|
||||
logger.info("[WebChannel] 7. wechatmp_service - 企业公众号")
|
||||
logger.info("[WebChannel] 1. weixin - 微信")
|
||||
logger.info("[WebChannel] 2. web - 网页")
|
||||
logger.info("[WebChannel] 3. terminal - 终端")
|
||||
logger.info("[WebChannel] 4. feishu - 飞书")
|
||||
logger.info("[WebChannel] 5. dingtalk - 钉钉")
|
||||
logger.info("[WebChannel] 6. wecom_bot - 企微智能机器人")
|
||||
logger.info("[WebChannel] 7. wechatcom_app - 企微自建应用")
|
||||
logger.info("[WebChannel] 8. wechatmp - 个人公众号")
|
||||
logger.info("[WebChannel] 9. wechatmp_service - 企业公众号")
|
||||
logger.info("[WebChannel] ✅ Web控制台已运行")
|
||||
logger.info(f"[WebChannel] 🌐 本地访问: http://localhost:{port}")
|
||||
logger.info(f"[WebChannel] 🌍 服务器访问: http://YOUR_IP:{port} (请将YOUR_IP替换为服务器IP)")
|
||||
@@ -380,6 +382,7 @@ class WebChannel(ChatChannel):
|
||||
'/chat', 'ChatHandler',
|
||||
'/config', 'ConfigHandler',
|
||||
'/api/channels', 'ChannelsHandler',
|
||||
'/api/weixin/qrlogin', 'WeixinQrHandler',
|
||||
'/api/tools', 'ToolsHandler',
|
||||
'/api/skills', 'SkillsHandler',
|
||||
'/api/memory', 'MemoryHandler',
|
||||
@@ -685,6 +688,12 @@ class ChannelsHandler:
|
||||
"""API for managing external channel configurations (feishu, dingtalk, etc)."""
|
||||
|
||||
CHANNEL_DEFS = OrderedDict([
|
||||
("weixin", {
|
||||
"label": {"zh": "微信", "en": "WeChat"},
|
||||
"icon": "fa-comment",
|
||||
"color": "emerald",
|
||||
"fields": [],
|
||||
}),
|
||||
("feishu", {
|
||||
"label": {"zh": "飞书", "en": "Feishu"},
|
||||
"icon": "fa-paper-plane",
|
||||
@@ -750,6 +759,20 @@ class ChannelsHandler:
|
||||
}),
|
||||
])
|
||||
|
||||
@staticmethod
|
||||
def _get_weixin_login_status() -> str:
|
||||
try:
|
||||
import sys
|
||||
app_module = sys.modules.get('__main__') or sys.modules.get('app')
|
||||
mgr = getattr(app_module, '_channel_mgr', None) if app_module else None
|
||||
if mgr:
|
||||
ch = mgr.get_channel("weixin")
|
||||
if ch and hasattr(ch, 'login_status'):
|
||||
return ch.login_status
|
||||
except Exception:
|
||||
pass
|
||||
return "unknown"
|
||||
|
||||
@staticmethod
|
||||
def _mask_secret(value: str) -> str:
|
||||
if not value or len(value) <= 8:
|
||||
@@ -789,14 +812,17 @@ class ChannelsHandler:
|
||||
"value": display_val,
|
||||
"default": f.get("default", ""),
|
||||
})
|
||||
channels.append({
|
||||
ch_info = {
|
||||
"name": ch_name,
|
||||
"label": ch_def["label"],
|
||||
"icon": ch_def["icon"],
|
||||
"color": ch_def["color"],
|
||||
"active": ch_name in active_channels,
|
||||
"fields": fields_out,
|
||||
})
|
||||
}
|
||||
if ch_name == "weixin" and ch_name in active_channels:
|
||||
ch_info["login_status"] = self._get_weixin_login_status()
|
||||
channels.append(ch_info)
|
||||
return json.dumps({"status": "success", "channels": channels}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
logger.error(f"[WebChannel] Channels API error: {e}")
|
||||
@@ -1016,6 +1042,157 @@ class ChannelsHandler:
|
||||
}, ensure_ascii=False)
|
||||
|
||||
|
||||
class WeixinQrHandler:
|
||||
"""Handle WeChat QR code login from the web console.
|
||||
|
||||
GET /api/weixin/qrlogin → fetch a new QR code
|
||||
POST /api/weixin/qrlogin → poll QR status or start channel after login
|
||||
"""
|
||||
|
||||
_qr_state = {}
|
||||
|
||||
@staticmethod
|
||||
def _qr_to_data_uri(data: str) -> str:
|
||||
"""Generate a QR code as a PNG data URI."""
|
||||
try:
|
||||
import qrcode as qr_lib
|
||||
import io
|
||||
import base64
|
||||
qr = qr_lib.QRCode(error_correction=qr_lib.constants.ERROR_CORRECT_L, box_size=6, border=2)
|
||||
qr.add_data(data)
|
||||
qr.make(fit=True)
|
||||
img = qr.make_image(fill_color="black", back_color="white")
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="PNG")
|
||||
b64 = base64.b64encode(buf.getvalue()).decode("ascii")
|
||||
return f"data:image/png;base64,{b64}"
|
||||
except ImportError:
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def _get_running_channel():
|
||||
try:
|
||||
import sys
|
||||
app_module = sys.modules.get('__main__') or sys.modules.get('app')
|
||||
mgr = getattr(app_module, '_channel_mgr', None) if app_module else None
|
||||
if mgr:
|
||||
return mgr.get_channel("weixin")
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def GET(self):
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
try:
|
||||
running_ch = self._get_running_channel()
|
||||
if running_ch and hasattr(running_ch, '_current_qr_url') and running_ch._current_qr_url:
|
||||
qr_image = self._qr_to_data_uri(running_ch._current_qr_url)
|
||||
return json.dumps({
|
||||
"status": "success",
|
||||
"qrcode_url": running_ch._current_qr_url,
|
||||
"qr_image": qr_image,
|
||||
"source": "channel",
|
||||
})
|
||||
|
||||
from channel.weixin.weixin_api import WeixinApi, DEFAULT_BASE_URL
|
||||
base_url = conf().get("weixin_base_url", DEFAULT_BASE_URL)
|
||||
api = WeixinApi(base_url=base_url)
|
||||
qr_resp = api.fetch_qr_code()
|
||||
qrcode = qr_resp.get("qrcode", "")
|
||||
qrcode_url = qr_resp.get("qrcode_img_content", "")
|
||||
if not qrcode:
|
||||
return json.dumps({"status": "error", "message": "No QR code returned"})
|
||||
qr_image = self._qr_to_data_uri(qrcode_url)
|
||||
WeixinQrHandler._qr_state = {
|
||||
"qrcode": qrcode,
|
||||
"qrcode_url": qrcode_url,
|
||||
"base_url": base_url,
|
||||
}
|
||||
return json.dumps({"status": "success", "qrcode_url": qrcode_url, "qr_image": qr_image})
|
||||
except Exception as e:
|
||||
logger.error(f"[WebChannel] WeixinQr GET error: {e}")
|
||||
return json.dumps({"status": "error", "message": str(e)})
|
||||
|
||||
def POST(self):
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
try:
|
||||
body = json.loads(web.data())
|
||||
action = body.get("action", "poll")
|
||||
|
||||
if action == "poll":
|
||||
return self._poll_status()
|
||||
elif action == "refresh":
|
||||
return self.GET()
|
||||
else:
|
||||
return json.dumps({"status": "error", "message": f"unknown action: {action}"})
|
||||
except Exception as e:
|
||||
logger.error(f"[WebChannel] WeixinQr POST error: {e}")
|
||||
return json.dumps({"status": "error", "message": str(e)})
|
||||
|
||||
def _poll_status(self):
|
||||
state = WeixinQrHandler._qr_state
|
||||
qrcode = state.get("qrcode", "")
|
||||
base_url = state.get("base_url", "")
|
||||
if not qrcode:
|
||||
return json.dumps({"status": "error", "message": "No active QR session"})
|
||||
|
||||
from channel.weixin.weixin_api import WeixinApi, DEFAULT_BASE_URL
|
||||
api = WeixinApi(base_url=base_url or DEFAULT_BASE_URL)
|
||||
try:
|
||||
status_resp = api.poll_qr_status(qrcode, timeout=10)
|
||||
except Exception as e:
|
||||
return json.dumps({"status": "error", "message": str(e)})
|
||||
|
||||
qr_status = status_resp.get("status", "wait")
|
||||
|
||||
if qr_status == "confirmed":
|
||||
bot_token = status_resp.get("bot_token", "")
|
||||
bot_id = status_resp.get("ilink_bot_id", "")
|
||||
result_base_url = status_resp.get("baseurl", base_url)
|
||||
user_id = status_resp.get("ilink_user_id", "")
|
||||
|
||||
if not bot_token or not bot_id:
|
||||
return json.dumps({"status": "error", "message": "Login confirmed but missing token"})
|
||||
|
||||
cred_path = os.path.expanduser(
|
||||
conf().get("weixin_credentials_path", "~/.weixin_cow_credentials.json")
|
||||
)
|
||||
from channel.weixin.weixin_channel import _save_credentials
|
||||
_save_credentials(cred_path, {
|
||||
"token": bot_token,
|
||||
"base_url": result_base_url,
|
||||
"bot_id": bot_id,
|
||||
"user_id": user_id,
|
||||
})
|
||||
conf()["weixin_token"] = bot_token
|
||||
conf()["weixin_base_url"] = result_base_url
|
||||
|
||||
WeixinQrHandler._qr_state = {}
|
||||
logger.info(f"[WebChannel] WeChat QR login confirmed: bot_id={bot_id}")
|
||||
|
||||
return json.dumps({
|
||||
"status": "success",
|
||||
"qr_status": "confirmed",
|
||||
"bot_id": bot_id,
|
||||
})
|
||||
|
||||
if qr_status == "expired":
|
||||
new_resp = api.fetch_qr_code()
|
||||
new_qrcode = new_resp.get("qrcode", "")
|
||||
new_qrcode_url = new_resp.get("qrcode_img_content", "")
|
||||
new_qr_image = self._qr_to_data_uri(new_qrcode_url)
|
||||
WeixinQrHandler._qr_state["qrcode"] = new_qrcode
|
||||
WeixinQrHandler._qr_state["qrcode_url"] = new_qrcode_url
|
||||
return json.dumps({
|
||||
"status": "success",
|
||||
"qr_status": "expired",
|
||||
"qrcode_url": new_qrcode_url,
|
||||
"qr_image": new_qr_image,
|
||||
})
|
||||
|
||||
return json.dumps({"status": "success", "qr_status": qr_status})
|
||||
|
||||
|
||||
def _get_workspace_root():
|
||||
"""Resolve the agent workspace directory."""
|
||||
from common.utils import expand_path
|
||||
|
||||
@@ -26,6 +26,7 @@ from channel.wecom_bot.wecom_bot_message import WecomBotMessage
|
||||
from common.expired_dict import ExpiredDict
|
||||
from common.log import logger
|
||||
from common.singleton import singleton
|
||||
from common.ws_client_compat import websocket_app_run_forever
|
||||
from config import conf
|
||||
|
||||
WECOM_WS_URL = "wss://openws.work.weixin.qq.com"
|
||||
@@ -119,7 +120,7 @@ class WecomBotChannel(ChatChannel):
|
||||
|
||||
def run_forever():
|
||||
try:
|
||||
self._ws.run_forever(ping_interval=0, reconnect=0)
|
||||
websocket_app_run_forever(self._ws, ping_interval=0, reconnect=0)
|
||||
except (SystemExit, KeyboardInterrupt):
|
||||
logger.info("[WecomBot] WebSocket thread interrupted")
|
||||
except Exception as e:
|
||||
|
||||
0
channel/weixin/__init__.py
Normal file
0
channel/weixin/__init__.py
Normal file
385
channel/weixin/weixin_api.py
Normal file
385
channel/weixin/weixin_api.py
Normal file
@@ -0,0 +1,385 @@
|
||||
"""
|
||||
Weixin HTTP JSON API client.
|
||||
|
||||
Implements the ilink bot protocol:
|
||||
- getUpdates (long-poll)
|
||||
- sendMessage
|
||||
- getUploadUrl
|
||||
- getConfig
|
||||
- sendTyping
|
||||
- QR login (get_bot_qrcode / get_qrcode_status)
|
||||
|
||||
CDN media upload with AES-128-ECB encryption.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import os
|
||||
import random
|
||||
import struct
|
||||
import time
|
||||
import uuid
|
||||
|
||||
import requests
|
||||
|
||||
from common.log import logger
|
||||
|
||||
DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com"
|
||||
CDN_BASE_URL = "https://novac2c.cdn.weixin.qq.com/c2c"
|
||||
DEFAULT_LONG_POLL_TIMEOUT = 35
|
||||
DEFAULT_API_TIMEOUT = 15
|
||||
QR_POLL_TIMEOUT = 35
|
||||
BOT_TYPE = "3"
|
||||
|
||||
|
||||
def _random_wechat_uin() -> str:
|
||||
val = random.randint(0, 0xFFFFFFFF)
|
||||
return base64.b64encode(str(val).encode("utf-8")).decode("utf-8")
|
||||
|
||||
|
||||
def _build_headers(token: str = "") -> dict:
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"AuthorizationType": "ilink_bot_token",
|
||||
"X-WECHAT-UIN": _random_wechat_uin(),
|
||||
}
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
return headers
|
||||
|
||||
|
||||
def _ensure_trailing_slash(url: str) -> str:
|
||||
return url if url.endswith("/") else url + "/"
|
||||
|
||||
|
||||
class WeixinApi:
|
||||
"""Stateless HTTP client for the Weixin ilink bot API."""
|
||||
|
||||
def __init__(self, base_url: str = DEFAULT_BASE_URL, token: str = "",
|
||||
cdn_base_url: str = CDN_BASE_URL):
|
||||
self.base_url = base_url
|
||||
self.token = token
|
||||
self.cdn_base_url = cdn_base_url
|
||||
|
||||
def _post(self, endpoint: str, body: dict, timeout: int = DEFAULT_API_TIMEOUT) -> dict:
|
||||
url = _ensure_trailing_slash(self.base_url) + endpoint
|
||||
headers = _build_headers(self.token)
|
||||
try:
|
||||
resp = requests.post(url, json=body, headers=headers, timeout=timeout)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
except requests.exceptions.Timeout:
|
||||
logger.debug(f"[Weixin] API timeout: {endpoint}")
|
||||
return {"ret": 0, "msgs": []}
|
||||
except Exception as e:
|
||||
logger.error(f"[Weixin] API error {endpoint}: {e}")
|
||||
raise
|
||||
|
||||
# ── getUpdates (long-poll) ─────────────────────────────────────────
|
||||
|
||||
def get_updates(self, get_updates_buf: str = "", timeout: int = DEFAULT_LONG_POLL_TIMEOUT) -> dict:
|
||||
return self._post("ilink/bot/getupdates", {
|
||||
"get_updates_buf": get_updates_buf,
|
||||
}, timeout=timeout + 5)
|
||||
|
||||
# ── sendMessage ────────────────────────────────────────────────────
|
||||
|
||||
def send_text(self, to: str, text: str, context_token: str) -> dict:
|
||||
return self._post("ilink/bot/sendmessage", {
|
||||
"msg": {
|
||||
"from_user_id": "",
|
||||
"to_user_id": to,
|
||||
"client_id": uuid.uuid4().hex[:16],
|
||||
"message_type": 2, # BOT
|
||||
"message_state": 2, # FINISH
|
||||
"item_list": [{"type": 1, "text_item": {"text": text}}],
|
||||
"context_token": context_token,
|
||||
}
|
||||
})
|
||||
|
||||
def send_image_item(self, to: str, context_token: str,
|
||||
encrypt_query_param: str, aes_key_b64: str,
|
||||
ciphertext_size: int, text: str = "") -> dict:
|
||||
items = []
|
||||
if text:
|
||||
items.append({"type": 1, "text_item": {"text": text}})
|
||||
items.append({
|
||||
"type": 2,
|
||||
"image_item": {
|
||||
"media": {
|
||||
"encrypt_query_param": encrypt_query_param,
|
||||
"aes_key": aes_key_b64,
|
||||
"encrypt_type": 1,
|
||||
},
|
||||
"mid_size": ciphertext_size,
|
||||
}
|
||||
})
|
||||
return self._send_items(to, context_token, items)
|
||||
|
||||
def send_file_item(self, to: str, context_token: str,
|
||||
encrypt_query_param: str, aes_key_b64: str,
|
||||
file_name: str, file_size: int, text: str = "") -> dict:
|
||||
items = []
|
||||
if text:
|
||||
items.append({"type": 1, "text_item": {"text": text}})
|
||||
items.append({
|
||||
"type": 4,
|
||||
"file_item": {
|
||||
"media": {
|
||||
"encrypt_query_param": encrypt_query_param,
|
||||
"aes_key": aes_key_b64,
|
||||
"encrypt_type": 1,
|
||||
},
|
||||
"file_name": file_name,
|
||||
"len": str(file_size),
|
||||
}
|
||||
})
|
||||
return self._send_items(to, context_token, items)
|
||||
|
||||
def send_video_item(self, to: str, context_token: str,
|
||||
encrypt_query_param: str, aes_key_b64: str,
|
||||
ciphertext_size: int, text: str = "") -> dict:
|
||||
items = []
|
||||
if text:
|
||||
items.append({"type": 1, "text_item": {"text": text}})
|
||||
items.append({
|
||||
"type": 5,
|
||||
"video_item": {
|
||||
"media": {
|
||||
"encrypt_query_param": encrypt_query_param,
|
||||
"aes_key": aes_key_b64,
|
||||
"encrypt_type": 1,
|
||||
},
|
||||
"video_size": ciphertext_size,
|
||||
}
|
||||
})
|
||||
return self._send_items(to, context_token, items)
|
||||
|
||||
def _send_items(self, to: str, context_token: str, items: list) -> dict:
|
||||
return self._post("ilink/bot/sendmessage", {
|
||||
"msg": {
|
||||
"from_user_id": "",
|
||||
"to_user_id": to,
|
||||
"client_id": uuid.uuid4().hex[:16],
|
||||
"message_type": 2,
|
||||
"message_state": 2,
|
||||
"item_list": items,
|
||||
"context_token": context_token,
|
||||
}
|
||||
})
|
||||
|
||||
# ── getUploadUrl ───────────────────────────────────────────────────
|
||||
|
||||
def get_upload_url(self, filekey: str, media_type: int, to_user_id: str,
|
||||
rawsize: int, rawfilemd5: str, filesize: int,
|
||||
aeskey: str,
|
||||
thumb_rawsize: int = 0, thumb_rawfilemd5: str = "",
|
||||
thumb_filesize: int = 0) -> dict:
|
||||
body = {
|
||||
"filekey": filekey,
|
||||
"media_type": media_type,
|
||||
"to_user_id": to_user_id,
|
||||
"rawsize": rawsize,
|
||||
"rawfilemd5": rawfilemd5,
|
||||
"filesize": filesize,
|
||||
"aeskey": aeskey,
|
||||
}
|
||||
if thumb_rawsize > 0:
|
||||
body["thumb_rawsize"] = thumb_rawsize
|
||||
body["thumb_rawfilemd5"] = thumb_rawfilemd5
|
||||
body["thumb_filesize"] = thumb_filesize
|
||||
else:
|
||||
body["no_need_thumb"] = True
|
||||
return self._post("ilink/bot/getuploadurl", body)
|
||||
|
||||
# ── getConfig / sendTyping ─────────────────────────────────────────
|
||||
|
||||
def get_config(self, user_id: str, context_token: str = "") -> dict:
|
||||
return self._post("ilink/bot/getconfig", {
|
||||
"ilink_user_id": user_id,
|
||||
"context_token": context_token,
|
||||
}, timeout=10)
|
||||
|
||||
def send_typing(self, user_id: str, typing_ticket: str, status: int = 1) -> dict:
|
||||
return self._post("ilink/bot/sendtyping", {
|
||||
"ilink_user_id": user_id,
|
||||
"typing_ticket": typing_ticket,
|
||||
"status": status,
|
||||
}, timeout=10)
|
||||
|
||||
# ── QR Login ───────────────────────────────────────────────────────
|
||||
|
||||
def fetch_qr_code(self) -> dict:
|
||||
url = _ensure_trailing_slash(self.base_url) + f"ilink/bot/get_bot_qrcode?bot_type={BOT_TYPE}"
|
||||
resp = requests.get(url, timeout=15)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
def poll_qr_status(self, qrcode: str, timeout: int = QR_POLL_TIMEOUT) -> dict:
|
||||
url = (_ensure_trailing_slash(self.base_url) +
|
||||
f"ilink/bot/get_qrcode_status?qrcode={requests.utils.quote(qrcode)}")
|
||||
headers = {"iLink-App-ClientVersion": "1"}
|
||||
try:
|
||||
resp = requests.get(url, headers=headers, timeout=timeout)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
except requests.exceptions.Timeout:
|
||||
return {"status": "wait"}
|
||||
|
||||
|
||||
# ── AES-128-ECB helpers ─────────────────────────────────────────────
|
||||
|
||||
def _aes_ecb_encrypt(data: bytes, key: bytes) -> bytes:
|
||||
from Crypto.Cipher import AES
|
||||
pad_len = 16 - (len(data) % 16)
|
||||
padded = data + bytes([pad_len] * pad_len)
|
||||
cipher = AES.new(key, AES.MODE_ECB)
|
||||
return cipher.encrypt(padded)
|
||||
|
||||
|
||||
def _aes_ecb_decrypt(data: bytes, key: bytes) -> bytes:
|
||||
from Crypto.Cipher import AES
|
||||
cipher = AES.new(key, AES.MODE_ECB)
|
||||
decrypted = cipher.decrypt(data)
|
||||
pad_len = decrypted[-1]
|
||||
if pad_len > 16:
|
||||
return decrypted
|
||||
return decrypted[:-pad_len]
|
||||
|
||||
|
||||
def _file_md5(file_path: str) -> str:
|
||||
h = hashlib.md5()
|
||||
with open(file_path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(8192), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def _md5_bytes(data: bytes) -> str:
|
||||
return hashlib.md5(data).hexdigest()
|
||||
|
||||
|
||||
def upload_media_to_cdn(api: WeixinApi, file_path: str, to_user_id: str,
|
||||
media_type: int) -> dict:
|
||||
"""
|
||||
Upload a local file to the Weixin CDN.
|
||||
|
||||
Args:
|
||||
api: WeixinApi instance
|
||||
file_path: local file path
|
||||
to_user_id: target user id
|
||||
media_type: 1=IMAGE, 2=VIDEO, 3=FILE
|
||||
|
||||
Returns:
|
||||
dict with keys: encrypt_query_param, aes_key_b64, ciphertext_size, raw_size
|
||||
"""
|
||||
aes_key = os.urandom(16)
|
||||
aes_key_hex = aes_key.hex()
|
||||
|
||||
with open(file_path, "rb") as f:
|
||||
raw_data = f.read()
|
||||
|
||||
raw_size = len(raw_data)
|
||||
raw_md5 = _md5_bytes(raw_data)
|
||||
encrypted = _aes_ecb_encrypt(raw_data, aes_key)
|
||||
cipher_size = len(encrypted)
|
||||
filekey = uuid.uuid4().hex
|
||||
|
||||
thumb_rawsize = 0
|
||||
thumb_rawfilemd5 = ""
|
||||
thumb_filesize = 0
|
||||
|
||||
if media_type == 1: # IMAGE - generate a tiny thumbnail
|
||||
try:
|
||||
from PIL import Image
|
||||
import io
|
||||
img = Image.open(file_path)
|
||||
img.thumbnail((100, 100))
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="JPEG", quality=60)
|
||||
thumb_raw = buf.getvalue()
|
||||
thumb_rawsize = len(thumb_raw)
|
||||
thumb_rawfilemd5 = _md5_bytes(thumb_raw)
|
||||
thumb_encrypted = _aes_ecb_encrypt(thumb_raw, aes_key)
|
||||
thumb_filesize = len(thumb_encrypted)
|
||||
except Exception as e:
|
||||
logger.warning(f"[Weixin] Thumbnail generation failed, skipping: {e}")
|
||||
|
||||
resp = api.get_upload_url(
|
||||
filekey=filekey,
|
||||
media_type=media_type,
|
||||
to_user_id=to_user_id,
|
||||
rawsize=raw_size,
|
||||
rawfilemd5=raw_md5,
|
||||
filesize=cipher_size,
|
||||
aeskey=aes_key_hex,
|
||||
thumb_rawsize=thumb_rawsize,
|
||||
thumb_rawfilemd5=thumb_rawfilemd5,
|
||||
thumb_filesize=thumb_filesize,
|
||||
)
|
||||
|
||||
upload_param = resp.get("upload_param", "")
|
||||
if not upload_param:
|
||||
raise RuntimeError(f"[Weixin] getUploadUrl returned no upload_param: {resp}")
|
||||
|
||||
cdn_url = api.cdn_base_url + "?" + upload_param
|
||||
put_resp = requests.put(cdn_url, data=encrypted, headers={
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Content-Length": str(cipher_size),
|
||||
}, timeout=60)
|
||||
put_resp.raise_for_status()
|
||||
|
||||
# Upload thumbnail if we have one
|
||||
thumb_upload_param = resp.get("thumb_upload_param", "")
|
||||
if thumb_upload_param and thumb_filesize > 0:
|
||||
thumb_cdn_url = api.cdn_base_url + "?" + thumb_upload_param
|
||||
try:
|
||||
requests.put(thumb_cdn_url, data=thumb_encrypted, headers={
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Content-Length": str(thumb_filesize),
|
||||
}, timeout=30)
|
||||
except Exception as e:
|
||||
logger.warning(f"[Weixin] Thumbnail upload failed (non-fatal): {e}")
|
||||
|
||||
return {
|
||||
"encrypt_query_param": upload_param,
|
||||
"aes_key_b64": base64.b64encode(aes_key).decode("utf-8"),
|
||||
"ciphertext_size": cipher_size,
|
||||
"raw_size": raw_size,
|
||||
}
|
||||
|
||||
|
||||
def download_media_from_cdn(cdn_base_url: str, encrypt_query_param: str,
|
||||
aes_key: str, save_path: str) -> str:
|
||||
"""
|
||||
Download and decrypt a media file from Weixin CDN.
|
||||
|
||||
Args:
|
||||
cdn_base_url: CDN base URL
|
||||
encrypt_query_param: encrypted query parameter from message
|
||||
aes_key: hex or base64 encoded AES key
|
||||
save_path: path to save decrypted file
|
||||
|
||||
Returns:
|
||||
save_path on success
|
||||
"""
|
||||
url = cdn_base_url + "?" + encrypt_query_param
|
||||
resp = requests.get(url, timeout=60)
|
||||
resp.raise_for_status()
|
||||
|
||||
# Determine key format (hex string or base64)
|
||||
try:
|
||||
key_bytes = bytes.fromhex(aes_key)
|
||||
if len(key_bytes) != 16:
|
||||
raise ValueError()
|
||||
except (ValueError, TypeError):
|
||||
key_bytes = base64.b64decode(aes_key)
|
||||
if len(key_bytes) != 16:
|
||||
raise ValueError(f"Invalid AES key length: {len(key_bytes)}")
|
||||
|
||||
decrypted = _aes_ecb_decrypt(resp.content, key_bytes)
|
||||
|
||||
os.makedirs(os.path.dirname(save_path), exist_ok=True)
|
||||
with open(save_path, "wb") as f:
|
||||
f.write(decrypted)
|
||||
return save_path
|
||||
603
channel/weixin/weixin_channel.py
Normal file
603
channel/weixin/weixin_channel.py
Normal file
@@ -0,0 +1,603 @@
|
||||
"""
|
||||
Weixin channel implementation.
|
||||
|
||||
Uses HTTP long-poll (getUpdates) to receive messages and sendMessage to reply.
|
||||
Login via QR code scan through the ilink bot API.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
|
||||
import requests
|
||||
|
||||
from bridge.context import Context, ContextType
|
||||
from bridge.reply import Reply, ReplyType
|
||||
from channel.chat_channel import ChatChannel, check_prefix
|
||||
from channel.weixin.weixin_api import (
|
||||
WeixinApi, upload_media_to_cdn,
|
||||
DEFAULT_BASE_URL, CDN_BASE_URL,
|
||||
)
|
||||
from channel.weixin.weixin_message import WeixinMessage
|
||||
from common.expired_dict import ExpiredDict
|
||||
from common.log import logger
|
||||
from common.singleton import singleton
|
||||
from config import conf
|
||||
|
||||
MAX_CONSECUTIVE_FAILURES = 3
|
||||
BACKOFF_DELAY = 30
|
||||
RETRY_DELAY = 2
|
||||
SESSION_EXPIRED_ERRCODE = -14
|
||||
TEXT_CHUNK_LIMIT = 4000
|
||||
|
||||
|
||||
def _load_credentials(cred_path: str) -> dict:
|
||||
"""Load saved credentials from JSON file."""
|
||||
try:
|
||||
if os.path.exists(cred_path):
|
||||
with open(cred_path, "r") as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
logger.warning(f"[Weixin] Failed to load credentials: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
def _save_credentials(cred_path: str, data: dict):
|
||||
"""Save credentials to JSON file."""
|
||||
os.makedirs(os.path.dirname(cred_path), exist_ok=True)
|
||||
with open(cred_path, "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
try:
|
||||
os.chmod(cred_path, 0o600)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@singleton
|
||||
class WeixinChannel(ChatChannel):
|
||||
|
||||
LOGIN_STATUS_IDLE = "idle"
|
||||
LOGIN_STATUS_WAITING = "waiting_scan"
|
||||
LOGIN_STATUS_SCANNED = "scanned"
|
||||
LOGIN_STATUS_OK = "logged_in"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.api = None
|
||||
self._stop_event = threading.Event()
|
||||
self._poll_thread = None
|
||||
self._context_tokens = {} # user_id -> context_token
|
||||
self._received_msgs = ExpiredDict(60 * 60 * 7.1)
|
||||
self._get_updates_buf = ""
|
||||
self._credentials_path = ""
|
||||
self.login_status = self.LOGIN_STATUS_IDLE
|
||||
self._current_qr_url = ""
|
||||
|
||||
conf()["single_chat_prefix"] = [""]
|
||||
|
||||
# ── Lifecycle ──────────────────────────────────────────────────────
|
||||
|
||||
def startup(self):
|
||||
base_url = conf().get("weixin_base_url", DEFAULT_BASE_URL)
|
||||
cdn_base_url = conf().get("weixin_cdn_base_url", CDN_BASE_URL)
|
||||
token = conf().get("weixin_token", "")
|
||||
|
||||
self._credentials_path = os.path.expanduser(
|
||||
conf().get("weixin_credentials_path", "~/.weixin_cow_credentials.json")
|
||||
)
|
||||
|
||||
if not token:
|
||||
creds = _load_credentials(self._credentials_path)
|
||||
token = creds.get("token", "")
|
||||
if creds.get("base_url"):
|
||||
base_url = creds["base_url"]
|
||||
|
||||
if not token:
|
||||
logger.info("[Weixin] No token found, starting QR login...")
|
||||
self.login_status = self.LOGIN_STATUS_WAITING
|
||||
login_result = self._qr_login(base_url)
|
||||
if not login_result:
|
||||
self.login_status = self.LOGIN_STATUS_IDLE
|
||||
err = "[Weixin] QR login failed. Set weixin_token in config or run login again."
|
||||
logger.error(err)
|
||||
self.report_startup_error(err)
|
||||
return
|
||||
token = login_result["token"]
|
||||
base_url = login_result.get("base_url", base_url)
|
||||
|
||||
self.api = WeixinApi(base_url=base_url, token=token, cdn_base_url=cdn_base_url)
|
||||
self.login_status = self.LOGIN_STATUS_OK
|
||||
|
||||
logger.info(f"[Weixin] 微信通道已启动,凭证保存在 {self._credentials_path},"
|
||||
f"如需重新扫码登录请删除该文件后重启")
|
||||
self.report_startup_success()
|
||||
|
||||
self._stop_event.clear()
|
||||
self._poll_loop()
|
||||
|
||||
def stop(self):
|
||||
logger.info("[Weixin] stop() called")
|
||||
self._stop_event.set()
|
||||
|
||||
def _relogin(self) -> bool:
|
||||
"""Re-login after session expiry. Returns True on success."""
|
||||
base_url = self.api.base_url if self.api else DEFAULT_BASE_URL
|
||||
if os.path.exists(self._credentials_path):
|
||||
try:
|
||||
os.remove(self._credentials_path)
|
||||
except Exception:
|
||||
pass
|
||||
self.login_status = self.LOGIN_STATUS_WAITING
|
||||
result = self._qr_login(base_url)
|
||||
if not result:
|
||||
self.login_status = self.LOGIN_STATUS_IDLE
|
||||
return False
|
||||
self.api = WeixinApi(
|
||||
base_url=result.get("base_url", base_url),
|
||||
token=result["token"],
|
||||
cdn_base_url=self.api.cdn_base_url if self.api else CDN_BASE_URL,
|
||||
)
|
||||
self.login_status = self.LOGIN_STATUS_OK
|
||||
self._context_tokens.clear()
|
||||
return True
|
||||
|
||||
# ── QR Login ───────────────────────────────────────────────────────
|
||||
|
||||
@staticmethod
|
||||
def _print_qr(qrcode_url: str):
|
||||
"""Print QR code to terminal for scanning."""
|
||||
print("\n" + "=" * 60)
|
||||
print(" 请使用微信扫描二维码登录 (二维码约2分钟后过期)")
|
||||
print("=" * 60)
|
||||
try:
|
||||
import qrcode as qr_lib
|
||||
qr = qr_lib.QRCode(error_correction=qr_lib.constants.ERROR_CORRECT_L, box_size=1, border=1)
|
||||
qr.add_data(qrcode_url)
|
||||
qr.make(fit=True)
|
||||
qr.print_ascii(invert=True)
|
||||
except ImportError:
|
||||
print(f"\n 二维码链接: {qrcode_url}")
|
||||
print(" (安装 'qrcode' 包可在终端显示二维码)\n")
|
||||
|
||||
def _notify_cloud_qrcode(self, qrcode_url: str):
|
||||
"""Send QR code URL to cloud console when running in cloud mode."""
|
||||
if not self.cloud_mode:
|
||||
return
|
||||
try:
|
||||
from common import cloud_client
|
||||
client = getattr(cloud_client, "chat_client", None)
|
||||
if client and getattr(client, "client_id", None):
|
||||
client.send_channel_qrcode("weixin", qrcode_url)
|
||||
except Exception as e:
|
||||
logger.warning(f"[Weixin] Failed to notify cloud QR code: {e}")
|
||||
|
||||
def _notify_cloud_connected(self):
|
||||
"""Send connected status to cloud console when login succeeds."""
|
||||
if not self.cloud_mode:
|
||||
return
|
||||
try:
|
||||
from common import cloud_client
|
||||
client = getattr(cloud_client, "chat_client", None)
|
||||
if client and getattr(client, "client_id", None):
|
||||
client.send_channel_status("weixin", "connected")
|
||||
except Exception as e:
|
||||
logger.warning(f"[Weixin] Failed to notify cloud connected: {e}")
|
||||
|
||||
def _qr_login(self, base_url: str) -> dict:
|
||||
"""Perform interactive QR code login. Returns dict with token/base_url or empty dict."""
|
||||
api = WeixinApi(base_url=base_url)
|
||||
try:
|
||||
qr_resp = api.fetch_qr_code()
|
||||
except Exception as e:
|
||||
logger.error(f"[Weixin] Failed to fetch QR code: {e}")
|
||||
return {}
|
||||
|
||||
qrcode = qr_resp.get("qrcode", "")
|
||||
qrcode_url = qr_resp.get("qrcode_img_content", "")
|
||||
|
||||
if not qrcode:
|
||||
logger.error("[Weixin] No QR code returned from server")
|
||||
return {}
|
||||
|
||||
self._current_qr_url = qrcode_url
|
||||
logger.info(f"[Weixin] QR code URL: {qrcode_url}")
|
||||
self._print_qr(qrcode_url)
|
||||
self._notify_cloud_qrcode(qrcode_url)
|
||||
print(" 等待扫码...\n")
|
||||
|
||||
scanned_printed = False
|
||||
|
||||
while not self._stop_event.is_set():
|
||||
try:
|
||||
status_resp = api.poll_qr_status(qrcode)
|
||||
except Exception as e:
|
||||
logger.error(f"[Weixin] QR status poll error: {e}")
|
||||
return {}
|
||||
|
||||
status = status_resp.get("status", "wait")
|
||||
|
||||
if status == "wait":
|
||||
pass
|
||||
elif status == "scaned":
|
||||
self.login_status = self.LOGIN_STATUS_SCANNED
|
||||
if not scanned_printed:
|
||||
print(" 已扫码,请在手机上确认...")
|
||||
scanned_printed = True
|
||||
elif status == "expired":
|
||||
print(" 二维码已过期,正在刷新...")
|
||||
try:
|
||||
qr_resp = api.fetch_qr_code()
|
||||
qrcode = qr_resp.get("qrcode", "")
|
||||
qrcode_url = qr_resp.get("qrcode_img_content", "")
|
||||
scanned_printed = False
|
||||
self._current_qr_url = qrcode_url
|
||||
logger.info(f"[Weixin] New QR code: {qrcode_url}")
|
||||
self._print_qr(qrcode_url)
|
||||
self._notify_cloud_qrcode(qrcode_url)
|
||||
except Exception as e:
|
||||
logger.error(f"[Weixin] QR refresh failed: {e}")
|
||||
return {}
|
||||
elif status == "confirmed":
|
||||
bot_token = status_resp.get("bot_token", "")
|
||||
bot_id = status_resp.get("ilink_bot_id", "")
|
||||
result_base_url = status_resp.get("baseurl", base_url)
|
||||
user_id = status_resp.get("ilink_user_id", "")
|
||||
|
||||
if not bot_token or not bot_id:
|
||||
logger.error("[Weixin] Login confirmed but missing token/bot_id")
|
||||
return {}
|
||||
|
||||
self._current_qr_url = ""
|
||||
print(f"\n ✅ 微信登录成功!bot_id={bot_id}")
|
||||
logger.info(f"[Weixin] Login confirmed: bot_id={bot_id}")
|
||||
self._notify_cloud_connected()
|
||||
|
||||
creds = {
|
||||
"token": bot_token,
|
||||
"base_url": result_base_url,
|
||||
"bot_id": bot_id,
|
||||
"user_id": user_id,
|
||||
}
|
||||
_save_credentials(self._credentials_path, creds)
|
||||
logger.info(f"[Weixin] Credentials saved to {self._credentials_path}")
|
||||
|
||||
return {"token": bot_token, "base_url": result_base_url}
|
||||
|
||||
self._stop_event.wait(1)
|
||||
|
||||
logger.info("[Weixin] QR login cancelled by stop event")
|
||||
self._current_qr_url = ""
|
||||
return {}
|
||||
|
||||
# ── Long-poll loop ─────────────────────────────────────────────────
|
||||
|
||||
def _poll_loop(self):
|
||||
"""Main long-poll loop: getUpdates -> parse -> produce."""
|
||||
logger.info("[Weixin] Starting long-poll loop")
|
||||
consecutive_failures = 0
|
||||
|
||||
while not self._stop_event.is_set():
|
||||
try:
|
||||
resp = self.api.get_updates(self._get_updates_buf)
|
||||
|
||||
ret = resp.get("ret", 0)
|
||||
errcode = resp.get("errcode", 0)
|
||||
|
||||
is_error = (ret != 0) or (errcode != 0)
|
||||
if is_error:
|
||||
if errcode == SESSION_EXPIRED_ERRCODE or ret == SESSION_EXPIRED_ERRCODE:
|
||||
logger.error("[Weixin] Session expired (errcode -14), starting re-login...")
|
||||
if self._relogin():
|
||||
logger.info("[Weixin] Re-login successful, resuming long-poll")
|
||||
self._get_updates_buf = ""
|
||||
consecutive_failures = 0
|
||||
continue
|
||||
else:
|
||||
logger.error("[Weixin] Re-login failed, will retry in 5 minutes")
|
||||
self._stop_event.wait(300)
|
||||
continue
|
||||
|
||||
consecutive_failures += 1
|
||||
errmsg = resp.get("errmsg", "")
|
||||
logger.error(f"[Weixin] getUpdates error: ret={ret} errcode={errcode} "
|
||||
f"errmsg={errmsg} ({consecutive_failures}/{MAX_CONSECUTIVE_FAILURES})")
|
||||
if consecutive_failures >= MAX_CONSECUTIVE_FAILURES:
|
||||
consecutive_failures = 0
|
||||
self._stop_event.wait(BACKOFF_DELAY)
|
||||
else:
|
||||
self._stop_event.wait(RETRY_DELAY)
|
||||
continue
|
||||
|
||||
consecutive_failures = 0
|
||||
|
||||
# Update sync cursor
|
||||
new_buf = resp.get("get_updates_buf", "")
|
||||
if new_buf:
|
||||
self._get_updates_buf = new_buf
|
||||
|
||||
# Process messages
|
||||
msgs = resp.get("msgs", [])
|
||||
for raw_msg in msgs:
|
||||
try:
|
||||
self._process_message(raw_msg)
|
||||
except Exception as e:
|
||||
logger.error(f"[Weixin] Failed to process message: {e}", exc_info=True)
|
||||
|
||||
except Exception as e:
|
||||
if self._stop_event.is_set():
|
||||
break
|
||||
consecutive_failures += 1
|
||||
logger.error(f"[Weixin] getUpdates exception: {e} "
|
||||
f"({consecutive_failures}/{MAX_CONSECUTIVE_FAILURES})")
|
||||
if consecutive_failures >= MAX_CONSECUTIVE_FAILURES:
|
||||
consecutive_failures = 0
|
||||
self._stop_event.wait(BACKOFF_DELAY)
|
||||
else:
|
||||
self._stop_event.wait(RETRY_DELAY)
|
||||
|
||||
logger.info("[Weixin] Long-poll loop ended")
|
||||
|
||||
def _process_message(self, raw_msg: dict):
|
||||
"""Parse a single inbound message and produce to the handling queue."""
|
||||
msg_type = raw_msg.get("message_type", 0)
|
||||
if msg_type != 1: # Only process USER messages (type=1)
|
||||
return
|
||||
|
||||
msg_id = str(raw_msg.get("message_id", raw_msg.get("seq", "")))
|
||||
if self._received_msgs.get(msg_id):
|
||||
return
|
||||
self._received_msgs[msg_id] = True
|
||||
|
||||
from_user = raw_msg.get("from_user_id", "")
|
||||
context_token = raw_msg.get("context_token", "")
|
||||
|
||||
if context_token and from_user:
|
||||
self._context_tokens[from_user] = context_token
|
||||
|
||||
cdn_base_url = self.api.cdn_base_url if self.api else CDN_BASE_URL
|
||||
try:
|
||||
wx_msg = WeixinMessage(raw_msg, cdn_base_url=cdn_base_url)
|
||||
except Exception as e:
|
||||
logger.error(f"[Weixin] Failed to parse WeixinMessage: {e}", exc_info=True)
|
||||
return
|
||||
|
||||
logger.info(f"[Weixin] Received: from={from_user} ctype={wx_msg.ctype} "
|
||||
f"content={str(wx_msg.content)[:50]}")
|
||||
|
||||
# File cache logic
|
||||
from channel.file_cache import get_file_cache
|
||||
file_cache = get_file_cache()
|
||||
session_id = from_user
|
||||
|
||||
if wx_msg.ctype == ContextType.IMAGE:
|
||||
if hasattr(wx_msg, "image_path") and wx_msg.image_path:
|
||||
file_cache.add(session_id, wx_msg.image_path, file_type="image")
|
||||
logger.info(f"[Weixin] Image cached for session {session_id}")
|
||||
return
|
||||
|
||||
if wx_msg.ctype == ContextType.FILE:
|
||||
wx_msg.prepare()
|
||||
file_cache.add(session_id, wx_msg.content, file_type="file")
|
||||
logger.info(f"[Weixin] File cached for session {session_id}: {wx_msg.content}")
|
||||
return
|
||||
|
||||
if wx_msg.ctype == ContextType.TEXT:
|
||||
cached_files = file_cache.get(session_id)
|
||||
if cached_files:
|
||||
refs = []
|
||||
for fi in cached_files:
|
||||
ftype, fpath = fi["type"], fi["path"]
|
||||
if ftype == "image":
|
||||
refs.append(f"[图片: {fpath}]")
|
||||
elif ftype == "video":
|
||||
refs.append(f"[视频: {fpath}]")
|
||||
else:
|
||||
refs.append(f"[文件: {fpath}]")
|
||||
wx_msg.content = wx_msg.content + "\n" + "\n".join(refs)
|
||||
file_cache.clear(session_id)
|
||||
|
||||
context = self._compose_context(
|
||||
wx_msg.ctype,
|
||||
wx_msg.content,
|
||||
isgroup=False,
|
||||
msg=wx_msg,
|
||||
no_need_at=True,
|
||||
)
|
||||
if context:
|
||||
self.produce(context)
|
||||
|
||||
# ── _compose_context ───────────────────────────────────────────────
|
||||
|
||||
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"]
|
||||
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.strip()
|
||||
|
||||
return context
|
||||
|
||||
# ── Send reply ─────────────────────────────────────────────────────
|
||||
|
||||
def send(self, reply: Reply, context: Context):
|
||||
receiver = context.get("receiver", "")
|
||||
msg = context.get("msg")
|
||||
context_token = self._get_context_token(receiver, msg)
|
||||
|
||||
if not context_token:
|
||||
logger.error(f"[Weixin] No context_token for receiver={receiver}, cannot send")
|
||||
return
|
||||
|
||||
if reply.type == ReplyType.TEXT:
|
||||
self._send_text(reply.content, receiver, context_token)
|
||||
elif reply.type in (ReplyType.IMAGE_URL, ReplyType.IMAGE):
|
||||
self._send_image(reply.content, receiver, context_token)
|
||||
elif reply.type == ReplyType.FILE:
|
||||
self._send_file(reply.content, receiver, context_token)
|
||||
elif reply.type in (ReplyType.VIDEO, ReplyType.VIDEO_URL):
|
||||
self._send_video(reply.content, receiver, context_token)
|
||||
else:
|
||||
logger.warning(f"[Weixin] Unsupported reply type: {reply.type}, fallback to text")
|
||||
self._send_text(str(reply.content), receiver, context_token)
|
||||
|
||||
def _get_context_token(self, receiver: str, msg=None) -> str:
|
||||
"""Get the context_token for a receiver, required for all sends."""
|
||||
if msg and hasattr(msg, "context_token") and msg.context_token:
|
||||
return msg.context_token
|
||||
return self._context_tokens.get(receiver, "")
|
||||
|
||||
def _send_text(self, text: str, receiver: str, context_token: str):
|
||||
if len(text) <= TEXT_CHUNK_LIMIT:
|
||||
try:
|
||||
self.api.send_text(receiver, text, context_token)
|
||||
logger.debug(f"[Weixin] Text sent to {receiver}, len={len(text)}")
|
||||
except Exception as e:
|
||||
logger.error(f"[Weixin] Failed to send text: {e}")
|
||||
return
|
||||
|
||||
chunks = self._split_text(text, TEXT_CHUNK_LIMIT)
|
||||
for i, chunk in enumerate(chunks):
|
||||
try:
|
||||
self.api.send_text(receiver, chunk, context_token)
|
||||
logger.debug(f"[Weixin] Text chunk {i+1}/{len(chunks)} sent to {receiver}, len={len(chunk)}")
|
||||
except Exception as e:
|
||||
logger.error(f"[Weixin] Failed to send text chunk {i+1}/{len(chunks)}: {e}")
|
||||
break
|
||||
if i < len(chunks) - 1:
|
||||
time.sleep(0.5)
|
||||
|
||||
@staticmethod
|
||||
def _split_text(text: str, limit: int) -> list:
|
||||
"""Split text into chunks, preferring to break at paragraph or line boundaries."""
|
||||
if len(text) <= limit:
|
||||
return [text]
|
||||
chunks = []
|
||||
while text:
|
||||
if len(text) <= limit:
|
||||
chunks.append(text)
|
||||
break
|
||||
cut = text.rfind("\n\n", 0, limit)
|
||||
if cut <= 0:
|
||||
cut = text.rfind("\n", 0, limit)
|
||||
if cut <= 0:
|
||||
cut = limit
|
||||
chunks.append(text[:cut])
|
||||
text = text[cut:].lstrip("\n")
|
||||
return chunks
|
||||
|
||||
def _send_image(self, img_path_or_url: str, receiver: str, context_token: str):
|
||||
local_path = self._resolve_media_path(img_path_or_url)
|
||||
if not local_path:
|
||||
self._send_text("[Image send failed: file not found]", receiver, context_token)
|
||||
return
|
||||
try:
|
||||
result = upload_media_to_cdn(self.api, local_path, receiver, media_type=1)
|
||||
self.api.send_image_item(
|
||||
to=receiver,
|
||||
context_token=context_token,
|
||||
encrypt_query_param=result["encrypt_query_param"],
|
||||
aes_key_b64=result["aes_key_b64"],
|
||||
ciphertext_size=result["ciphertext_size"],
|
||||
)
|
||||
logger.info(f"[Weixin] Image sent to {receiver}")
|
||||
except Exception as e:
|
||||
logger.error(f"[Weixin] Image send failed: {e}")
|
||||
self._send_text("[Image send failed]", receiver, context_token)
|
||||
|
||||
def _send_file(self, file_path_or_url: str, receiver: str, context_token: str):
|
||||
local_path = self._resolve_media_path(file_path_or_url)
|
||||
if not local_path:
|
||||
self._send_text("[File send failed: file not found]", receiver, context_token)
|
||||
return
|
||||
try:
|
||||
result = upload_media_to_cdn(self.api, local_path, receiver, media_type=3)
|
||||
self.api.send_file_item(
|
||||
to=receiver,
|
||||
context_token=context_token,
|
||||
encrypt_query_param=result["encrypt_query_param"],
|
||||
aes_key_b64=result["aes_key_b64"],
|
||||
file_name=os.path.basename(local_path),
|
||||
file_size=result["raw_size"],
|
||||
)
|
||||
logger.info(f"[Weixin] File sent to {receiver}")
|
||||
except Exception as e:
|
||||
logger.error(f"[Weixin] File send failed: {e}")
|
||||
self._send_text("[File send failed]", receiver, context_token)
|
||||
|
||||
def _send_video(self, video_path_or_url: str, receiver: str, context_token: str):
|
||||
local_path = self._resolve_media_path(video_path_or_url)
|
||||
if not local_path:
|
||||
self._send_text("[Video send failed: file not found]", receiver, context_token)
|
||||
return
|
||||
try:
|
||||
result = upload_media_to_cdn(self.api, local_path, receiver, media_type=2)
|
||||
self.api.send_video_item(
|
||||
to=receiver,
|
||||
context_token=context_token,
|
||||
encrypt_query_param=result["encrypt_query_param"],
|
||||
aes_key_b64=result["aes_key_b64"],
|
||||
ciphertext_size=result["ciphertext_size"],
|
||||
)
|
||||
logger.info(f"[Weixin] Video sent to {receiver}")
|
||||
except Exception as e:
|
||||
logger.error(f"[Weixin] Video send failed: {e}")
|
||||
self._send_text("[Video send failed]", receiver, context_token)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_media_path(path_or_url: str) -> str:
|
||||
"""Resolve a file path or URL to a local file path. Downloads if needed."""
|
||||
if not path_or_url:
|
||||
return ""
|
||||
|
||||
local_path = path_or_url
|
||||
if local_path.startswith("file://"):
|
||||
local_path = local_path[7:]
|
||||
|
||||
if local_path.startswith(("http://", "https://")):
|
||||
try:
|
||||
resp = requests.get(local_path, timeout=60)
|
||||
resp.raise_for_status()
|
||||
ct = resp.headers.get("Content-Type", "")
|
||||
ext = ".bin"
|
||||
if "jpeg" in ct or "jpg" in ct:
|
||||
ext = ".jpg"
|
||||
elif "png" in ct:
|
||||
ext = ".png"
|
||||
elif "gif" in ct:
|
||||
ext = ".gif"
|
||||
elif "webp" in ct:
|
||||
ext = ".webp"
|
||||
elif "mp4" in ct:
|
||||
ext = ".mp4"
|
||||
elif "pdf" in ct:
|
||||
ext = ".pdf"
|
||||
|
||||
tmp_path = f"/tmp/wx_media_{uuid.uuid4().hex[:8]}{ext}"
|
||||
with open(tmp_path, "wb") as f:
|
||||
f.write(resp.content)
|
||||
return tmp_path
|
||||
except Exception as e:
|
||||
logger.error(f"[Weixin] Failed to download media: {e}")
|
||||
return ""
|
||||
|
||||
if os.path.exists(local_path):
|
||||
return local_path
|
||||
|
||||
logger.warning(f"[Weixin] Media file not found: {local_path}")
|
||||
return ""
|
||||
200
channel/weixin/weixin_message.py
Normal file
200
channel/weixin/weixin_message.py
Normal file
@@ -0,0 +1,200 @@
|
||||
"""
|
||||
Weixin ChatMessage implementation.
|
||||
|
||||
Parses WeixinMessage from the getUpdates API into the unified ChatMessage format.
|
||||
"""
|
||||
|
||||
import os
|
||||
import uuid
|
||||
|
||||
from bridge.context import ContextType
|
||||
from channel.chat_message import ChatMessage
|
||||
from channel.weixin.weixin_api import download_media_from_cdn, CDN_BASE_URL
|
||||
from common.log import logger
|
||||
from common.utils import expand_path
|
||||
from config import conf
|
||||
|
||||
|
||||
# MessageItemType constants from the Weixin protocol
|
||||
ITEM_TEXT = 1
|
||||
ITEM_IMAGE = 2
|
||||
ITEM_VOICE = 3
|
||||
ITEM_FILE = 4
|
||||
ITEM_VIDEO = 5
|
||||
|
||||
|
||||
def _get_tmp_dir() -> str:
|
||||
ws_root = expand_path(conf().get("agent_workspace", "~/cow"))
|
||||
tmp_dir = os.path.join(ws_root, "tmp")
|
||||
os.makedirs(tmp_dir, exist_ok=True)
|
||||
return tmp_dir
|
||||
|
||||
|
||||
class WeixinMessage(ChatMessage):
|
||||
"""Message wrapper for Weixin channel."""
|
||||
|
||||
def __init__(self, msg: dict, cdn_base_url: str = CDN_BASE_URL):
|
||||
super().__init__(msg)
|
||||
|
||||
self.msg_id = str(msg.get("message_id", msg.get("seq", uuid.uuid4().hex[:8])))
|
||||
self.create_time = msg.get("create_time_ms", 0)
|
||||
self.context_token = msg.get("context_token", "")
|
||||
self.is_group = False # Weixin plugin only supports direct chat
|
||||
self.is_at = False
|
||||
|
||||
from_user_id = msg.get("from_user_id", "")
|
||||
to_user_id = msg.get("to_user_id", "")
|
||||
|
||||
self.from_user_id = from_user_id
|
||||
self.from_user_nickname = from_user_id
|
||||
self.to_user_id = to_user_id
|
||||
self.to_user_nickname = to_user_id
|
||||
self.other_user_id = from_user_id
|
||||
self.other_user_nickname = from_user_id
|
||||
self.actual_user_id = from_user_id
|
||||
self.actual_user_nickname = from_user_id
|
||||
|
||||
item_list = msg.get("item_list", [])
|
||||
|
||||
# Parse items: find text and media
|
||||
text_body = ""
|
||||
media_item = None
|
||||
media_type = None
|
||||
ref_text = ""
|
||||
|
||||
for item in item_list:
|
||||
itype = item.get("type", 0)
|
||||
|
||||
if itype == ITEM_TEXT:
|
||||
text_item = item.get("text_item", {})
|
||||
text_body = text_item.get("text", "")
|
||||
|
||||
ref = item.get("ref_msg")
|
||||
if ref:
|
||||
ref_title = ref.get("title", "")
|
||||
ref_mi = ref.get("message_item", {})
|
||||
ref_body = ""
|
||||
if ref_mi.get("type") == ITEM_TEXT:
|
||||
ref_body = ref_mi.get("text_item", {}).get("text", "")
|
||||
if ref_title or ref_body:
|
||||
parts = [p for p in [ref_title, ref_body] if p]
|
||||
ref_text = f"[引用: {' | '.join(parts)}]\n"
|
||||
# If ref is a media item, treat it as the media to download
|
||||
if ref_mi.get("type") in (ITEM_IMAGE, ITEM_VIDEO, ITEM_FILE):
|
||||
media_item = ref_mi
|
||||
media_type = ref_mi.get("type")
|
||||
|
||||
elif itype == ITEM_VOICE:
|
||||
voice_item = item.get("voice_item", {})
|
||||
voice_text = voice_item.get("text", "")
|
||||
if voice_text:
|
||||
text_body = voice_text
|
||||
else:
|
||||
# Voice without transcription - download the audio
|
||||
media_item = item
|
||||
media_type = ITEM_VOICE
|
||||
|
||||
elif itype in (ITEM_IMAGE, ITEM_VIDEO, ITEM_FILE):
|
||||
if not media_item:
|
||||
media_item = item
|
||||
media_type = itype
|
||||
|
||||
# Determine ctype and content
|
||||
if media_item and not text_body:
|
||||
self._setup_media(media_item, media_type, cdn_base_url)
|
||||
elif media_item and text_body:
|
||||
# Text + media: download media, attach as file ref in text
|
||||
self.ctype = ContextType.TEXT
|
||||
media_path = self._download_media(media_item, media_type, cdn_base_url)
|
||||
if media_path:
|
||||
if media_type == ITEM_IMAGE:
|
||||
text_body += f"\n[图片: {media_path}]"
|
||||
elif media_type == ITEM_VIDEO:
|
||||
text_body += f"\n[视频: {media_path}]"
|
||||
else:
|
||||
text_body += f"\n[文件: {media_path}]"
|
||||
self.content = ref_text + text_body
|
||||
else:
|
||||
self.ctype = ContextType.TEXT
|
||||
self.content = ref_text + text_body
|
||||
|
||||
def _setup_media(self, item: dict, media_type: int, cdn_base_url: str):
|
||||
"""Set up message as a media type, with lazy download via _prepare_fn."""
|
||||
if media_type == ITEM_IMAGE:
|
||||
self.ctype = ContextType.IMAGE
|
||||
image_path = self._download_media(item, ITEM_IMAGE, cdn_base_url)
|
||||
if image_path:
|
||||
self.content = image_path
|
||||
self.image_path = image_path
|
||||
else:
|
||||
self.ctype = ContextType.TEXT
|
||||
self.content = "[Image download failed]"
|
||||
|
||||
elif media_type == ITEM_VIDEO:
|
||||
self.ctype = ContextType.FILE
|
||||
save_path = os.path.join(_get_tmp_dir(), f"wx_{self.msg_id}.mp4")
|
||||
self.content = save_path
|
||||
|
||||
def _download():
|
||||
path = self._download_media(item, ITEM_VIDEO, cdn_base_url)
|
||||
if path:
|
||||
self.content = path
|
||||
self._prepare_fn = _download
|
||||
|
||||
elif media_type == ITEM_FILE:
|
||||
self.ctype = ContextType.FILE
|
||||
file_name = item.get("file_item", {}).get("file_name", f"wx_{self.msg_id}")
|
||||
save_path = os.path.join(_get_tmp_dir(), file_name)
|
||||
self.content = save_path
|
||||
|
||||
def _download():
|
||||
path = self._download_media(item, ITEM_FILE, cdn_base_url)
|
||||
if path:
|
||||
self.content = path
|
||||
self._prepare_fn = _download
|
||||
|
||||
elif media_type == ITEM_VOICE:
|
||||
self.ctype = ContextType.VOICE
|
||||
save_path = os.path.join(_get_tmp_dir(), f"wx_{self.msg_id}.silk")
|
||||
self.content = save_path
|
||||
|
||||
def _download():
|
||||
path = self._download_media(item, ITEM_VOICE, cdn_base_url)
|
||||
if path:
|
||||
self.content = path
|
||||
self._prepare_fn = _download
|
||||
|
||||
def _download_media(self, item: dict, media_type: int, cdn_base_url: str) -> str:
|
||||
"""Download media from CDN, returns local file path or empty string."""
|
||||
type_key_map = {
|
||||
ITEM_IMAGE: "image_item",
|
||||
ITEM_VIDEO: "video_item",
|
||||
ITEM_FILE: "file_item",
|
||||
ITEM_VOICE: "voice_item",
|
||||
}
|
||||
key = type_key_map.get(media_type, "")
|
||||
info = item.get(key, {})
|
||||
media = info.get("media", {})
|
||||
|
||||
encrypt_param = media.get("encrypt_query_param", "")
|
||||
# aes_key can be in image_item.aeskey (hex) or media.aes_key (b64)
|
||||
aes_key = info.get("aeskey", "") or media.get("aes_key", "")
|
||||
|
||||
if not encrypt_param or not aes_key:
|
||||
logger.warning(f"[Weixin] Missing CDN params for media download (type={media_type})")
|
||||
return ""
|
||||
|
||||
ext_map = {ITEM_IMAGE: ".jpg", ITEM_VIDEO: ".mp4", ITEM_FILE: "", ITEM_VOICE: ".silk"}
|
||||
ext = ext_map.get(media_type, "")
|
||||
if media_type == ITEM_FILE:
|
||||
ext = os.path.splitext(info.get("file_name", ""))[1] or ".bin"
|
||||
|
||||
save_path = os.path.join(_get_tmp_dir(), f"wx_{self.msg_id}{ext}")
|
||||
|
||||
try:
|
||||
download_media_from_cdn(cdn_base_url, encrypt_param, aes_key, save_path)
|
||||
logger.info(f"[Weixin] Media downloaded: {save_path}")
|
||||
return save_path
|
||||
except Exception as e:
|
||||
logger.error(f"[Weixin] Media download failed: {e}")
|
||||
return ""
|
||||
@@ -3,6 +3,18 @@ Cloud management client for connecting to the LinkAI control console.
|
||||
|
||||
Handles remote configuration sync, message push, and skill management
|
||||
via the LinkAI socket protocol.
|
||||
|
||||
NOTE: By default, no cloud-related config is enabled. The application runs
|
||||
entirely locally without connecting to any remote service. The cloud client
|
||||
is only activated when BOTH of the following conditions are met:
|
||||
|
||||
1. ``use_linkai`` is set to True in config (checked in app.py before
|
||||
importing this module).
|
||||
2. ``cloud_deployment_id`` (or env CLOUD_DEPLOYMENT_ID) is non-empty
|
||||
(checked in app.py and again in the ``start()`` function below).
|
||||
|
||||
If either condition is missing, this module is never loaded and the
|
||||
program continues as a purely local application.
|
||||
"""
|
||||
|
||||
from bridge.context import Context, ContextType
|
||||
@@ -201,27 +213,36 @@ class CloudClient(LinkAIClient):
|
||||
|
||||
def _handle_channel_create(self, channel_type: str, data: dict):
|
||||
local_config = conf()
|
||||
self._set_channel_credentials(local_config, channel_type,
|
||||
data.get("appId"), data.get("appSecret"))
|
||||
cred_changed = self._set_channel_credentials(
|
||||
local_config, channel_type, data.get("appId"), data.get("appSecret"))
|
||||
self._add_channel_type(local_config, channel_type)
|
||||
self._save_config_to_file(local_config)
|
||||
|
||||
if self.channel_mgr:
|
||||
if not self.channel_mgr:
|
||||
return
|
||||
|
||||
existing_ch = self.channel_mgr.get_channel(channel_type)
|
||||
if existing_ch and not cred_changed:
|
||||
logger.info(f"[CloudClient] Channel '{channel_type}' already running with same config, "
|
||||
"skip restart, reporting status only")
|
||||
threading.Thread(
|
||||
target=self._do_add_channel, args=(channel_type,), daemon=True
|
||||
target=self._report_channel_startup, args=(channel_type,), daemon=True
|
||||
).start()
|
||||
return
|
||||
|
||||
threading.Thread(
|
||||
target=self._do_add_channel, args=(channel_type,), daemon=True
|
||||
).start()
|
||||
|
||||
def _handle_channel_update(self, channel_type: str, data: dict):
|
||||
local_config = conf()
|
||||
enabled = data.get("enabled", "Y")
|
||||
|
||||
self._set_channel_credentials(local_config, channel_type,
|
||||
data.get("appId"), data.get("appSecret"))
|
||||
cred_changed = self._set_channel_credentials(
|
||||
local_config, channel_type, data.get("appId"), data.get("appSecret"))
|
||||
if enabled == "N":
|
||||
self._remove_channel_type(local_config, channel_type)
|
||||
else:
|
||||
# Ensure channel_type is persisted even if this channel was not
|
||||
# previously listed (e.g. update used as implicit create).
|
||||
self._add_channel_type(local_config, channel_type)
|
||||
self._save_config_to_file(local_config)
|
||||
|
||||
@@ -233,9 +254,17 @@ class CloudClient(LinkAIClient):
|
||||
target=self._do_remove_channel, args=(channel_type,), daemon=True
|
||||
).start()
|
||||
else:
|
||||
threading.Thread(
|
||||
target=self._do_restart_channel, args=(self.channel_mgr, channel_type), daemon=True
|
||||
).start()
|
||||
existing_ch = self.channel_mgr.get_channel(channel_type)
|
||||
if existing_ch and not cred_changed:
|
||||
logger.info(f"[CloudClient] Channel '{channel_type}' already running with same config, "
|
||||
"skip restart, reporting status only")
|
||||
threading.Thread(
|
||||
target=self._report_channel_startup, args=(channel_type,), daemon=True
|
||||
).start()
|
||||
else:
|
||||
threading.Thread(
|
||||
target=self._do_restart_channel, args=(self.channel_mgr, channel_type), daemon=True
|
||||
).start()
|
||||
|
||||
def _handle_channel_delete(self, channel_type: str, data: dict):
|
||||
local_config = conf()
|
||||
@@ -243,11 +272,27 @@ class CloudClient(LinkAIClient):
|
||||
self._remove_channel_type(local_config, channel_type)
|
||||
self._save_config_to_file(local_config)
|
||||
|
||||
if channel_type in ("weixin", "wx"):
|
||||
self._remove_weixin_credentials()
|
||||
|
||||
if self.channel_mgr:
|
||||
threading.Thread(
|
||||
target=self._do_remove_channel, args=(channel_type,), daemon=True
|
||||
).start()
|
||||
|
||||
@staticmethod
|
||||
def _remove_weixin_credentials():
|
||||
"""Remove the weixin token credentials file so next connect triggers QR login."""
|
||||
cred_path = os.path.expanduser(
|
||||
conf().get("weixin_credentials_path", "~/.weixin_cow_credentials.json")
|
||||
)
|
||||
try:
|
||||
if os.path.exists(cred_path):
|
||||
os.remove(cred_path)
|
||||
logger.info(f"[CloudClient] Removed weixin credentials: {cred_path}")
|
||||
except Exception as e:
|
||||
logger.warning(f"[CloudClient] Failed to remove weixin credentials: {e}")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# channel credentials helpers
|
||||
# ------------------------------------------------------------------
|
||||
@@ -322,7 +367,7 @@ class CloudClient(LinkAIClient):
|
||||
self.channel_mgr.add_channel(channel_type)
|
||||
logger.info(f"[CloudClient] Channel '{channel_type}' added successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"[CloudClient] Failed to add channel '{channel_type}': {e}")
|
||||
logger.error(f"[CloudClient] Failed to add channel '{channel_type}': {e}", exc_info=True)
|
||||
self.send_channel_status(channel_type, "error", str(e))
|
||||
return
|
||||
self._report_channel_startup(channel_type)
|
||||
@@ -334,12 +379,31 @@ class CloudClient(LinkAIClient):
|
||||
except Exception as e:
|
||||
logger.error(f"[CloudClient] Failed to remove channel '{channel_type}': {e}")
|
||||
|
||||
def send_channel_qrcode(self, channel_type: str, qrcode_url: str):
|
||||
"""Report QR code URL for a channel that requires scan-to-login."""
|
||||
if self.client_id:
|
||||
from linkai.api.client.client import ClientMsgType
|
||||
msg = self._build_package(ClientMsgType.CHANNEL_STATUS)
|
||||
msg["data"]["channelType"] = channel_type
|
||||
msg["data"]["status"] = "qrcode"
|
||||
msg["data"]["qrcodeUrl"] = qrcode_url
|
||||
self._send_package(msg)
|
||||
logger.info(f"[CloudClient] Sent QR code status for '{channel_type}'")
|
||||
|
||||
def _report_channel_startup(self, channel_type: str):
|
||||
"""Wait for channel startup result and report to cloud."""
|
||||
ch = self.channel_mgr.get_channel(channel_type)
|
||||
if not ch:
|
||||
self.send_channel_status(channel_type, "error", "channel instance not found")
|
||||
return
|
||||
|
||||
if channel_type in ("weixin", "wx") and hasattr(ch, "login_status"):
|
||||
login_status = getattr(ch, "login_status", "")
|
||||
if login_status in ("waiting_scan", "scanned", "idle"):
|
||||
logger.info(f"[CloudClient] Channel '{channel_type}' is waiting for QR login, "
|
||||
"skip reporting connected")
|
||||
return
|
||||
|
||||
success, error = ch.wait_startup(timeout=3)
|
||||
if success:
|
||||
logger.info(f"[CloudClient] Channel '{channel_type}' connected, reporting status")
|
||||
|
||||
@@ -92,14 +92,16 @@ QWEN35_PLUS = "qwen3.5-plus" # Qwen3.5 Plus - Omni model (MultiModalConversatio
|
||||
QWQ_PLUS = "qwq-plus"
|
||||
|
||||
# MiniMax
|
||||
MINIMAX_M2_5 = "MiniMax-M2.5" # MiniMax M2.5 - Latest
|
||||
MINIMAX_M2_1 = "MiniMax-M2.1" # MiniMax M2.1 - Agent推荐模型
|
||||
MINIMAX_M2_7 = "MiniMax-M2.7" # MiniMax M2.7 - Latest
|
||||
MINIMAX_M2_5 = "MiniMax-M2.5" # MiniMax M2.5
|
||||
MINIMAX_M2_1 = "MiniMax-M2.1" # MiniMax M2.1
|
||||
MINIMAX_M2_1_LIGHTNING = "MiniMax-M2.1-lightning" # MiniMax M2.1 极速版
|
||||
MINIMAX_M2 = "MiniMax-M2" # MiniMax M2
|
||||
MINIMAX_ABAB6_5 = "abab6.5-chat" # MiniMax abab6.5
|
||||
|
||||
# GLM (智谱AI)
|
||||
GLM_5 = "glm-5" # 智谱 GLM-5 - Latest
|
||||
GLM_5_TURBO = "glm-5-turbo" # 智谱 GLM-5-Turbo - Latest
|
||||
GLM_5 = "glm-5" # 智谱 GLM-5
|
||||
GLM_4 = "glm-4"
|
||||
GLM_4_PLUS = "glm-4-plus"
|
||||
GLM_4_flash = "glm-4-flash"
|
||||
@@ -166,10 +168,10 @@ MODEL_LIST = [
|
||||
QWEN, QWEN_TURBO, QWEN_PLUS, QWEN_MAX, QWEN_LONG, QWEN3_MAX, QWEN35_PLUS,
|
||||
|
||||
# MiniMax
|
||||
MiniMax, MINIMAX_M2_5, MINIMAX_M2_1, MINIMAX_M2_1_LIGHTNING, MINIMAX_M2, MINIMAX_ABAB6_5,
|
||||
MiniMax, MINIMAX_M2_7, MINIMAX_M2_5, MINIMAX_M2_1, MINIMAX_M2_1_LIGHTNING, MINIMAX_M2, MINIMAX_ABAB6_5,
|
||||
|
||||
# GLM
|
||||
ZHIPU_AI, GLM_5, GLM_4, GLM_4_PLUS, GLM_4_flash, GLM_4_LONG, GLM_4_ALLTOOLS,
|
||||
ZHIPU_AI, GLM_5_TURBO, GLM_5, GLM_4, GLM_4_PLUS, GLM_4_flash, GLM_4_LONG, GLM_4_ALLTOOLS,
|
||||
GLM_4_0520, GLM_4_AIR, GLM_4_AIRX, GLM_4_7,
|
||||
|
||||
# Kimi
|
||||
@@ -191,3 +193,4 @@ FEISHU = "feishu"
|
||||
DINGTALK = "dingtalk"
|
||||
WECOM_BOT = "wecom_bot"
|
||||
QQ = "qq"
|
||||
WEIXIN = "weixin"
|
||||
|
||||
@@ -115,3 +115,22 @@ def expand_path(path: str) -> str:
|
||||
expanded = os.path.join(home, path[2:])
|
||||
|
||||
return expanded
|
||||
|
||||
|
||||
def get_cloud_headers(api_key: str) -> dict:
|
||||
"""
|
||||
Build standard headers for LinkAI API requests,
|
||||
including client_id when available.
|
||||
"""
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
}
|
||||
try:
|
||||
from linkai import LinkAIClient
|
||||
client_id = LinkAIClient.fetch_client_id()
|
||||
if client_id:
|
||||
headers["X-Client-Id"] = client_id
|
||||
except Exception:
|
||||
pass
|
||||
return headers
|
||||
|
||||
17
common/ws_client_compat.py
Normal file
17
common/ws_client_compat.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import inspect
|
||||
from typing import Any
|
||||
|
||||
|
||||
def websocket_app_run_forever(ws: Any, **kwargs: Any) -> None:
|
||||
"""
|
||||
Call WebSocketApp.run_forever; strip reconnect= if the installed
|
||||
websocket-client is too old (reconnect was added in a later 1.x release).
|
||||
"""
|
||||
if "reconnect" in kwargs:
|
||||
try:
|
||||
params = inspect.signature(ws.run_forever).parameters
|
||||
except (TypeError, ValueError):
|
||||
params = {}
|
||||
if "reconnect" not in params:
|
||||
kwargs = {k: v for k, v in kwargs.items() if k != "reconnect"}
|
||||
ws.run_forever(**kwargs)
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"channel_type": "web",
|
||||
"model": "MiniMax-M2.5",
|
||||
"channel_type": "weixin",
|
||||
"model": "MiniMax-M2.7",
|
||||
"minimax_api_key": "",
|
||||
"zhipu_ai_api_key": "",
|
||||
"ark_api_key": "",
|
||||
|
||||
10
config.py
10
config.py
@@ -153,10 +153,15 @@ available_setting = {
|
||||
# 企微智能机器人配置(长连接模式)
|
||||
"wecom_bot_id": "", # 企微智能机器人BotID
|
||||
"wecom_bot_secret": "", # 企微智能机器人长连接Secret
|
||||
# 微信配置
|
||||
"weixin_token": "", # 微信登录后获取的bot_token,留空则启动时自动扫码登录
|
||||
"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_credentials_path": "~/.weixin_cow_credentials.json", # credentials file path
|
||||
# chatgpt指令自定义触发词
|
||||
"clear_memory_commands": ["#清除记忆"], # 重置会话指令,必须以#开头
|
||||
# channel配置
|
||||
"channel_type": "", # 通道类型,支持多渠道同时运行。单个: "feishu",多个: "feishu, dingtalk" 或 ["feishu", "dingtalk"]。可选值: web,feishu,dingtalk,wecom_bot,wechatmp,wechatmp_service,wechatcom_app
|
||||
"channel_type": "", # 通道类型,支持多渠道同时运行。单个: "feishu",多个: "feishu, dingtalk" 或 ["feishu", "dingtalk"]。可选值: web,feishu,dingtalk,wecom_bot,weixin,wechatmp,wechatmp_service,wechatcom_app
|
||||
"web_console": True, # 是否自动启动Web控制台(默认启动)。设为False可禁用
|
||||
"subscribe_msg": "", # 订阅消息, 支持: wechatmp, wechatmp_service, wechatcom_app
|
||||
"debug": False, # 是否开启debug模式,开启后会打印更多日志
|
||||
@@ -382,7 +387,8 @@ def load_config():
|
||||
"wechatcomapp_agent_id": "WECHATCOMAPP_AGENT_ID",
|
||||
"wechatcomapp_secret": "WECHATCOMAPP_SECRET",
|
||||
"qq_app_id": "QQ_APP_ID",
|
||||
"qq_app_secret": "QQ_APP_SECRET"
|
||||
"qq_app_secret": "QQ_APP_SECRET",
|
||||
"weixin_token": "WEIXIN_TOKEN",
|
||||
}
|
||||
injected = 0
|
||||
for conf_key, env_key in _CONFIG_TO_ENV.items():
|
||||
|
||||
@@ -8,8 +8,8 @@ services:
|
||||
ports:
|
||||
- "9899:9899"
|
||||
environment:
|
||||
CHANNEL_TYPE: 'web'
|
||||
MODEL: 'MiniMax-M2.5'
|
||||
CHANNEL_TYPE: 'weixin'
|
||||
MODEL: 'MiniMax-M2.7'
|
||||
MINIMAX_API_KEY: ''
|
||||
ZHIPU_AI_API_KEY: ''
|
||||
ARK_API_KEY: ''
|
||||
|
||||
@@ -137,8 +137,8 @@ bash <(curl -fsSL https://cdn.link-ai.tech/code/cow/run.sh)
|
||||
|
||||
Agent模式推荐使用以下模型,可根据效果及成本综合选择:
|
||||
|
||||
- **MiniMax**: `MiniMax-M2.5`
|
||||
- **GLM**: `glm-5`
|
||||
- **MiniMax**: `MiniMax-M2.7`
|
||||
- **GLM**: `glm-5-turbo`
|
||||
- **Kimi**: `kimi-k2.5`
|
||||
- **Doubao**: `doubao-seed-2-0-code-preview-260215`
|
||||
- **Qwen**: `qwen3.5-plus`
|
||||
|
||||
74
docs/channels/weixin.mdx
Normal file
74
docs/channels/weixin.mdx
Normal file
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title: 微信
|
||||
description: 将 CowAgent 接入个人微信
|
||||
---
|
||||
|
||||
> 接入个人微信,扫码登录即可使用,支持文本、图片、语音、文件、视频等消息的收发。
|
||||
|
||||
## 一、配置和运行
|
||||
|
||||
### 方式一:Web 控制台接入
|
||||
|
||||
启动 Cow 项目后打开 Web 控制台 (本地链接为: http://127.0.0.1:9899/ ),选择 **通道** 菜单,点击 **接入通道**,选择 **微信**,点击接入后按照提示扫码登录。
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/20260322195114.png" width="800" />
|
||||
|
||||
### 方式二:配置文件接入
|
||||
|
||||
在 `config.json` 中设置 `channel_type` 为 `weixin`:
|
||||
|
||||
```json
|
||||
{
|
||||
"channel_type": "weixin"
|
||||
}
|
||||
```
|
||||
|
||||
启动程序后,终端会显示二维码,使用微信扫码授权即可完成登录。
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/20260322195509.png" width="800" />
|
||||
|
||||
|
||||
<Note>
|
||||
1. 兼容历史配置:`channel_type` 设为 `wx` 同样可以启用微信通道。
|
||||
2. 注意微信客户端需要更新至 8.0.69 版本或以上
|
||||
</Note>
|
||||
|
||||
## 二、使用说明
|
||||
|
||||
微信扫码并进行授权确认后,即可完成接入并开始对话。接入微信后会在对话中创建出一个机器人助理,不会对已有账号的正常使用有任何影响。
|
||||
|
||||
> 你可以通过搜索"微信ClawBot"随时找到这个机器人,还可以修改这个机器人的头像、备注等信息,将机器人置顶在消息列表等。
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/83ae8251d896219fde4803f4205205be.jpg" width="250" />
|
||||
|
||||
|
||||
|
||||
## 三、登录说明
|
||||
|
||||
### 扫码登录
|
||||
|
||||
首次启动时,终端会显示一个二维码(有效期约 2 分钟)。使用微信扫描二维码并在手机上确认后即可完成登录。
|
||||
|
||||
- 二维码过期后会自动刷新并重新显示
|
||||
- `requirements.txt` 中已默认包含 `qrcode` 依赖,安装后可在终端直接渲染二维码图案
|
||||
|
||||
### 凭证保存
|
||||
|
||||
登录成功后,凭证会自动保存至 `~/.weixin_cow_credentials.json`,下次启动时无需重新扫码。
|
||||
|
||||
如需重新登录,删除该凭证文件后重启程序即可。
|
||||
|
||||
### Session 过期
|
||||
|
||||
当微信 session 过期时(errcode -14),程序会自动清除旧凭证并重新发起扫码登录,无需手动干预。
|
||||
|
||||
## 四、功能说明
|
||||
|
||||
| 功能 | 支持情况 |
|
||||
| --- | --- |
|
||||
| 单聊 | ✅ |
|
||||
| 文本消息 | ✅ 收发 |
|
||||
| 图片消息 | ✅ 收发 |
|
||||
| 文件消息 | ✅ 收发 |
|
||||
| 视频消息 | ✅ 收发 |
|
||||
| 语音消息 | ✅ 接收 (自带语音识别) |
|
||||
154
docs/docs.json
154
docs/docs.json
@@ -155,6 +155,7 @@
|
||||
{
|
||||
"group": "接入渠道",
|
||||
"pages": [
|
||||
"channels/weixin",
|
||||
"channels/web",
|
||||
"channels/feishu",
|
||||
"channels/dingtalk",
|
||||
@@ -173,6 +174,7 @@
|
||||
"group": "发布记录",
|
||||
"pages": [
|
||||
"releases/overview",
|
||||
"releases/v2.0.4",
|
||||
"releases/v2.0.3",
|
||||
"releases/v2.0.2",
|
||||
"releases/v2.0.1",
|
||||
@@ -301,6 +303,7 @@
|
||||
{
|
||||
"group": "Platforms",
|
||||
"pages": [
|
||||
"en/channels/weixin",
|
||||
"en/channels/web",
|
||||
"en/channels/feishu",
|
||||
"en/channels/dingtalk",
|
||||
@@ -319,6 +322,7 @@
|
||||
"group": "Release Notes",
|
||||
"pages": [
|
||||
"en/releases/overview",
|
||||
"en/releases/v2.0.4",
|
||||
"en/releases/v2.0.2",
|
||||
"en/releases/v2.0.1",
|
||||
"en/releases/v2.0.0"
|
||||
@@ -327,6 +331,156 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"language": "ja",
|
||||
"tabs": [
|
||||
{
|
||||
"tab": "紹介",
|
||||
"groups": [
|
||||
{
|
||||
"group": "概要",
|
||||
"pages": [
|
||||
"ja/intro/index",
|
||||
"ja/intro/architecture",
|
||||
"ja/intro/features"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "クイックスタート",
|
||||
"groups": [
|
||||
{
|
||||
"group": "インストール",
|
||||
"pages": [
|
||||
"ja/guide/quick-start",
|
||||
"ja/guide/manual-install",
|
||||
"ja/guide/upgrade"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "モデル",
|
||||
"groups": [
|
||||
{
|
||||
"group": "モデル設定",
|
||||
"pages": [
|
||||
"ja/models/index",
|
||||
"ja/models/minimax",
|
||||
"ja/models/glm",
|
||||
"ja/models/qwen",
|
||||
"ja/models/kimi",
|
||||
"ja/models/doubao",
|
||||
"ja/models/claude",
|
||||
"ja/models/gemini",
|
||||
"ja/models/openai",
|
||||
"ja/models/deepseek",
|
||||
"ja/models/linkai",
|
||||
"ja/models/coding-plan"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "ツール",
|
||||
"groups": [
|
||||
{
|
||||
"group": "ツールシステム",
|
||||
"pages": [
|
||||
"ja/tools/index"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "内蔵ツール",
|
||||
"pages": [
|
||||
"ja/tools/read",
|
||||
"ja/tools/write",
|
||||
"ja/tools/edit",
|
||||
"ja/tools/ls",
|
||||
"ja/tools/bash",
|
||||
"ja/tools/send",
|
||||
"ja/tools/memory",
|
||||
"ja/tools/env-config",
|
||||
"ja/tools/browser"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "オプションツール",
|
||||
"pages": [
|
||||
"ja/tools/web-search",
|
||||
"ja/tools/scheduler"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "スキル",
|
||||
"groups": [
|
||||
{
|
||||
"group": "スキルシステム",
|
||||
"pages": [
|
||||
"ja/skills/index",
|
||||
"ja/skills/skill-creator"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "内蔵スキル",
|
||||
"pages": [
|
||||
"ja/skills/image-vision",
|
||||
"ja/skills/linkai-agent",
|
||||
"ja/skills/web-fetch"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "メモリ",
|
||||
"groups": [
|
||||
{
|
||||
"group": "メモリシステム",
|
||||
"pages": [
|
||||
"ja/memory"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "チャネル",
|
||||
"groups": [
|
||||
{
|
||||
"group": "プラットフォーム",
|
||||
"pages": [
|
||||
"ja/channels/weixin",
|
||||
"ja/channels/web",
|
||||
"ja/channels/feishu",
|
||||
"ja/channels/dingtalk",
|
||||
"ja/channels/wecom-bot",
|
||||
"ja/channels/qq",
|
||||
"ja/channels/wecom",
|
||||
"ja/channels/wechatmp"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "リリース",
|
||||
"groups": [
|
||||
{
|
||||
"group": "リリースノート",
|
||||
"pages": [
|
||||
"ja/releases/overview",
|
||||
"ja/releases/v2.0.4",
|
||||
"ja/releases/v2.0.3",
|
||||
"ja/releases/v2.0.2",
|
||||
"ja/releases/v2.0.1",
|
||||
"ja/releases/v2.0.0"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
<a href="https://github.com/zhayujie/chatgpt-on-wechat/releases/latest"><img src="https://img.shields.io/github/v/release/zhayujie/chatgpt-on-wechat" alt="Latest release"></a>
|
||||
<a href="https://github.com/zhayujie/chatgpt-on-wechat/blob/master/LICENSE"><img src="https://img.shields.io/github/license/zhayujie/chatgpt-on-wechat" alt="License: MIT"></a>
|
||||
<a href="https://github.com/zhayujie/chatgpt-on-wechat"><img src="https://img.shields.io/github/stars/zhayujie/chatgpt-on-wechat?style=flat-square" alt="Stars"></a> <br/>
|
||||
[<a href="https://github.com/zhayujie/chatgpt-on-wechat/blob/master/README.md">中文</a>] | [English]
|
||||
[<a href="https://github.com/zhayujie/chatgpt-on-wechat/blob/master/README.md">中文</a>] | [English] | [<a href="https://github.com/zhayujie/chatgpt-on-wechat/blob/master/docs/ja/README.md">日本語</a>]
|
||||
</p>
|
||||
|
||||
**CowAgent** is an AI super assistant powered by LLMs, capable of autonomous task planning, operating computers and external resources, creating and executing Skills, and continuously growing with long-term memory. It supports flexible model switching, handles text, voice, images, and files, and can be integrated into Web, Feishu, DingTalk, WeCom Bot, WeCom App, and WeChat Official Account — running 7×24 hours on your personal computer or server.
|
||||
**CowAgent** is an AI super assistant powered by LLMs, capable of autonomous task planning, operating computers and external resources, creating and executing Skills, and continuously growing with long-term memory. It supports flexible model switching, handles text, voice, images, and files, and can be integrated into WeChat, Web, Feishu, DingTalk, WeCom Bot, WeCom App, and WeChat Official Account — running 7×24 hours on your personal computer or server.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://cowagent.ai/">🌐 Website</a> ·
|
||||
@@ -25,7 +25,7 @@
|
||||
- ✅ **Skills System**: Implements a Skills creation and execution engine with multiple built-in skills, and supports custom Skills development through natural language conversation.
|
||||
- ✅ **Multimodal Messages**: Supports parsing, processing, generating, and sending text, images, voice, files, and other message types.
|
||||
- ✅ **Multiple Model Support**: Supports OpenAI, Claude, Gemini, DeepSeek, MiniMax, GLM, Qwen, Kimi, Doubao, and other mainstream model providers.
|
||||
- ✅ **Multi-platform Deployment**: Runs on local computers or servers, integrable into Web, Feishu, DingTalk, WeChat Official Account, and WeCom applications.
|
||||
- ✅ **Multi-platform Deployment**: Runs on local computers or servers, integrable into WeChat, Web, Feishu, DingTalk, WeChat Official Account, and WeCom applications.
|
||||
- ✅ **Knowledge Base**: Integrates enterprise knowledge base capabilities via the [LinkAI](https://link-ai.tech) platform.
|
||||
|
||||
## Disclaimer
|
||||
@@ -121,8 +121,8 @@ Supports mainstream model providers. Recommended models for Agent mode:
|
||||
|
||||
| Provider | Recommended Model |
|
||||
| --- | --- |
|
||||
| MiniMax | `MiniMax-M2.5` |
|
||||
| GLM | `glm-5` |
|
||||
| MiniMax | `MiniMax-M2.7` |
|
||||
| GLM | `glm-5-turbo` |
|
||||
| Kimi | `kimi-k2.5` |
|
||||
| Doubao | `doubao-seed-2-0-code-preview-260215` |
|
||||
| Qwen | `qwen3.5-plus` |
|
||||
@@ -163,6 +163,7 @@ Supports multiple platforms. Set `channel_type` in `config.json` to switch:
|
||||
|
||||
| Channel | `channel_type` | Docs |
|
||||
| --- | --- | --- |
|
||||
| WeChat | `weixin` | [WeChat Setup](https://docs.cowagent.ai/en/channels/weixin) |
|
||||
| Web (default) | `web` | [Web Channel](https://docs.cowagent.ai/en/channels/web) |
|
||||
| Feishu | `feishu` | [Feishu Setup](https://docs.cowagent.ai/en/channels/feishu) |
|
||||
| DingTalk | `dingtalk` | [DingTalk Setup](https://docs.cowagent.ai/en/channels/dingtalk) |
|
||||
|
||||
72
docs/en/channels/weixin.mdx
Normal file
72
docs/en/channels/weixin.mdx
Normal file
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: WeChat
|
||||
description: Connect CowAgent to personal WeChat
|
||||
---
|
||||
|
||||
> Connect CowAgent to your personal WeChat. Simply scan a QR code to log in — no public IP required. Supports text, image, voice, file, and video messages.
|
||||
|
||||
## 1. Configuration
|
||||
|
||||
### 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 **WeChat**, and follow the prompts to scan the QR code.
|
||||
|
||||
### Option B: Config File
|
||||
|
||||
Set `channel_type` to `weixin` in your `config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"channel_type": "weixin"
|
||||
}
|
||||
```
|
||||
|
||||
After starting the program, a QR code will be displayed in the terminal. Scan it with WeChat and confirm on your phone to complete login.
|
||||
|
||||
<Note>
|
||||
For backward compatibility, setting `channel_type` to `wx` also activates the WeChat channel.
|
||||
</Note>
|
||||
|
||||
## 2. Parameters
|
||||
|
||||
| Parameter | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `channel_type` | Set to `weixin` or `wx` | — |
|
||||
|
||||
Login credentials are automatically saved to `~/.weixin_cow_credentials.json`. To force a re-login, delete this file and restart.
|
||||
|
||||
## 3. Login
|
||||
|
||||
### QR Code Login
|
||||
|
||||
On first startup, a QR code is displayed in the terminal (valid for approximately 2 minutes). Scan it with WeChat and confirm on your phone.
|
||||
|
||||
- The QR code automatically refreshes when it expires
|
||||
- The `qrcode` dependency is already included in `requirements.txt`, enabling QR code rendering directly in the terminal
|
||||
|
||||
### Credential Persistence
|
||||
|
||||
After successful login, credentials are saved to `~/.weixin_cow_credentials.json`. Subsequent startups will reuse the saved credentials without requiring a new scan.
|
||||
|
||||
To force a re-login, delete the credentials file and restart the program.
|
||||
|
||||
### Session Expiry
|
||||
|
||||
When the WeChat session expires (errcode -14), the program automatically clears old credentials and initiates a new QR login — no manual intervention required.
|
||||
|
||||
## 4. Supported Features
|
||||
|
||||
| Feature | Status |
|
||||
| --- | --- |
|
||||
| Direct Messages | ✅ |
|
||||
| Text Messages | ✅ Send & Receive |
|
||||
| Image Messages | ✅ Send & Receive |
|
||||
| File Messages | ✅ Send & Receive |
|
||||
| Video Messages | ✅ Send & Receive |
|
||||
| Voice Messages | ✅ Receive |
|
||||
|
||||
## 5. Notes
|
||||
|
||||
1. Ensure network access to `ilinkai.weixin.qq.com`.
|
||||
2. Media files (images, files, videos) are transferred via CDN with AES-128-ECB encryption, handled automatically by the program.
|
||||
3. A stable network connection is recommended to avoid frequent disconnections that would require re-scanning.
|
||||
@@ -7,7 +7,7 @@ description: CowAgent - AI Super Assistant powered by LLMs
|
||||
|
||||
**CowAgent** is an AI super assistant powered by LLMs with autonomous task planning, long-term memory, skills system, multimodal messages, multiple model support, and multi-platform deployment.
|
||||
|
||||
CowAgent can proactively think and plan tasks, operate computers and external resources, create and execute Skills, and continuously grow with long-term memory. It supports flexible switching between multiple models, handles text, voice, images, files and other multimodal messages, and can be integrated into web, Feishu, DingTalk, WeCom, and WeChat Official Account. It runs 7x24 hours on your personal computer or server.
|
||||
CowAgent can proactively think and plan tasks, operate computers and external resources, create and execute Skills, and continuously grow with long-term memory. It supports flexible switching between multiple models, handles text, voice, images, files and other multimodal messages, and can be integrated into WeChat, web, Feishu, DingTalk, WeCom, and WeChat Official Account. It runs 7x24 hours on your personal computer or server.
|
||||
|
||||
<Card title="GitHub" icon="github" href="https://github.com/zhayujie/chatgpt-on-wechat">
|
||||
github.com/zhayujie/chatgpt-on-wechat
|
||||
@@ -31,8 +31,8 @@ CowAgent can proactively think and plan tasks, operate computers and external re
|
||||
<Card title="Multiple Model Support" icon="microchip" href="/en/models/index">
|
||||
Supports mainstream model providers including OpenAI, Claude, Gemini, DeepSeek, MiniMax, GLM, Qwen, Kimi, Doubao, and more.
|
||||
</Card>
|
||||
<Card title="Multi-platform Deployment" icon="server" href="/en/channels/web">
|
||||
Runs on local computers or servers, integrable into web, Feishu, DingTalk, WeChat Official Account, and WeCom applications.
|
||||
<Card title="Multi-platform Deployment" icon="server" href="/en/channels/weixin">
|
||||
Runs on local computers or servers, integrable into WeChat, web, Feishu, DingTalk, WeChat Official Account, and WeCom applications.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
|
||||
@@ -5,14 +5,14 @@ description: Zhipu AI GLM model configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "glm-5",
|
||||
"model": "glm-5-turbo",
|
||||
"zhipu_ai_api_key": "YOUR_API_KEY"
|
||||
}
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| --- | --- |
|
||||
| `model` | Options include `glm-5`, `glm-4.7`, `glm-4-plus`, `glm-4-flash`, `glm-4-air`, etc. See [model codes](https://bigmodel.cn/dev/api/normal-model/glm-4) |
|
||||
| `model` | Options include `glm-5-turbo`, `glm-5`, `glm-4.7`, `glm-4-plus`, `glm-4-flash`, `glm-4-air`, etc. See [model codes](https://bigmodel.cn/dev/api/normal-model/glm-4) |
|
||||
| `zhipu_ai_api_key` | Create at [Zhipu AI Console](https://www.bigmodel.cn/usercenter/proj-mgmt/apikeys) |
|
||||
|
||||
OpenAI-compatible configuration is also supported:
|
||||
@@ -20,7 +20,7 @@ OpenAI-compatible configuration is also supported:
|
||||
```json
|
||||
{
|
||||
"bot_type": "openai",
|
||||
"model": "glm-5",
|
||||
"model": "glm-5-turbo",
|
||||
"open_ai_api_base": "https://open.bigmodel.cn/api/paas/v4",
|
||||
"open_ai_api_key": "YOUR_API_KEY"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ description: Supported models and recommended choices for CowAgent
|
||||
CowAgent supports mainstream LLMs from domestic and international providers. Model interfaces are implemented in the project's `models/` directory.
|
||||
|
||||
<Note>
|
||||
For Agent mode, the following models are recommended based on quality and cost: MiniMax-M2.5, glm-5, kimi-k2.5, qwen3.5-plus, claude-sonnet-4-6, gemini-3.1-pro-preview
|
||||
For Agent mode, the following models are recommended based on quality and cost: MiniMax-M2.7, glm-5-turbo, kimi-k2.5, qwen3.5-plus, claude-sonnet-4-6, gemini-3.1-pro-preview
|
||||
</Note>
|
||||
|
||||
## Configuration
|
||||
@@ -19,10 +19,10 @@ You can also use the [LinkAI](https://link-ai.tech) platform interface to flexib
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="MiniMax" href="/en/models/minimax">
|
||||
MiniMax-M2.5 and other series models
|
||||
MiniMax-M2.7 and other series models
|
||||
</Card>
|
||||
<Card title="GLM (Zhipu AI)" href="/en/models/glm">
|
||||
glm-5, glm-4.7 and other series models
|
||||
glm-5-turbo, glm-5 and other series models
|
||||
</Card>
|
||||
<Card title="Qwen (Tongyi Qianwen)" href="/en/models/qwen">
|
||||
qwen3.5-plus, qwen3-max and more
|
||||
|
||||
@@ -5,14 +5,14 @@ description: MiniMax model configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "MiniMax-M2.5",
|
||||
"model": "MiniMax-M2.7",
|
||||
"minimax_api_key": "YOUR_API_KEY"
|
||||
}
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| --- | --- |
|
||||
| `model` | Options include `MiniMax-M2.5`, `MiniMax-M2.1`, `MiniMax-M2.1-lightning`, `MiniMax-M2`, etc. |
|
||||
| `model` | Options include `MiniMax-M2.7`, `MiniMax-M2.5`, `MiniMax-M2.1`, `MiniMax-M2.1-lightning`, `MiniMax-M2`, etc. |
|
||||
| `minimax_api_key` | Create at [MiniMax Console](https://platform.minimaxi.com/user-center/basic-information/interface-key) |
|
||||
|
||||
OpenAI-compatible configuration is also supported:
|
||||
@@ -20,7 +20,7 @@ OpenAI-compatible configuration is also supported:
|
||||
```json
|
||||
{
|
||||
"bot_type": "openai",
|
||||
"model": "MiniMax-M2.5",
|
||||
"model": "MiniMax-M2.7",
|
||||
"open_ai_api_base": "https://api.minimaxi.com/v1",
|
||||
"open_ai_api_key": "YOUR_API_KEY"
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ description: CowAgent version history
|
||||
|
||||
| Version | Date | Description |
|
||||
| --- | --- | --- |
|
||||
| [2.0.4](/en/releases/v2.0.4) | 2026.03.22 | Personal WeChat channel, new model support, Japanese docs, script refactoring and bug fixes |
|
||||
| [2.0.2](/en/releases/v2.0.2) | 2026.02.27 | Web Console upgrade, multi-channel concurrency, session persistence |
|
||||
| [2.0.1](/en/releases/v2.0.1) | 2026.02.27 | Built-in Web Search tool, smart context management, multiple fixes |
|
||||
| [2.0.0](/en/releases/v2.0.0) | 2026.02.03 | Full upgrade to AI super assistant |
|
||||
|
||||
55
docs/en/releases/v2.0.4.mdx
Normal file
55
docs/en/releases/v2.0.4.mdx
Normal file
@@ -0,0 +1,55 @@
|
||||
---
|
||||
title: v2.0.4
|
||||
description: CowAgent 2.0.4 - Personal WeChat channel, new model support, Japanese docs, script refactoring and bug fixes
|
||||
---
|
||||
|
||||
## 🔌 Personal WeChat Channel
|
||||
|
||||
Added personal WeChat (`weixin`) channel — the most important update in this release. Simply scan a QR code to connect CowAgent to your personal WeChat account, with support for:
|
||||
|
||||
- **Messaging**: Send and receive text, image, file, and video messages; receive voice messages
|
||||
- **QR Code Login**: QR code displayed in terminal, scan with WeChat to log in; auto-refresh on expiry
|
||||
- **Credential Persistence**: Login credentials saved to `~/.weixin_cow_credentials.json` automatically, no re-scan needed on restart
|
||||
- **Session Auto-Reconnect**: Automatically clears expired credentials and re-initiates QR code login
|
||||
- **Web Console Integration**: Add WeChat channel from the Web Console with synchronized QR code login flow
|
||||
- **Docker & Script Support**: Both `run.sh` and `docker-compose.yml` now support the WeChat channel
|
||||
|
||||
Documentation: [WeChat Channel](https://docs.cowagent.ai/channels/weixin).
|
||||
|
||||
Related commits: [ce89869](https://github.com/zhayujie/chatgpt-on-wechat/commit/ce89869), [a483ec0](https://github.com/zhayujie/chatgpt-on-wechat/commit/a483ec0), [c1421e0](https://github.com/zhayujie/chatgpt-on-wechat/commit/c1421e0)
|
||||
|
||||
## 🤖 New Models
|
||||
|
||||
- **MiniMax-M2.7**: Added MiniMax-M2.7 model support
|
||||
- **GLM-5-Turbo**: Added Zhipu glm-5-turbo model support
|
||||
|
||||
Related commits: [9192f6f](https://github.com/zhayujie/chatgpt-on-wechat/commit/9192f6f)
|
||||
|
||||
## 🔧 Script Refactoring
|
||||
|
||||
- **run.sh Refactoring**: Extracted shared logic and eliminated duplication, reducing from 600+ lines to 177 lines ([49d8707](https://github.com/zhayujie/chatgpt-on-wechat/commit/49d8707))
|
||||
- **Executable Permission**: Fixed `run.sh` file permission issue ([652156e](https://github.com/zhayujie/chatgpt-on-wechat/commit/652156e))
|
||||
|
||||
## ⚡ Improvements
|
||||
|
||||
- **Unified Request Headers**: Added identification headers to external requests across Agent services (Chat, Embedding, Vision, WebSearch, etc.) ([b4e711f](https://github.com/zhayujie/chatgpt-on-wechat/commit/b4e711f))
|
||||
- **Auto-Repair Messages**: Enhanced message protocol fault tolerance with automatic repair of malformed message sequences ([b8b57e3](https://github.com/zhayujie/chatgpt-on-wechat/commit/b8b57e3))
|
||||
|
||||
## 🌍 Japanese Documentation
|
||||
|
||||
Added complete Japanese documentation covering getting started guide, channel integration, model configuration and other major sections. Thanks [@Ikko Ashimine](https://github.com/ikoamu)
|
||||
|
||||
Related commits: [5487c0b](https://github.com/zhayujie/chatgpt-on-wechat/commit/5487c0b)
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
- **WeCom Bot Compatibility**: Fixed compatibility with older `websocket-client` versions, added unified WebSocket compatibility layer ([bc7f627](https://github.com/zhayujie/chatgpt-on-wechat/commit/bc7f627))
|
||||
- **run.sh PID**: Fixed process PID retrieval error in `run.sh` ([9febb07](https://github.com/zhayujie/chatgpt-on-wechat/commit/9febb07))
|
||||
- **Feishu Encoding**: Fixed message and log encoding issue in Feishu channel ([7d0e156](https://github.com/zhayujie/chatgpt-on-wechat/commit/7d0e156))
|
||||
- **Feishu Config**: Removed redundant `feishu_bot_name` dependency in `run.sh` ([1b5be1b](https://github.com/zhayujie/chatgpt-on-wechat/commit/1b5be1b))
|
||||
|
||||
## 📦 Upgrade
|
||||
|
||||
Run `./run.sh update` for a one-click upgrade, or manually pull the latest code and restart. See [Upgrade Guide](https://docs.cowagent.ai/guide/upgrade) for details.
|
||||
|
||||
**Release Date**: 2026.03.22 | [Full Changelog](https://github.com/zhayujie/chatgpt-on-wechat/compare/2.0.3...master)
|
||||
@@ -7,7 +7,7 @@ description: CowAgent - 基于大模型的超级AI助理
|
||||
|
||||
**CowAgent** 是基于大模型的超级AI助理,能够主动思考和任务规划、操作计算机和外部资源、创造和执行Skills、拥有长期记忆并不断成长。
|
||||
|
||||
CowAgent 支持灵活切换多种模型,能处理文本、语音、图片、文件等多模态消息,可接入网页、飞书、钉钉、企业微信应用、微信公众号中使用,7×24小时运行于你的个人电脑或服务器中。
|
||||
CowAgent 支持灵活切换多种模型,能处理文本、语音、图片、文件等多模态消息,可接入微信、飞书、钉钉、企业微信应用、微信公众号、网页中使用,7×24小时运行于你的个人电脑或服务器中。
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="GitHub" icon="github" href="https://github.com/zhayujie/chatgpt-on-wechat">
|
||||
@@ -36,8 +36,8 @@ CowAgent 支持灵活切换多种模型,能处理文本、语音、图片、
|
||||
<Card title="多模型接入" icon="microchip" href="/models/index">
|
||||
支持 OpenAI, Claude, Gemini, DeepSeek, MiniMax, GLM, Qwen, Kimi, Doubao 等国内外主流模型厂商。
|
||||
</Card>
|
||||
<Card title="多端部署" icon="server" href="/channels/web">
|
||||
支持运行在本地计算机或服务器,可集成到网页、飞书、钉钉、微信公众号、企业微信应用中使用。
|
||||
<Card title="多端部署" icon="server" href="/channels/weixin">
|
||||
支持运行在本地计算机或服务器,可集成到微信、网页、飞书、钉钉、微信公众号、企业微信应用中使用。
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -49,7 +49,7 @@ CowAgent 支持灵活切换多种模型,能处理文本、语音、图片、
|
||||
bash <(curl -fsSL https://cdn.link-ai.tech/code/cow/run.sh)
|
||||
```
|
||||
|
||||
运行后默认会启动 Web 服务,通过访问 `http://localhost:9899/chat` 在网页端对话。
|
||||
运行后默认会启动 Web 控制台,通过访问 `http://localhost:9899` 可以在网页端进行对话、配置、应用通道接入等操作。
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="快速开始" icon="rocket" href="/guide/quick-start">
|
||||
|
||||
206
docs/ja/README.md
Normal file
206
docs/ja/README.md
Normal file
@@ -0,0 +1,206 @@
|
||||
<p align="center"><img src="https://github.com/user-attachments/assets/eca9a9ec-8534-4615-9e0f-96c5ac1d10a3" alt="CowAgent" width="550" /></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/zhayujie/chatgpt-on-wechat/releases/latest"><img src="https://img.shields.io/github/v/release/zhayujie/chatgpt-on-wechat" alt="Latest release"></a>
|
||||
<a href="https://github.com/zhayujie/chatgpt-on-wechat/blob/master/LICENSE"><img src="https://img.shields.io/github/license/zhayujie/chatgpt-on-wechat" alt="License: MIT"></a>
|
||||
<a href="https://github.com/zhayujie/chatgpt-on-wechat"><img src="https://img.shields.io/github/stars/zhayujie/chatgpt-on-wechat?style=flat-square" alt="Stars"></a> <br/>
|
||||
[<a href="https://github.com/zhayujie/chatgpt-on-wechat/blob/master/README.md">中文</a>] | [<a href="https://github.com/zhayujie/chatgpt-on-wechat/blob/master/docs/en/README.md">English</a>] | [日本語]
|
||||
</p>
|
||||
|
||||
**CowAgent** はLLMを搭載したAIスーパーアシスタントです。自律的なタスク計画、コンピュータや外部リソースの操作、Skillの作成・実行、長期記憶による継続的な成長が可能です。柔軟なモデル切り替えに対応し、テキスト・音声・画像・ファイルを処理でき、WeChat、Web、Feishu(飛書)、DingTalk(釘釘)、WeCom Bot(企業微信ボット)、WeComアプリ、WeChat公式アカウントに統合可能で、個人のPCやサーバー上で24時間365日稼働できます。
|
||||
|
||||
<p align="center">
|
||||
<a href="https://cowagent.ai/">🌐 ウェブサイト</a> ·
|
||||
<a href="https://docs.cowagent.ai/en/intro/index">📖 ドキュメント</a> ·
|
||||
<a href="https://docs.cowagent.ai/en/guide/quick-start">🚀 クイックスタート</a> ·
|
||||
<a href="https://link-ai.tech/cowagent/create">☁️ オンラインで試す</a>
|
||||
</p>
|
||||
|
||||
## はじめに
|
||||
|
||||
> CowAgentは、すぐに使えるAIスーパーアシスタントであると同時に、高い拡張性を持つAgentフレームワークでもあります。新しいモデルインターフェース、チャネル、組み込みツール、Skillシステムを拡張することで、さまざまなカスタマイズニーズに柔軟に対応できます。
|
||||
|
||||
- ✅ **自律的タスク計画**: 複雑なタスクを理解し、自律的に実行計画を立て、目標達成までツールを呼び出しながら継続的に思考します。ツールを通じてファイル、ターミナル、ブラウザ、スケジューラなどのシステムリソースにアクセスできます。
|
||||
- ✅ **長期記憶**: 会話の記憶をローカルファイルやデータベースに自動的に永続化します。コアメモリとデイリーメモリを含み、キーワード検索やベクトル検索に対応しています。
|
||||
- ✅ **Skillシステム**: Skillの作成・実行エンジンを実装しており、複数の組み込みSkillを備え、自然言語での会話を通じたカスタムSkillの開発もサポートしています。
|
||||
- ✅ **マルチモーダルメッセージ**: テキスト、画像、音声、ファイルなど、さまざまなメッセージタイプの解析・処理・生成・送信に対応しています。
|
||||
- ✅ **複数モデル対応**: OpenAI、Claude、Gemini、DeepSeek、MiniMax、GLM、Qwen、Kimi、Doubaoなど、主要なモデルプロバイダーに対応しています。
|
||||
- ✅ **マルチプラットフォームデプロイ**: ローカルPCやサーバー上で実行でき、WeChat、Web、Feishu、DingTalk、WeChat公式アカウント、WeComアプリケーションに統合可能です。
|
||||
- ✅ **ナレッジベース**: [LinkAI](https://link-ai.tech) プラットフォームを通じて、企業向けナレッジベース機能を統合できます。
|
||||
|
||||
## 免責事項
|
||||
|
||||
1. 本プロジェクトは [MIT License](/LICENSE) に基づいており、技術研究・学習を目的としています。利用者は現地の法律、規制、ポリシー、企業の社則を遵守する必要があります。違法行為や権利侵害となる利用は禁止されています。
|
||||
2. Agentモードは通常のチャットモードよりも多くのトークンを消費します。効果とコストに基づいてモデルを選択してください。AgentはホストOSにアクセスできるため、信頼できる環境にデプロイしてください。
|
||||
3. CowAgentはオープンソース開発に注力しており、いかなる暗号通貨の発行・参加・承認も行っていません。
|
||||
|
||||
## デモ
|
||||
|
||||
オンラインで試す(デプロイ不要): [CowAgent](https://link-ai.tech/cowagent/create)
|
||||
|
||||
## 更新履歴
|
||||
|
||||
> **2026.02.27:** [v2.0.2](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/2.0.2) — Webコンソールの全面刷新(ストリーミングチャット、モデル/Skill/メモリ/チャネル/スケジューラ/ログ管理)、マルチチャネル同時実行、セッション永続化、Gemini 3.1 Pro / Claude 4.6 Sonnet / Qwen3.5 Plusなど新モデル追加。
|
||||
|
||||
> **2026.02.13:** [v2.0.1](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/2.0.1) — 組み込みWeb検索ツール、スマートコンテキストトリミング、ランタイム情報の動的更新、Windows互換性、スケジューラのメモリ喪失やFeishu接続問題などの修正。
|
||||
|
||||
> **2026.02.03:** [v2.0.0](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/2.0.0) — マルチステップタスク計画、長期記憶、組み込みツール、Skillフレームワーク、新モデル、チャネル最適化を備えたAIスーパーアシスタントへの全面アップグレード。
|
||||
|
||||
> **2025.05.23:** [v1.7.6](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/1.7.6) — Webチャネル最適化、AgentMeshマルチエージェントプラグイン、Baidu TTS、claude-4-sonnet/opus対応。
|
||||
|
||||
> **2025.04.11:** [v1.7.5](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/1.7.5) — wechatferryプロトコル、DeepSeekモデル、Tencent Cloud音声、ModelScope・Gitee-AI対応。
|
||||
|
||||
> **2024.12.13:** [v1.7.4](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/1.7.4) — Gemini 2.0モデル、Webチャネル、メモリリーク修正。
|
||||
|
||||
全更新履歴: [リリースノート](https://docs.cowagent.ai/en/releases/overview)
|
||||
|
||||
<br/>
|
||||
|
||||
## 🚀 クイックスタート
|
||||
|
||||
本プロジェクトは、インストール・設定・起動・管理をワンクリックで行えるスクリプトを提供しています:
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://cdn.link-ai.tech/code/cow/run.sh)
|
||||
```
|
||||
|
||||
実行後、デフォルトでWebサービスが起動します。`http://localhost:9899/chat` にアクセスしてチャットを開始できます。
|
||||
|
||||
スクリプトの使い方: [ワンクリックインストール](https://docs.cowagent.ai/en/guide/quick-start)
|
||||
|
||||
### 手動インストール
|
||||
|
||||
**1. プロジェクトのクローン**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/zhayujie/chatgpt-on-wechat
|
||||
cd chatgpt-on-wechat/
|
||||
```
|
||||
|
||||
**2. 依存関係のインストール**
|
||||
|
||||
```bash
|
||||
pip3 install -r requirements.txt
|
||||
pip3 install -r requirements-optional.txt # 任意ですが推奨
|
||||
```
|
||||
|
||||
**3. 設定**
|
||||
|
||||
```bash
|
||||
cp config-template.json config.json
|
||||
```
|
||||
|
||||
`config.json` にモデルのAPIキーとチャネルタイプを記入してください。詳細は[設定ドキュメント](https://docs.cowagent.ai/en/guide/manual-install)を参照してください。
|
||||
|
||||
**4. 実行**
|
||||
|
||||
```bash
|
||||
python3 app.py
|
||||
```
|
||||
|
||||
サーバーでバックグラウンド実行する場合:
|
||||
|
||||
```bash
|
||||
nohup python3 app.py & tail -f nohup.out
|
||||
```
|
||||
|
||||
### Dockerデプロイ
|
||||
|
||||
```bash
|
||||
curl -O https://cdn.link-ai.tech/code/cow/docker-compose.yml
|
||||
# docker-compose.yml を編集して設定を記入
|
||||
sudo docker compose up -d
|
||||
sudo docker logs -f chatgpt-on-wechat
|
||||
```
|
||||
|
||||
<br/>
|
||||
|
||||
## モデル
|
||||
|
||||
主要なモデルプロバイダーに対応しています。Agentモードの推奨モデル:
|
||||
|
||||
| プロバイダー | 推奨モデル |
|
||||
| --- | --- |
|
||||
| MiniMax | `MiniMax-M2.7` |
|
||||
| GLM | `glm-5-turbo` |
|
||||
| Kimi | `kimi-k2.5` |
|
||||
| Doubao | `doubao-seed-2-0-code-preview-260215` |
|
||||
| Qwen | `qwen3.5-plus` |
|
||||
| Claude | `claude-sonnet-4-6` |
|
||||
| Gemini | `gemini-3.1-pro-preview` |
|
||||
| OpenAI | `gpt-5.4` |
|
||||
| DeepSeek | `deepseek-chat` |
|
||||
|
||||
各モデルの詳細設定については、[モデルドキュメント](https://docs.cowagent.ai/en/models/index)を参照してください。
|
||||
|
||||
### Coding Plan
|
||||
|
||||
Coding Planは各プロバイダーが提供する月額サブスクリプションパッケージで、高頻度のAgent利用に最適です。すべてのプロバイダーはOpenAI互換モードでアクセスできます:
|
||||
|
||||
```json
|
||||
{
|
||||
"bot_type": "openai",
|
||||
"model": "MODEL_NAME",
|
||||
"open_ai_api_base": "PROVIDER_CODING_PLAN_API_BASE",
|
||||
"open_ai_api_key": "YOUR_API_KEY"
|
||||
}
|
||||
```
|
||||
|
||||
- `bot_type`: `openai` を指定
|
||||
- `model`: プロバイダーがサポートするモデル名
|
||||
- `open_ai_api_base`: プロバイダーのCoding Plan API Base(標準の従量課金とは異なります)
|
||||
- `open_ai_api_key`: プロバイダーのCoding Plan APIキー
|
||||
|
||||
> 注意:Coding PlanのAPI BaseとAPIキーは、通常の従量課金のものとは別です。各プロバイダーのプラットフォームから取得してください。
|
||||
|
||||
対応プロバイダーには、Alibaba Cloud、MiniMax、Zhipu GLM、Kimi、Volcengineなどがあります。各プロバイダーの詳細設定については、[Coding Planドキュメント](https://docs.cowagent.ai/en/models/coding-plan)を参照してください。
|
||||
|
||||
<br/>
|
||||
|
||||
## チャネル
|
||||
|
||||
複数のプラットフォームに対応しています。`config.json` の `channel_type` を設定して切り替えます:
|
||||
|
||||
| チャネル | `channel_type` | ドキュメント |
|
||||
| --- | --- | --- |
|
||||
| WeChat | `weixin` | [WeChat設定](https://docs.cowagent.ai/ja/channels/weixin) |
|
||||
| Web(デフォルト) | `web` | [Webチャネル](https://docs.cowagent.ai/en/channels/web) |
|
||||
| Feishu(飛書) | `feishu` | [Feishu設定](https://docs.cowagent.ai/en/channels/feishu) |
|
||||
| DingTalk(釘釘) | `dingtalk` | [DingTalk設定](https://docs.cowagent.ai/en/channels/dingtalk) |
|
||||
| WeCom Bot | `wecom_bot` | [WeCom Bot設定](https://docs.cowagent.ai/en/channels/wecom-bot) |
|
||||
| WeComアプリ | `wechatcom_app` | [WeCom設定](https://docs.cowagent.ai/en/channels/wecom) |
|
||||
| WeChat公式アカウント | `wechatmp` / `wechatmp_service` | [WeChat公式アカウント設定](https://docs.cowagent.ai/en/channels/wechatmp) |
|
||||
| ターミナル | `terminal` | — |
|
||||
|
||||
複数チャネルを同時に有効化できます。カンマ区切りで指定してください:`"channel_type": "feishu,dingtalk"`
|
||||
|
||||
<br/>
|
||||
|
||||
## エンタープライズサービス
|
||||
|
||||
<a href="https://link-ai.tech" target="_blank"><img width="720" src="https://cdn.link-ai.tech/image/link-ai-intro.jpg"></a>
|
||||
|
||||
> [LinkAI](https://link-ai.tech/) は、企業や開発者向けのワンストップAIエージェントプラットフォームです。マルチモーダルLLM、ナレッジベース、Agentプラグイン、ワークフローを統合しています。主要プラットフォームへのワンクリック統合、SaaSおよびプライベートデプロイに対応しています。
|
||||
|
||||
<br/>
|
||||
|
||||
## 🔗 関連プロジェクト
|
||||
|
||||
- [bot-on-anything](https://github.com/zhayujie/bot-on-anything): 軽量で高い拡張性を持つLLMアプリケーションフレームワーク。Slack、Telegram、Discord、Gmailなどに対応。
|
||||
- [AgentMesh](https://github.com/MinimalFuture/AgentMesh): エージェントチームの協調による複雑な問題解決のためのオープンソースのマルチエージェントフレームワーク。
|
||||
|
||||
## 🔎 よくある質問
|
||||
|
||||
FAQ: <https://github.com/zhayujie/chatgpt-on-wechat/wiki/FAQs>
|
||||
|
||||
## 🛠️ コントリビューション
|
||||
|
||||
新しいチャネルの追加を歓迎します。[Feishuチャネル](https://github.com/zhayujie/chatgpt-on-wechat/blob/master/channel/feishu/feishu_channel.py)を参考にしてください。また、新しいSkillのコントリビューションも歓迎します。[Skill Creatorドキュメント](https://github.com/zhayujie/chatgpt-on-wechat/blob/master/skills/skill-creator/SKILL.md)を参照してください。
|
||||
|
||||
## ✉ お問い合わせ
|
||||
|
||||
PRやIssueの提出を歓迎します。🌟 Starでプロジェクトをサポートしてください。ご質問がある場合は、[FAQリスト](https://github.com/zhayujie/chatgpt-on-wechat/wiki/FAQs)を確認するか、[Issues](https://github.com/zhayujie/chatgpt-on-wechat/issues)を検索してください。
|
||||
|
||||
## 🌟 コントリビューター
|
||||
|
||||

|
||||
58
docs/ja/channels/dingtalk.mdx
Normal file
58
docs/ja/channels/dingtalk.mdx
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
title: DingTalk
|
||||
description: CowAgent を DingTalk アプリケーションに統合する
|
||||
---
|
||||
|
||||
DingTalk オープンプラットフォームでインテリジェントロボットアプリを作成して、CowAgent を DingTalk に統合します。
|
||||
|
||||
## 1. アプリの作成
|
||||
|
||||
1. [DingTalk 開発者コンソール](https://open-dev.dingtalk.com/fe/app#/corp/app)にアクセスし、ログインして**アプリを作成**をクリックし、アプリ情報を入力します:
|
||||
|
||||
<img src="https://img-1317903499.cos.ap-guangzhou.myqcloud.com/docs/dingtalk-create-app.png" width="800"/>
|
||||
|
||||
2. **アプリ機能の追加**をクリックし、**ロボット**機能を選択して**追加**をクリックします:
|
||||
|
||||
<img src="https://img-1317903499.cos.ap-guangzhou.myqcloud.com/docs/dingtalk-add-bot.png" width="800"/>
|
||||
|
||||
3. ロボット情報を設定し、**公開**をクリックします。公開後、「**デバッグ**」をクリックすると自動的にテストグループチャットが作成され、クライアントで確認できます:
|
||||
|
||||
<img src="https://img-1317903499.cos.ap-guangzhou.myqcloud.com/docs/dingtalk-config-bot.png" width="600"/>
|
||||
|
||||
4. **バージョン管理とリリース**をクリックし、新しいバージョンを作成して公開します:
|
||||
|
||||
<img src="https://img-1317903499.cos.ap-guangzhou.myqcloud.com/docs/dingtalk-publish-bot.png" width="700"/>
|
||||
|
||||
## 2. プロジェクト設定
|
||||
|
||||
1. **認証情報と基本情報**をクリックし、`Client ID` と `Client Secret` を取得します:
|
||||
|
||||
<img src="https://img-1317903499.cos.ap-guangzhou.myqcloud.com/docs/dingtalk-get-secret.png" width="700"/>
|
||||
|
||||
2. プロジェクトルートの `config.json` に以下の設定を追加します:
|
||||
|
||||
```json
|
||||
{
|
||||
"channel_type": "dingtalk",
|
||||
"dingtalk_client_id": "YOUR_CLIENT_ID",
|
||||
"dingtalk_client_secret": "YOUR_CLIENT_SECRET"
|
||||
}
|
||||
```
|
||||
|
||||
3. 依存パッケージをインストールします:
|
||||
|
||||
```bash
|
||||
pip3 install dingtalk_stream
|
||||
```
|
||||
|
||||
<img src="https://img-1317903499.cos.ap-guangzhou.myqcloud.com/docs/dingtalk-app-config.png" width="700"/>
|
||||
|
||||
4. プロジェクト起動後、DingTalk 開発者コンソールに移動し、**イベントサブスクリプション**をクリックし、**接続確認済み、チャネルを確認**をクリックします。「**接続成功**」と表示されれば設定完了です:
|
||||
|
||||
<img src="https://img-1317903499.cos.ap-guangzhou.myqcloud.com/docs/dingtalk-event-sub.png" width="700"/>
|
||||
|
||||
## 3. 使い方
|
||||
|
||||
ロボットと個別チャットするか、企業グループに追加して会話を開始します:
|
||||
|
||||
<img src="https://img-1317903499.cos.ap-guangzhou.myqcloud.com/docs/dingtalk-hosting-demo.png" width="650"/>
|
||||
69
docs/ja/channels/feishu.mdx
Normal file
69
docs/ja/channels/feishu.mdx
Normal file
@@ -0,0 +1,69 @@
|
||||
---
|
||||
title: Feishu (Lark)
|
||||
description: CowAgent を Feishu アプリケーションに統合する
|
||||
---
|
||||
|
||||
企業向けカスタムアプリを作成して、CowAgent を Feishu に統合します。管理者権限を持つ Feishu 企業ユーザーである必要があります。
|
||||
|
||||
## 1. 企業カスタムアプリの作成
|
||||
|
||||
### 1.1 アプリの作成
|
||||
|
||||
[Feishu 開発者プラットフォーム](https://open.feishu.cn/app/)にアクセスし、**企業カスタムアプリを作成**をクリックして、必要な情報を入力し**作成**をクリックします:
|
||||
|
||||
<img src="https://img-1317903499.cos.ap-guangzhou.myqcloud.com/docs/feishu-hosting-create-app.jpg" width="500"/>
|
||||
|
||||
### 1.2 Bot 機能の追加
|
||||
|
||||
**アプリ機能の追加**で、アプリに **Bot** 機能を追加します:
|
||||
|
||||
<img src="https://img-1317903499.cos.ap-guangzhou.myqcloud.com/docs/feishu-hosting-add-bot.jpg" width="800"/>
|
||||
|
||||
### 1.3 アプリ権限の設定
|
||||
|
||||
**権限管理**をクリックし、**権限設定**の下の入力欄に以下の権限文字列を貼り付け、フィルタされたすべての権限を選択し、**一括有効化**をクリックして確認します:
|
||||
|
||||
```
|
||||
im:message,im:message.group_at_msg,im:message.group_at_msg:readonly,im:message.p2p_msg,im:message.p2p_msg:readonly,im:message:send_as_bot,im:resource
|
||||
```
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/feishu-hosting-add-auth2.png" width="800"/>
|
||||
|
||||
## 2. プロジェクト設定
|
||||
|
||||
1. **認証情報と基本情報**から `App ID` と `App Secret` を取得します:
|
||||
|
||||
<img src="https://img-1317903499.cos.ap-guangzhou.myqcloud.com/docs/feishu-hosting-appid-secret.jpg" width="800"/>
|
||||
|
||||
2. プロジェクトルートの `config.json` に以下の設定を追加します:
|
||||
|
||||
```json
|
||||
{
|
||||
"channel_type": "feishu",
|
||||
"feishu_app_id": "YOUR_APP_ID",
|
||||
"feishu_app_secret": "YOUR_APP_SECRET",
|
||||
"feishu_bot_name": "YOUR_BOT_NAME"
|
||||
}
|
||||
```
|
||||
|
||||
| パラメータ | 説明 |
|
||||
| --- | --- |
|
||||
| `feishu_app_id` | Feishu Bot の App ID |
|
||||
| `feishu_app_secret` | Feishu Bot の App Secret |
|
||||
| `feishu_bot_name` | Bot 名(アプリ作成時に設定)、グループチャットで使用する際に必要 |
|
||||
|
||||
設定完了後、プロジェクトを起動します。
|
||||
|
||||
## 3. イベントサブスクリプションの設定
|
||||
|
||||
1. プロジェクトが正常に動作した後、Feishu 開発者プラットフォームに移動し、**イベントとコールバック**をクリックし、**ロングコネクション**モードを選択して保存をクリックします:
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/202601311731183.png" width="600"/>
|
||||
|
||||
2. 下の**イベントを追加**をクリックし、「メッセージ受信」を検索して「**メッセージ受信 v2.0**」を選択し、確認します。
|
||||
|
||||
3. **バージョン管理とリリース**をクリックし、新しいバージョンを作成して**本番リリース**を申請します。Feishu クライアントで承認メッセージを確認し、承認します:
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/202601311807356.png" width="600"/>
|
||||
|
||||
完了後、Feishu で Bot 名を検索してチャットを開始できます。
|
||||
88
docs/ja/channels/qq.mdx
Normal file
88
docs/ja/channels/qq.mdx
Normal file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
title: QQ Bot
|
||||
description: CowAgent を QQ Bot に接続する(WebSocket ロングコネクション)
|
||||
---
|
||||
|
||||
> QQ オープンプラットフォームの Bot API を介して CowAgent を接続し、QQ のダイレクトメッセージ、グループチャット(@bot)、ギルドチャネルメッセージ、ギルド DM に対応します。パブリック IP は不要で、WebSocket ロングコネクションを使用します。
|
||||
|
||||
<Note>
|
||||
QQ Bot は QQ オープンプラットフォームを通じて作成します。WebSocket ロングコネクションでメッセージを受信し、OpenAPI でメッセージを送信します。パブリック IP やドメインは不要です。
|
||||
</Note>
|
||||
|
||||
## 1. QQ Bot の作成
|
||||
|
||||
> [QQ オープンプラットフォーム](https://q.qq.com)にアクセスし、QQ でサインインします。未登録の場合は、先に[アカウント登録](https://q.qq.com/#/register)を完了してください。
|
||||
|
||||
1.[QQ オープンプラットフォーム - Bot 一覧](https://q.qq.com/#/apps)に移動し、**Bot を作成**をクリックします:
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/20260317162900.png" width="800"/>
|
||||
|
||||
2.Bot 名、アバター、その他の基本情報を入力して作成を完了します:
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/20260317163005.png" width="800"/>
|
||||
|
||||
3.Bot 設定ページに入り、**開発管理**に移動して以下の手順を完了します:
|
||||
|
||||
- **AppID**(Bot ID)をコピーして保存します
|
||||
- **AppSecret**(Bot Secret)を生成して保存します
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/20260317164955.png" width="800"/>
|
||||
|
||||
## 2. 設定と起動
|
||||
|
||||
### 方法 A: Web コンソール
|
||||
|
||||
プログラムを起動し、Web コンソール(ローカルアクセス: http://127.0.0.1:9899/)を開きます。**チャネル**タブに移動し、**チャネルを接続**をクリックして **QQ Bot** を選択し、前のステップで取得した AppID と AppSecret を入力して接続をクリックします。
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/20260317165425.png" width="800"/>
|
||||
|
||||
### 方法 B: 設定ファイル
|
||||
|
||||
`config.json` に以下を追加します:
|
||||
|
||||
```json
|
||||
{
|
||||
"channel_type": "qq",
|
||||
"qq_app_id": "YOUR_APP_ID",
|
||||
"qq_app_secret": "YOUR_APP_SECRET"
|
||||
}
|
||||
```
|
||||
|
||||
| パラメータ | 説明 |
|
||||
| --- | --- |
|
||||
| `qq_app_id` | QQ Bot の AppID。オープンプラットフォームの開発管理で確認できます |
|
||||
| `qq_app_secret` | QQ Bot の AppSecret。オープンプラットフォームの開発管理で確認できます |
|
||||
|
||||
設定後、プログラムを起動します。ログに `[QQ] ✅ Connected successfully` と表示されれば接続成功です。
|
||||
|
||||
|
||||
## 3. 使い方
|
||||
|
||||
QQ オープンプラットフォームで、**管理 → 利用範囲とメンバー**に移動し、「グループとメッセージリストに追加」の QR コードを QQ クライアントでスキャンして Bot とのチャットを開始します:
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/20260317165947.png" width="800"/>
|
||||
|
||||
チャット例:
|
||||
<img src="https://cdn.link-ai.tech/doc/20260317171508.png" width="800"/>
|
||||
|
||||
## 4. 対応機能
|
||||
|
||||
> 注意: グループチャットやギルドチャネルで QQ Bot を使用するには、公開審査を完了し、利用範囲の権限を設定する必要があります。
|
||||
|
||||
| 機能 | 状態 |
|
||||
| --- | --- |
|
||||
| QQ ダイレクトメッセージ | ✅ |
|
||||
| QQ グループチャット(@bot) | ✅ |
|
||||
| ギルドチャネル(@bot) | ✅ |
|
||||
| ギルド DM | ✅ |
|
||||
| テキストメッセージ | ✅ 送受信 |
|
||||
| 画像メッセージ | ✅ 送受信(グループ・ダイレクト) |
|
||||
| ファイルメッセージ | ✅ 送信(グループ・ダイレクト) |
|
||||
| スケジュールタスク | ✅ 能動的プッシュ(ユーザーあたり月4回) |
|
||||
|
||||
|
||||
## 5. 注意事項
|
||||
|
||||
- **受動メッセージの制限**: QQ ダイレクトメッセージの返信は60分間有効です(1メッセージあたり最大5回返信可能)。グループチャットの返信は5分間有効です。
|
||||
- **能動メッセージの制限**: ダイレクトメッセージとグループチャットの両方で、月あたりの能動メッセージは4件までです。スケジュールタスク機能を使用する際はこの点にご注意ください。
|
||||
- **イベント権限**: デフォルトでは `GROUP_AND_C2C_EVENT`(QQ グループ/ダイレクト)と `PUBLIC_GUILD_MESSAGES`(ギルド公開メッセージ)がサブスクライブされています。追加の権限が必要な場合は、オープンプラットフォームで申請してください。
|
||||
75
docs/ja/channels/web.mdx
Normal file
75
docs/ja/channels/web.mdx
Normal file
@@ -0,0 +1,75 @@
|
||||
---
|
||||
title: Web コンソール
|
||||
description: Web コンソールで CowAgent を使用する
|
||||
---
|
||||
|
||||
Web コンソールは CowAgent のデフォルトチャネルです。起動後に自動的に開始され、ブラウザを通じて Agent とチャットしたり、モデル、Skill、メモリ、チャネルなどの設定をオンラインで管理できます。
|
||||
|
||||
## 設定
|
||||
|
||||
```json
|
||||
{
|
||||
"channel_type": "web",
|
||||
"web_port": 9899
|
||||
}
|
||||
```
|
||||
|
||||
| パラメータ | 説明 | デフォルト値 |
|
||||
| --- | --- | --- |
|
||||
| `channel_type` | `web` に設定 | `web` |
|
||||
| `web_port` | Web サービスのリスンポート | `9899` |
|
||||
|
||||
## アクセス URL
|
||||
|
||||
プロジェクト起動後、以下にアクセスしてください:
|
||||
|
||||
- ローカル: `http://localhost:9899`
|
||||
- サーバー: `http://<server-ip>:9899`
|
||||
|
||||
<Note>
|
||||
サーバーのファイアウォールとセキュリティグループで該当ポートが許可されていることを確認してください。
|
||||
</Note>
|
||||
|
||||
## 機能
|
||||
|
||||
### チャット画面
|
||||
|
||||
ストリーミング出力に対応しており、Agent の推論プロセスやツール呼び出しをリアルタイムで表示し、Agent の意思決定を直感的に観察できます:
|
||||
|
||||
<img width="850" src="https://cdn.link-ai.tech/doc/20260227180120.png" />
|
||||
|
||||
### モデル管理
|
||||
|
||||
設定ファイルを手動で編集せずに、オンラインでモデル設定を管理できます:
|
||||
|
||||
<img width="850" src="https://cdn.link-ai.tech/doc/20260227173811.png" />
|
||||
|
||||
### Skill 管理
|
||||
|
||||
Agent の Skill をオンラインで閲覧・管理できます:
|
||||
|
||||
<img width="850" src="https://cdn.link-ai.tech/doc/20260227173403.png" />
|
||||
|
||||
### メモリ管理
|
||||
|
||||
Agent のメモリをオンラインで閲覧・管理できます:
|
||||
|
||||
<img width="850" src="https://cdn.link-ai.tech/doc/20260227173349.png" />
|
||||
|
||||
### チャネル管理
|
||||
|
||||
接続中のチャネルをオンラインで管理し、リアルタイムで接続・切断操作を行えます:
|
||||
|
||||
<img width="850" src="https://cdn.link-ai.tech/doc/20260227173331.png" />
|
||||
|
||||
### スケジュールタスク
|
||||
|
||||
スケジュールタスクをオンラインで閲覧・管理できます。一回限りのタスク、固定間隔、Cron 式に対応しています:
|
||||
|
||||
<img width="850" src="https://cdn.link-ai.tech/doc/20260227173704.png" />
|
||||
|
||||
### ログ
|
||||
|
||||
Agent のランタイムログをリアルタイムで確認でき、監視やトラブルシューティングに活用できます:
|
||||
|
||||
<img width="850" src="https://cdn.link-ai.tech/doc/20260227173514.png" />
|
||||
72
docs/ja/channels/wechatmp.mdx
Normal file
72
docs/ja/channels/wechatmp.mdx
Normal file
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: WeChat 公式アカウント
|
||||
description: CowAgent を WeChat 公式アカウントに統合する
|
||||
---
|
||||
|
||||
CowAgent は個人サブスクリプションアカウントと企業サービスアカウントの両方に対応しています。
|
||||
|
||||
| 種類 | 要件 | 特徴 |
|
||||
| --- | --- | --- |
|
||||
| **個人サブスクリプション** | 個人で利用可能 | まずプレースホルダーの返信を送信し、ユーザーが完全な応答を取得するにはメッセージを送信する必要があります |
|
||||
| **企業サービス** | カスタマーサービス API が認証済みの企業 | ユーザーに能動的に返信をプッシュできます |
|
||||
|
||||
<Note>
|
||||
公式アカウントはサーバーおよび Docker デプロイのみサポートしており、ローカル実行モードには対応していません。拡張依存パッケージをインストールしてください: `pip3 install -r requirements-optional.txt`
|
||||
</Note>
|
||||
|
||||
## 1. 個人サブスクリプションアカウント
|
||||
|
||||
`config.json` に以下の設定を追加します:
|
||||
|
||||
```json
|
||||
{
|
||||
"channel_type": "wechatmp",
|
||||
"single_chat_prefix": [""],
|
||||
"wechatmp_app_id": "wx73f9******d1e48",
|
||||
"wechatmp_app_secret": "YOUR_APP_SECRET",
|
||||
"wechatmp_aes_key": "",
|
||||
"wechatmp_token": "YOUR_TOKEN",
|
||||
"wechatmp_port": 80
|
||||
}
|
||||
```
|
||||
|
||||
### セットアップ手順
|
||||
|
||||
これらの設定は [WeChat 公式アカウントプラットフォーム](https://mp.weixin.qq.com/advanced/advanced?action=dev&t=advanced/dev)と一致している必要があります。**設定と開発 → 基本設定 → サーバー設定**に移動し、以下のように設定します:
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/20260228103506.png" width="480"/>
|
||||
|
||||
1. プラットフォームで開発者シークレットを有効化し(`wechatmp_app_secret` に対応)、サーバー IP をホワイトリストに追加します
|
||||
2. プラットフォームの設定と一致するように公式アカウントのパラメータを `config.json` に入力します
|
||||
3. プログラムを起動します。ポート 80 でリスンします(権限がない場合は `sudo` を使用してください。ポート 80 を占有しているプロセスがあれば停止してください)
|
||||
4. 公式アカウントプラットフォームで**サーバー設定を有効化**して送信します。正常に保存できれば設定完了です。**「サーバー URL」**は `http://{HOST}/wx` の形式で入力する必要があり、`{HOST}` にはサーバー IP またはドメインを指定できます
|
||||
|
||||
アカウントをフォローしてメッセージを送信すると、以下のような結果が表示されるはずです:
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/20260228103522.png" width="720"/>
|
||||
|
||||
サブスクリプションアカウントの制限により、短い返信(15秒以内)は即座に返されますが、長い返信の場合はまず「考え中...」というプレースホルダーが送信され、ユーザーは任意のテキストを送信して回答を取得する必要があります。企業サービスアカウントではカスタマーサービス API を使用してこの問題を解決できます。
|
||||
|
||||
<Tip>
|
||||
**音声認識**: WeChat 内蔵の音声認識を使用できます。公式アカウント管理ページの「設定と開発 → API 権限」で「音声認識結果の受信」を有効にしてください。
|
||||
</Tip>
|
||||
|
||||
## 2. 企業サービスアカウント
|
||||
|
||||
企業サービスアカウントのセットアップ手順は個人サブスクリプションアカウントとほぼ同じですが、以下の点が異なります:
|
||||
|
||||
1. プラットフォームで企業サービスアカウントを登録し、WeChat 認証を完了します。**カスタマーサービス API** の権限が付与されていることを確認してください
|
||||
2. `config.json` で `"channel_type": "wechatmp_service"` に設定します。その他の設定は同じです
|
||||
3. 長い返信であっても、ユーザーに能動的にプッシュでき、手動での取得が不要です
|
||||
|
||||
```json
|
||||
{
|
||||
"channel_type": "wechatmp_service",
|
||||
"single_chat_prefix": [""],
|
||||
"wechatmp_app_id": "YOUR_APP_ID",
|
||||
"wechatmp_app_secret": "YOUR_APP_SECRET",
|
||||
"wechatmp_aes_key": "",
|
||||
"wechatmp_token": "YOUR_TOKEN",
|
||||
"wechatmp_port": 80
|
||||
}
|
||||
```
|
||||
73
docs/ja/channels/wecom-bot.mdx
Normal file
73
docs/ja/channels/wecom-bot.mdx
Normal file
@@ -0,0 +1,73 @@
|
||||
---
|
||||
title: WeCom Bot
|
||||
description: CowAgent を WeCom AI Bot に接続する(WebSocket ロングコネクション)
|
||||
---
|
||||
|
||||
WeCom AI Bot を介して CowAgent を接続し、ダイレクトメッセージとグループチャットの両方に対応します。パブリック IP は不要で、WebSocket ロングコネクションを使用し、Markdown レンダリングとストリーミング出力をサポートします。
|
||||
|
||||
<Note>
|
||||
WeCom Bot と WeCom App は異なる統合方式です。WeCom Bot は WebSocket ロングコネクションを使用するため、パブリック IP やドメインが不要で、セットアップが簡単です。
|
||||
</Note>
|
||||
|
||||
## 1. AI Bot の作成
|
||||
|
||||
1. WeCom クライアントを開き、**ワークベンチ**に移動し、**AI Bot** をクリックします:
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/20260316180959.png" width="800"/>
|
||||
|
||||
2. **Bot を作成** → **手動作成**をクリックします:
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/20260316181118.png" width="600"/>
|
||||
|
||||
3. 右パネルの一番下までスクロールし、**API モード**を選択します:
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/20260316181215.png" width="600"/>
|
||||
|
||||
4. Bot 名、アバター、公開範囲を設定します。**ロングコネクション**モードを選択し、**Bot ID** と **Secret** をメモしてから保存をクリックします。
|
||||
|
||||
## 2. 設定
|
||||
|
||||
### 方法 A: Web コンソール
|
||||
|
||||
プログラムを起動し、Web コンソール(ローカルアクセス: http://127.0.0.1:9899)を開きます。**チャネル**タブに移動し、**チャネルを接続**をクリックして **WeCom Bot** を選択し、前のステップで取得した Bot ID と Secret を入力して接続をクリックします。
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/20260316181711.png" width="600"/>
|
||||
|
||||
### 方法 B: 設定ファイル
|
||||
|
||||
`config.json` に以下を追加します:
|
||||
|
||||
```json
|
||||
{
|
||||
"channel_type": "wecom_bot",
|
||||
"wecom_bot_id": "YOUR_BOT_ID",
|
||||
"wecom_bot_secret": "YOUR_SECRET"
|
||||
}
|
||||
```
|
||||
|
||||
| パラメータ | 説明 |
|
||||
| --- | --- |
|
||||
| `wecom_bot_id` | AI Bot の Bot ID |
|
||||
| `wecom_bot_secret` | AI Bot の Secret |
|
||||
|
||||
設定後、プログラムを起動します。ログに `[WecomBot] Subscribe success` と表示されれば接続成功です。
|
||||
|
||||
## 3. 対応機能
|
||||
|
||||
| 機能 | 状態 |
|
||||
| --- | --- |
|
||||
| ダイレクトメッセージ | ✅ |
|
||||
| グループチャット(@bot) | ✅ |
|
||||
| テキストメッセージ | ✅ 送受信 |
|
||||
| 画像メッセージ | ✅ 送受信 |
|
||||
| ファイルメッセージ | ✅ 送受信 |
|
||||
| ストリーミング返信 | ✅ |
|
||||
| スケジュール配信 | ✅ |
|
||||
|
||||
## 4. 使い方
|
||||
|
||||
WeCom で Bot 名を検索してダイレクトメッセージを開始できます。
|
||||
|
||||
グループチャットで使用するには、Bot をグループに追加し、@メンションしてメッセージを送信します。
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/20260316182902.png" width="800"/>
|
||||
98
docs/ja/channels/wecom.mdx
Normal file
98
docs/ja/channels/wecom.mdx
Normal file
@@ -0,0 +1,98 @@
|
||||
---
|
||||
title: WeCom
|
||||
description: CowAgent を WeCom 企業アプリに統合する
|
||||
---
|
||||
|
||||
カスタム企業アプリを通じて CowAgent を WeCom に統合し、社内従業員との1対1チャットに対応します。
|
||||
|
||||
<Note>
|
||||
WeCom は Docker デプロイまたはサーバー上の Python デプロイのみサポートしています。ローカル実行モードには対応していません。
|
||||
</Note>
|
||||
|
||||
## 1. 前提条件
|
||||
|
||||
必要なリソース:
|
||||
|
||||
1. パブリック IP を持つサーバー(海外サーバー、または国際 API アクセス用のプロキシを持つ国内サーバー)
|
||||
2. 登録済みの WeCom アカウント(個人登録は可能ですが認証はできません)
|
||||
3. 認証済みの WeCom アカウントには、対応する法人名義で届け出済みのドメインが別途必要です
|
||||
|
||||
## 2. WeCom アプリの作成
|
||||
|
||||
1. [WeCom 管理コンソール](https://work.weixin.qq.com/wework_admin/frame#profile)で、**自社情報**をクリックし、ページ下部の **Corp ID** を確認します。この ID を `wechatcom_corp_id` 設定フィールド用に保存します。
|
||||
|
||||
2. **アプリ管理**に切り替え、アプリを作成をクリックします:
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/20260228103156.png" width="480"/>
|
||||
|
||||
3. アプリ作成ページで、`AgentId` と `Secret` を記録します:
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/20260228103218.png" width="580"/>
|
||||
|
||||
4. **API 受信設定**をクリックしてアプリケーションインターフェースを設定します:
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/20260228103211.png" width="520"/>
|
||||
|
||||
- URL の形式: `http://ip:port/wxcomapp`(認証済み企業は届け出済みドメインを使用する必要があります)
|
||||
- ランダムな `Token` と `EncodingAESKey` を生成し、設定ファイル用に保存します
|
||||
|
||||
<Note>
|
||||
プログラムがまだ起動していないため、この時点では API 受信設定を保存できません。プロジェクトが動作した後に戻って保存してください。
|
||||
</Note>
|
||||
|
||||
## 3. 設定と起動
|
||||
|
||||
`config.json` に以下の設定を追加します(各パラメータと WeCom コンソールの対応関係は上のスクリーンショットを参照してください):
|
||||
|
||||
```json
|
||||
{
|
||||
"channel_type": "wechatcom_app",
|
||||
"single_chat_prefix": [""],
|
||||
"wechatcom_corp_id": "YOUR_CORP_ID",
|
||||
"wechatcomapp_token": "YOUR_TOKEN",
|
||||
"wechatcomapp_secret": "YOUR_SECRET",
|
||||
"wechatcomapp_agent_id": "YOUR_AGENT_ID",
|
||||
"wechatcomapp_aes_key": "YOUR_AES_KEY",
|
||||
"wechatcomapp_port": 9898
|
||||
}
|
||||
```
|
||||
|
||||
| パラメータ | 説明 |
|
||||
| --- | --- |
|
||||
| `wechatcom_corp_id` | Corp ID |
|
||||
| `wechatcomapp_token` | API 受信設定の Token |
|
||||
| `wechatcomapp_secret` | アプリの Secret |
|
||||
| `wechatcomapp_agent_id` | アプリの AgentId |
|
||||
| `wechatcomapp_aes_key` | API 受信設定の EncodingAESKey |
|
||||
| `wechatcomapp_port` | リスンポート、デフォルトは 9898 |
|
||||
|
||||
設定後、プログラムを起動します。ログに `http://0.0.0.0:9898/` と表示されれば、プログラムは正常に動作しています。このポートを外部に公開する必要があります(例:クラウドサーバーのセキュリティグループで許可します)。
|
||||
|
||||
プログラム起動後、WeCom 管理コンソールに戻って**メッセージサーバー設定**を保存します。保存が成功したら、サーバー IP を**企業の信頼済み IP** に追加する必要もあります。追加しないとメッセージの送受信ができません:
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/20260228103224.png" width="520"/>
|
||||
|
||||
<Warning>
|
||||
URL 設定のコールバックが失敗する場合や、設定がうまくいかない場合:
|
||||
1. サーバーのファイアウォールが無効になっており、セキュリティグループでリスンポートが許可されていることを確認してください
|
||||
2. Token、Secret Key などのパラメータ設定が一致しているか、URL の形式が正しいか慎重に確認してください
|
||||
3. 認証済みの WeCom アカウントは、法人に対応する届け出済みドメインを設定する必要があります
|
||||
</Warning>
|
||||
|
||||
## 4. 使い方
|
||||
|
||||
WeCom で作成したアプリ名を検索して、直接チャットを開始できます。異なるポートでリスンする複数のインスタンスを実行して、複数の WeCom アプリを作成できます:
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/20260228103228.png" width="720"/>
|
||||
|
||||
外部の個人 WeChat ユーザーにアプリを利用してもらうには、**自社情報 → WeChat プラグイン**に移動し、招待 QR コードを共有します。スキャンしてフォローした後、個人 WeChat ユーザーがアプリとチャットできるようになります:
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/20260228103232.png" width="520"/>
|
||||
|
||||
## FAQ
|
||||
|
||||
以下の依存パッケージがインストールされていることを確認してください:
|
||||
|
||||
```bash
|
||||
pip install websocket-client pycryptodome
|
||||
```
|
||||
72
docs/ja/channels/weixin.mdx
Normal file
72
docs/ja/channels/weixin.mdx
Normal file
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: WeChat
|
||||
description: CowAgent を個人の WeChat に接続する
|
||||
---
|
||||
|
||||
> 個人の WeChat に接続します。QR コードをスキャンするだけでログインでき、パブリック IP は不要です。テキスト、画像、音声、ファイル、動画メッセージの送受信に対応しています。
|
||||
|
||||
## 1. 設定
|
||||
|
||||
### 方法 A: Web コンソール
|
||||
|
||||
プログラムを起動し、Web コンソール(ローカルアクセス: http://127.0.0.1:9899)を開きます。**チャネル**タブに移動し、**チャネルを接続**をクリックして **WeChat** を選択し、プロンプトに従って QR コードをスキャンしてください。
|
||||
|
||||
### 方法 B: 設定ファイル
|
||||
|
||||
`config.json` で `channel_type` を `weixin` に設定します:
|
||||
|
||||
```json
|
||||
{
|
||||
"channel_type": "weixin"
|
||||
}
|
||||
```
|
||||
|
||||
プログラム起動後、ターミナルに QR コードが表示されます。WeChat でスキャンし、スマートフォンで確認してログインを完了してください。
|
||||
|
||||
<Note>
|
||||
後方互換性のため、`channel_type` を `wx` に設定しても WeChat チャネルが有効になります。
|
||||
</Note>
|
||||
|
||||
## 2. パラメータ
|
||||
|
||||
| パラメータ | 説明 | デフォルト |
|
||||
| --- | --- | --- |
|
||||
| `channel_type` | `weixin` または `wx` を指定 | — |
|
||||
|
||||
ログイン認証情報は `~/.weixin_cow_credentials.json` に自動保存されます。再ログインするには、このファイルを削除してプログラムを再起動してください。
|
||||
|
||||
## 3. ログイン
|
||||
|
||||
### QR コードログイン
|
||||
|
||||
初回起動時に、ターミナルに QR コードが表示されます(有効期限は約 2 分)。WeChat でスキャンし、スマートフォンで確認してください。
|
||||
|
||||
- QR コードが期限切れになると自動的に更新・再表示されます
|
||||
- `qrcode` 依存関係は `requirements.txt` にデフォルトで含まれており、ターミナルに直接 QR コードを表示できます
|
||||
|
||||
### 認証情報の永続化
|
||||
|
||||
ログイン成功後、認証情報は `~/.weixin_cow_credentials.json` に保存されます。次回起動時は保存された認証情報が再利用され、再スキャンは不要です。
|
||||
|
||||
再ログインするには、認証情報ファイルを削除してプログラムを再起動してください。
|
||||
|
||||
### セッションの期限切れ
|
||||
|
||||
WeChat セッションが期限切れになった場合(errcode -14)、プログラムは自動的に古い認証情報をクリアし、新しい QR ログインを開始します。手動での操作は不要です。
|
||||
|
||||
## 4. 対応機能
|
||||
|
||||
| 機能 | 状態 |
|
||||
| --- | --- |
|
||||
| ダイレクトメッセージ | ✅ |
|
||||
| テキストメッセージ | ✅ 送受信 |
|
||||
| 画像メッセージ | ✅ 送受信 |
|
||||
| ファイルメッセージ | ✅ 送受信 |
|
||||
| 動画メッセージ | ✅ 送受信 |
|
||||
| 音声メッセージ | ✅ 受信 |
|
||||
|
||||
## 5. 注意事項
|
||||
|
||||
1. `ilinkai.weixin.qq.com` へのネットワークアクセスが必要です。
|
||||
2. メディアファイル(画像、ファイル、動画)は CDN 経由で AES-128-ECB 暗号化を使用して転送され、プログラムが自動的に処理します。
|
||||
3. 頻繁な切断による再スキャンを避けるため、安定したネットワーク環境での実行を推奨します。
|
||||
113
docs/ja/guide/manual-install.mdx
Normal file
113
docs/ja/guide/manual-install.mdx
Normal file
@@ -0,0 +1,113 @@
|
||||
---
|
||||
title: 手動インストール
|
||||
description: CowAgentの手動デプロイ(ソースコード / Docker)
|
||||
---
|
||||
|
||||
## ソースコードによるデプロイ
|
||||
|
||||
### 1. プロジェクトをクローン
|
||||
|
||||
```bash
|
||||
git clone https://github.com/zhayujie/chatgpt-on-wechat
|
||||
cd chatgpt-on-wechat/
|
||||
```
|
||||
|
||||
<Tip>
|
||||
ネットワークに問題がある場合は、ミラーを使用してください: https://gitee.com/zhayujie/chatgpt-on-wechat
|
||||
</Tip>
|
||||
|
||||
### 2. 依存パッケージをインストール
|
||||
|
||||
コア依存パッケージ(必須):
|
||||
|
||||
```bash
|
||||
pip3 install -r requirements.txt
|
||||
```
|
||||
|
||||
オプション依存パッケージ(推奨):
|
||||
|
||||
```bash
|
||||
pip3 install -r requirements-optional.txt
|
||||
```
|
||||
|
||||
### 3. 設定
|
||||
|
||||
設定テンプレートをコピーして編集します:
|
||||
|
||||
```bash
|
||||
cp config-template.json config.json
|
||||
```
|
||||
|
||||
`config.json` にモデルの API キー、チャネルタイプ、その他の設定を入力します。詳細は[モデルのドキュメント](/ja/models/index)を参照してください。
|
||||
|
||||
### 4. 実行
|
||||
|
||||
**ローカルで実行:**
|
||||
|
||||
```bash
|
||||
python3 app.py
|
||||
```
|
||||
|
||||
デフォルトではWebサービスが起動します。`http://localhost:9899/chat` にアクセスしてチャットできます。
|
||||
|
||||
**サーバーでバックグラウンド実行:**
|
||||
|
||||
```bash
|
||||
nohup python3 app.py & tail -f nohup.out
|
||||
```
|
||||
|
||||
## Docker によるデプロイ
|
||||
|
||||
Docker デプロイでは、ソースコードのクローンや依存パッケージのインストールは不要です。Agent モードを使用する場合は、より広範なシステムアクセスが可能なソースコードによるデプロイを推奨します。
|
||||
|
||||
<Note>
|
||||
[Docker](https://docs.docker.com/engine/install/) と docker-compose が必要です。
|
||||
</Note>
|
||||
|
||||
**1. 設定ファイルをダウンロード**
|
||||
|
||||
```bash
|
||||
curl -O https://cdn.link-ai.tech/code/cow/docker-compose.yml
|
||||
```
|
||||
|
||||
`docker-compose.yml` を編集して設定を行います。
|
||||
|
||||
**2. コンテナを起動**
|
||||
|
||||
```bash
|
||||
sudo docker compose up -d
|
||||
```
|
||||
|
||||
**3. ログを確認**
|
||||
|
||||
```bash
|
||||
sudo docker logs -f chatgpt-on-wechat
|
||||
```
|
||||
|
||||
## 主要な設定項目
|
||||
|
||||
```json
|
||||
{
|
||||
"channel_type": "web",
|
||||
"model": "MiniMax-M2.5",
|
||||
"agent": true,
|
||||
"agent_workspace": "~/cow",
|
||||
"agent_max_context_tokens": 40000,
|
||||
"agent_max_context_turns": 30,
|
||||
"agent_max_steps": 15
|
||||
}
|
||||
```
|
||||
|
||||
| パラメータ | 説明 | デフォルト値 |
|
||||
| --- | --- | --- |
|
||||
| `channel_type` | チャネルタイプ | `web` |
|
||||
| `model` | モデル名 | `MiniMax-M2.5` |
|
||||
| `agent` | Agent モードを有効化 | `true` |
|
||||
| `agent_workspace` | Agent のワークスペースパス | `~/cow` |
|
||||
| `agent_max_context_tokens` | 最大コンテキストトークン数 | `40000` |
|
||||
| `agent_max_context_turns` | 最大コンテキストターン数 | `30` |
|
||||
| `agent_max_steps` | タスクごとの最大判断ステップ数 | `15` |
|
||||
|
||||
<Tip>
|
||||
すべての設定オプションはプロジェクトの [`config.py`](https://github.com/zhayujie/chatgpt-on-wechat/blob/master/config.py) に記載されています。
|
||||
</Tip>
|
||||
39
docs/ja/guide/quick-start.mdx
Normal file
39
docs/ja/guide/quick-start.mdx
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
title: ワンクリックインストール
|
||||
description: スクリプトによるCowAgentのワンクリックインストールと管理
|
||||
---
|
||||
|
||||
本プロジェクトでは、ワンクリックでのインストール、設定、起動、管理を行うスクリプトを提供しています。素早くセットアップするには、スクリプトによるデプロイを推奨します。
|
||||
|
||||
Linux、macOS、Windowsに対応しています。Python 3.7〜3.12が必要です(3.9を推奨)。
|
||||
|
||||
## インストールコマンド
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://cdn.link-ai.tech/code/cow/run.sh)
|
||||
```
|
||||
|
||||
スクリプトは以下の手順を自動的に実行します:
|
||||
|
||||
1. Python環境の確認(Python 3.7以上が必要)
|
||||
2. 必要なツールのインストール(git、curlなど)
|
||||
3. プロジェクトを `~/chatgpt-on-wechat` にクローン
|
||||
4. Pythonの依存パッケージをインストール
|
||||
5. AIモデルとチャネルの対話式設定
|
||||
6. サービスの起動
|
||||
|
||||
デフォルトでは、インストール後にWebサービスが起動します。`http://localhost:9899/chat` にアクセスしてチャットを開始できます。
|
||||
|
||||
## 管理コマンド
|
||||
|
||||
インストール後、以下のコマンドでサービスを管理できます:
|
||||
|
||||
| コマンド | 説明 |
|
||||
| --- | --- |
|
||||
| `./run.sh start` | サービスを起動 |
|
||||
| `./run.sh stop` | サービスを停止 |
|
||||
| `./run.sh restart` | サービスを再起動 |
|
||||
| `./run.sh status` | 実行状態を確認 |
|
||||
| `./run.sh logs` | リアルタイムログを表示 |
|
||||
| `./run.sh config` | 再設定 |
|
||||
| `./run.sh update` | プロジェクトコードを更新 |
|
||||
52
docs/ja/guide/upgrade.mdx
Normal file
52
docs/ja/guide/upgrade.mdx
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
title: アップデート
|
||||
description: CowAgent のアップグレード方法
|
||||
---
|
||||
|
||||
## スクリプトによるアップグレード(推奨)
|
||||
|
||||
`run.sh` でサービスを管理している場合、以下のコマンドでワンクリックアップグレードできます:
|
||||
|
||||
```bash
|
||||
./run.sh update
|
||||
```
|
||||
|
||||
このコマンドは以下のフローを自動的に実行します:
|
||||
|
||||
1. 現在実行中のサービスを停止
|
||||
2. 最新コードをプル
|
||||
3. 依存関係を再チェック
|
||||
4. サービスを起動
|
||||
|
||||
## 手動アップグレード
|
||||
|
||||
プロジェクトのルートディレクトリで以下を実行します:
|
||||
|
||||
```bash
|
||||
git pull
|
||||
pip3 install -r requirements.txt
|
||||
```
|
||||
|
||||
更新完了後、サービスを再起動します:
|
||||
|
||||
```bash
|
||||
# run.sh で管理している場合
|
||||
./run.sh restart
|
||||
|
||||
# nohup で直接実行している場合
|
||||
kill $(ps -ef | grep app.py | grep -v grep | awk '{print $2}')
|
||||
nohup python3 app.py & tail -f nohup.out
|
||||
```
|
||||
|
||||
## Docker アップグレード
|
||||
|
||||
`docker-compose.yml` があるディレクトリで以下を実行します:
|
||||
|
||||
```bash
|
||||
sudo docker compose pull
|
||||
sudo docker compose up -d
|
||||
```
|
||||
|
||||
<Tip>
|
||||
アップグレード前に `config.json` 設定ファイルのバックアップを推奨します。Docker 環境でデータを保持する場合は、volume マウントでワークスペースディレクトリを永続化できます。
|
||||
</Tip>
|
||||
77
docs/ja/intro/architecture.mdx
Normal file
77
docs/ja/intro/architecture.mdx
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
title: アーキテクチャ
|
||||
description: CowAgent 2.0 のシステムアーキテクチャとコア設計
|
||||
---
|
||||
|
||||
CowAgent 2.0 は、シンプルなチャットボットから、自律的な思考、タスク計画、長期記憶、Skill の拡張性を備えた Agent アーキテクチャのスーパーインテリジェントアシスタントへと進化しました。
|
||||
|
||||
## システムアーキテクチャ
|
||||
|
||||
CowAgent のアーキテクチャは以下のコアモジュールで構成されています:
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/68ef7b212c6f791e0e74314b912149f9-sz_5847990.png" alt="CowAgent Architecture" />
|
||||
|
||||
### コアモジュール
|
||||
|
||||
| モジュール | 説明 |
|
||||
| --- | --- |
|
||||
| **Channels** | メッセージの受信と送信を行うメッセージチャネル層。Web、Feishu(飛書)、DingTalk(釘釘)、WeCom(企業微信)、WeChat公式アカウントなどをサポート |
|
||||
| **Agent Core** | タスク計画、記憶システム、Skill エンジンを含む Agent エンジン |
|
||||
| **Tools** | Agent が OS リソースにアクセスするためのツール層。10 以上の組み込みツール |
|
||||
| **Models** | 主要な LLM への統一アクセスを提供するモデル層 |
|
||||
|
||||
## Agent モードのワークフロー
|
||||
|
||||
Agent モードが有効な場合、CowAgent は以下のワークフローで自律的な Agent として動作します:
|
||||
|
||||
1. **メッセージ受信** — チャネルを通じてユーザーの入力を受信
|
||||
2. **意図の理解** — タスク要件とコンテキストを分析
|
||||
3. **タスク計画** — 複雑なタスクを複数のステップに分解
|
||||
4. **ツール呼び出し** — 各ステップに適切なツールを選択・実行
|
||||
5. **記憶の更新** — 重要な情報を長期記憶に保存
|
||||
6. **結果の返却** — 実行結果をユーザーに送信
|
||||
|
||||
## ワークスペースのディレクトリ構成
|
||||
|
||||
Agent のワークスペースはデフォルトで `~/cow` にあり、システムプロンプト、記憶ファイル、Skill ファイルを格納しています:
|
||||
|
||||
```
|
||||
~/cow/
|
||||
├── system.md # Agent システムプロンプト
|
||||
├── user.md # ユーザープロフィール
|
||||
├── memory/ # 長期記憶ストレージ
|
||||
│ ├── core.md # コアメモリ
|
||||
│ └── daily/ # デイリーメモリ
|
||||
└── skills/ # カスタム Skill
|
||||
├── skill-1/
|
||||
└── skill-2/
|
||||
```
|
||||
|
||||
シークレットキーはセキュリティのため `~/.cow` ディレクトリに別途保存されます:
|
||||
|
||||
```
|
||||
~/.cow/
|
||||
└── .env # Skill 用のシークレットキー
|
||||
```
|
||||
|
||||
## コア設定
|
||||
|
||||
`config.json` で Agent モードのパラメータを設定します:
|
||||
|
||||
```json
|
||||
{
|
||||
"agent": true,
|
||||
"agent_workspace": "~/cow",
|
||||
"agent_max_context_tokens": 40000,
|
||||
"agent_max_context_turns": 30,
|
||||
"agent_max_steps": 15
|
||||
}
|
||||
```
|
||||
|
||||
| パラメータ | 説明 | デフォルト値 |
|
||||
| --- | --- | --- |
|
||||
| `agent` | Agent モードの有効化 | `true` |
|
||||
| `agent_workspace` | ワークスペースのパス | `~/cow` |
|
||||
| `agent_max_context_tokens` | 最大コンテキストトークン数 | `40000` |
|
||||
| `agent_max_context_turns` | 最大コンテキストターン数 | `30` |
|
||||
| `agent_max_steps` | タスクあたりの最大判断ステップ数 | `15` |
|
||||
105
docs/ja/intro/features.mdx
Normal file
105
docs/ja/intro/features.mdx
Normal file
@@ -0,0 +1,105 @@
|
||||
---
|
||||
title: 機能詳細
|
||||
description: CowAgent の長期記憶、タスク計画、Skill システムの詳細
|
||||
---
|
||||
|
||||
## 1. 長期記憶
|
||||
|
||||
記憶システムにより、Agent は重要な情報を長期にわたって記憶できます。ユーザーが好みや決定、重要な事実を共有すると、Agent は自発的に情報を保存し、会話が一定の長さに達すると自動的に要約を抽出します。記憶はコアメモリとデイリーメモリに分かれており、キーワード検索とベクトル検索の両方をサポートするハイブリッド検索が可能です。
|
||||
|
||||
初回起動時、Agent はユーザーに重要な情報を自発的に尋ね、ワークスペース(デフォルト `~/cow`)に記録します。これには Agent の設定、ユーザーの身元情報、記憶ファイルが含まれます。
|
||||
|
||||
その後の長期的な会話において、Agent は必要に応じてインテリジェントに記憶を保存・取得し、自身の設定やユーザーの好み、記憶ファイルを継続的に更新し、経験と教訓を要約します。これにより、真に自律的な思考と継続的な成長を実現しています。
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260203000455.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
## 2. タスク計画とツール活用
|
||||
|
||||
ツールは Agent がオペレーティングシステムのリソースにアクセスするための中核です。Agent はタスク要件に基づいてインテリジェントにツールを選択・呼び出し、ファイルの読み書き、コマンド実行、スケジュールタスクなどを実行します。組み込みツールはプロジェクトの `agent/tools/` ディレクトリに実装されています。
|
||||
|
||||
**主なツール:** ファイルの読み書き・編集、Bash ターミナル、ファイル送信、スケジューラ、記憶検索、Web 検索、環境設定など。
|
||||
|
||||
### 2.1 ターミナルとファイルアクセス
|
||||
|
||||
OS のターミナルとファイルシステムへのアクセスは、最も基本的かつ中核的な機能です。多くの他のツールや Skill はこの機能の上に構築されています。ユーザーはモバイルデバイスから Agent とやり取りし、パソコンやサーバーのリソースを操作できます:
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260202181130.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
### 2.2 プログラミング能力
|
||||
|
||||
プログラミングとシステムアクセスを組み合わせることで、Agent は完全な **Vibecoding ワークフロー** を実行できます。情報検索、アセット生成、コーディング、テスト、デプロイ、Nginx 設定、公開まで、すべてスマートフォンからの一つのコマンドで実行可能です:
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260203121008.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
### 2.3 スケジュールタスク
|
||||
|
||||
`scheduler` ツールにより動的なスケジュールタスクが可能で、**ワンタイムタスク、固定間隔、Cron 式**をサポートしています。タスクは**固定メッセージ送信**または **Agent 動的タスク**実行としてトリガーできます:
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260202195402.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
### 2.4 環境変数管理
|
||||
|
||||
Skill が必要とするシークレットキーは環境変数ファイルに保存され、`env_config` ツールによって管理されます。会話を通じてシークレットを更新でき、セキュリティ保護とマスキング機能が組み込まれています:
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260202234939.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
## 3. Skill システム
|
||||
|
||||
Skill システムは Agent に無限の拡張性を提供します。各 Skill は説明ファイル、実行スクリプト(任意)、リソース(任意)で構成され、特定のタイプのタスクを完了する方法を記述します。Skill により Agent は複雑なワークフローの指示に従い、ツールを呼び出し、サードパーティシステムと連携できます。
|
||||
|
||||
- **組み込み Skill:** プロジェクトの `skills/` ディレクトリにあり、Skill クリエイター、画像認識、LinkAI Agent、Web フェッチなどが含まれます。組み込み Skill は依存条件(API キー、システムコマンドなど)に基づいて自動的に有効化されます。
|
||||
- **カスタム Skill:** ユーザーが会話を通じて作成し、ワークスペース(`~/cow/skills/`)に保存されます。あらゆる複雑なビジネスプロセスやサードパーティ連携を実装できます。
|
||||
|
||||
### 3.1 Skill の作成
|
||||
|
||||
`skill-creator` Skill により、会話を通じて Skill を素早く作成できます。ワークフローを Skill としてコード化するよう Agent に依頼したり、API ドキュメントやサンプルを送信して Agent に直接連携を完成させることができます:
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260202202247.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
### 3.2 Web 検索と画像認識
|
||||
|
||||
- **Web 検索:** 組み込みの `web_search` ツールで、複数の検索エンジンをサポートします。`BOCHA_API_KEY` または `LINKAI_API_KEY` を設定して有効化してください。
|
||||
- **画像認識:** 組み込みの `openai-image-vision` Skill で、`gpt-4.1-mini`、`gpt-4.1` などのモデルをサポートします。`OPENAI_API_KEY` が必要です。
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260202213219.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
### 3.3 サードパーティナレッジベースとプラグイン
|
||||
|
||||
`linkai-agent` Skill により、[LinkAI](https://link-ai.tech/) 上のすべての Agent を Skill として利用でき、マルチ Agent による意思決定が可能になります。
|
||||
|
||||
設定方法:`env_config` で `LINKAI_API_KEY` を設定し、`skills/linkai-agent/config.json` に Agent の説明を追加します:
|
||||
|
||||
```json
|
||||
{
|
||||
"apps": [
|
||||
{
|
||||
"app_code": "G7z6vKwp",
|
||||
"app_name": "LinkAI Customer Support",
|
||||
"app_description": "Select only when the user needs help with LinkAI platform questions"
|
||||
},
|
||||
{
|
||||
"app_code": "SFY5x7JR",
|
||||
"app_name": "Content Creator",
|
||||
"app_description": "Use only when the user needs to create images or videos"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260202234350.png" width="750" />
|
||||
</Frame>
|
||||
68
docs/ja/intro/index.mdx
Normal file
68
docs/ja/intro/index.mdx
Normal file
@@ -0,0 +1,68 @@
|
||||
---
|
||||
title: はじめに
|
||||
description: CowAgent - LLM を活用した AI スーパーアシスタント
|
||||
---
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/78c5dd674e2c828642ecc0406669fed7.png" alt="CowAgent" width="600px"/>
|
||||
|
||||
**CowAgent** は、自律的なタスク計画、長期記憶、Skill システム、マルチモーダルメッセージ、複数モデル対応、マルチプラットフォームデプロイを備えた、LLM を活用した AI スーパーアシスタントです。
|
||||
|
||||
CowAgent は自ら思考しタスクを計画し、コンピュータや外部リソースを操作し、Skill を作成・実行し、長期記憶により継続的に成長します。複数モデルの柔軟な切り替えをサポートし、テキスト、音声、画像、ファイルなどのマルチモーダルメッセージを処理でき、WeChat、Web、Feishu(飛書)、DingTalk(釘釘)、WeCom(企業微信)、WeChat公式アカウントに統合できます。お使いのパソコンやサーバー上で24時間365日稼働します。
|
||||
|
||||
<Card title="GitHub" icon="github" href="https://github.com/zhayujie/chatgpt-on-wechat">
|
||||
github.com/zhayujie/chatgpt-on-wechat
|
||||
</Card>
|
||||
|
||||
## コア機能
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="自律タスク計画" icon="brain" href="/ja/intro/architecture">
|
||||
複雑なタスクを理解し、自律的に実行計画を立て、目標が達成されるまで思考とツール呼び出しを続けます。ツールを通じてファイルシステム、ターミナル、ブラウザ、スケジューラなどのシステムリソースにアクセスできます。
|
||||
</Card>
|
||||
<Card title="長期記憶" icon="database" href="/ja/memory">
|
||||
会話の記憶をローカルファイルやデータベースに自動的に永続化します。コアメモリとデイリーメモリを含み、キーワード検索とベクトル検索に対応しています。
|
||||
</Card>
|
||||
<Card title="Skill システム" icon="puzzle-piece" href="/ja/skills/index">
|
||||
Skill の作成・実行エンジンを実装し、組み込み Skill を搭載。自然言語の会話を通じてカスタム Skill の開発もサポートしています。
|
||||
</Card>
|
||||
<Card title="マルチモーダルメッセージ" icon="image" href="/ja/channels/web">
|
||||
テキスト、画像、音声、ファイルなどのメッセージタイプの解析、処理、生成、送信をサポートします。
|
||||
</Card>
|
||||
<Card title="複数モデル対応" icon="microchip" href="/ja/models/index">
|
||||
OpenAI、Claude、Gemini、DeepSeek、MiniMax、GLM、Qwen、Kimi、Doubao など、主要なモデルプロバイダーをサポートしています。
|
||||
</Card>
|
||||
<Card title="マルチプラットフォームデプロイ" icon="server" href="/ja/channels/weixin">
|
||||
ローカルコンピュータやサーバー上で動作し、WeChat、Web、Feishu(飛書)、DingTalk(釘釘)、WeChat公式アカウント、WeCom(企業微信)アプリケーションに統合できます。
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## クイック体験
|
||||
|
||||
ターミナルで以下のコマンドを実行すると、ワンクリックでインストール、設定、起動ができます:
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://cdn.link-ai.tech/code/cow/run.sh)
|
||||
```
|
||||
|
||||
デフォルトでは実行後に Web サービスが起動します。`http://localhost:9899/chat` にアクセスして Web インターフェースでチャットできます。
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="クイックスタート" icon="rocket" href="/ja/guide/quick-start">
|
||||
インストールと実行の完全ガイド
|
||||
</Card>
|
||||
<Card title="アーキテクチャ" icon="sitemap" href="/ja/intro/architecture">
|
||||
CowAgent システムアーキテクチャ設計
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## 免責事項
|
||||
|
||||
1. 本プロジェクトは [MIT License](https://github.com/zhayujie/chatgpt-on-wechat/blob/master/LICENSE) に基づき、技術研究および学習を目的としています。利用者は現地の法律、規制、ポリシー、および企業の社内規程を遵守する必要があります。違法行為や権利侵害につながる利用は禁止されています。
|
||||
2. Agent モードは通常のチャットモードよりも多くのトークンを消費します。効果とコストを考慮してモデルを選択してください。Agent はホスト OS にアクセスできるため、デプロイには十分注意してください。
|
||||
3. CowAgent はオープンソース開発に注力しており、いかなる暗号通貨の発行、認可、参加も行っておりません。
|
||||
|
||||
## コミュニティ
|
||||
|
||||
WeChat でアシスタントを追加して、オープンソースコミュニティに参加しましょう:
|
||||
|
||||
<img width="140" src="https://img-1317903499.cos.ap-guangzhou.myqcloud.com/docs/open-community.png" />
|
||||
66
docs/ja/memory.mdx
Normal file
66
docs/ja/memory.mdx
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
title: 記憶
|
||||
description: CowAgent 長期記憶システム
|
||||
---
|
||||
|
||||
記憶システムにより、Agent は重要な情報を長期にわたって記憶し、継続的に経験を蓄積し、ユーザーの好みを理解し、真に自律的な思考と継続的な成長を実現できます。
|
||||
|
||||
## 記憶の種類
|
||||
|
||||
### コア記憶 (MEMORY.md)
|
||||
|
||||
`~/cow/MEMORY.md` に保存され、長期的なユーザーの好み、重要な決定、主要な事実など、時間が経っても薄れない情報を含みます。毎回の会話ターンでバックグラウンド知識としてシステムプロンプトに自動的に注入されます。
|
||||
|
||||
### 日次記憶 (memory/YYYY-MM-DD.md)
|
||||
|
||||
`~/cow/memory/` ディレクトリに保存され、日付で命名されます(例:`2026-03-08.md`)。日々の会話の要約と主要なイベントを記録します。空ファイルの生成を避けるため、最初の書き込み時にのみファイルが作成されます。
|
||||
|
||||
## 記憶の書き込み
|
||||
|
||||
Agent は以下のメカニズムにより、会話内容を日次記憶に自動的に永続化します:
|
||||
|
||||
- **コンテキストトリミング時** — 会話ターン数またはトークン数が設定上限を超えた場合、コンテキストの古い半分が一括でトリミングされ、破棄されたコンテンツは LLM によって要約されて重要な情報として日次記憶ファイルに書き込まれます
|
||||
- **毎日のスケジュール要約** — 毎日 23:55 に自動的にフル要約がトリガーされ、アクティビティが少ない日でも記憶が保存されます(内容が変更されていない場合はスキップ)
|
||||
- **API コンテキストオーバーフロー時** — モデル API がコンテキストオーバーフローエラーを返した場合、緊急措置として現在の会話要約が保存されます
|
||||
|
||||
すべての記憶書き込みはバックグラウンドスレッドで非同期に実行され(LLM の要約 + ファイル書き込み)、通常の会話応答をブロックしません。
|
||||
|
||||
## 初回起動
|
||||
|
||||
初回起動時に、Agent はユーザーに主要な情報を積極的に尋ね、ワークスペース(デフォルト `~/cow`)に保存します:
|
||||
|
||||
| ファイル | 説明 |
|
||||
| --- | --- |
|
||||
| `system.md` | Agent のシステムプロンプトと動作設定 |
|
||||
| `user.md` | ユーザーの身元情報と好み |
|
||||
| `MEMORY.md` | コア記憶(長期) |
|
||||
| `memory/YYYY-MM-DD.md` | 日次記憶(オンデマンドで作成) |
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260203000455.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
## 記憶の検索
|
||||
|
||||
記憶システムはハイブリッド検索モードをサポートしています:
|
||||
|
||||
- **キーワード検索** — キーワードに基づいて過去の記憶をマッチング
|
||||
- **ベクトル検索** — セマンティック類似性検索により、異なる表現でも関連する記憶を発見
|
||||
|
||||
Agent は必要に応じて会話中に自動的に記憶検索をトリガーし、関連する過去の情報をコンテキストに組み込みます。コア記憶(`MEMORY.md`)は常にシステムプロンプトに注入され、日次記憶は検索を通じてオンデマンドで読み込まれます。
|
||||
|
||||
## 設定
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_workspace": "~/cow",
|
||||
"agent_max_context_tokens": 40000,
|
||||
"agent_max_context_turns": 20
|
||||
}
|
||||
```
|
||||
|
||||
| パラメータ | 説明 | デフォルト |
|
||||
| --- | --- | --- |
|
||||
| `agent_workspace` | ワークスペースパス、記憶ファイルはこのディレクトリ配下に保存されます | `~/cow` |
|
||||
| `agent_max_context_tokens` | 最大コンテキストトークン数。超過時に半分がトリミングされ、記憶として要約されます | `40000` |
|
||||
| `agent_max_context_turns` | 最大コンテキストターン数。超過時に半分がトリミングされ、記憶として要約されます | `20` |
|
||||
17
docs/ja/models/claude.mdx
Normal file
17
docs/ja/models/claude.mdx
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: Claude
|
||||
description: Claudeモデルの設定
|
||||
---
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "claude-sonnet-4-6",
|
||||
"claude_api_key": "YOUR_API_KEY"
|
||||
}
|
||||
```
|
||||
|
||||
| パラメータ | 説明 |
|
||||
| --- | --- |
|
||||
| `model` | `claude-sonnet-4-6`、`claude-opus-4-6`、`claude-sonnet-4-5`、`claude-sonnet-4-0`、`claude-3-5-sonnet-latest`などから選択可能。[公式モデル一覧](https://docs.anthropic.com/en/docs/about-claude/models/overview)を参照 |
|
||||
| `claude_api_key` | [Claude Console](https://console.anthropic.com/settings/keys)で作成 |
|
||||
| `claude_api_base` | 任意。デフォルトは`https://api.anthropic.com/v1`。サードパーティプロキシを使用する場合に変更 |
|
||||
139
docs/ja/models/coding-plan.mdx
Normal file
139
docs/ja/models/coding-plan.mdx
Normal file
@@ -0,0 +1,139 @@
|
||||
---
|
||||
title: Coding Plan
|
||||
description: Coding Planモデルの設定
|
||||
---
|
||||
|
||||
> Coding Planは各プロバイダーが提供する月額サブスクリプションパッケージで、高頻度のAgent利用に最適です。CowAgentはOpenAI互換モードにより、すべてのCoding Planプロバイダーをサポートしています。
|
||||
|
||||
<Note>
|
||||
Coding PlanのAPI BaseとAPI Keyは、通常の従量課金制のものとは別になっています。各プロバイダーのプラットフォームから取得してください。
|
||||
</Note>
|
||||
|
||||
## 共通設定
|
||||
|
||||
すべてのプロバイダーはOpenAI互換プロトコルでアクセスでき、Webコンソールから素早く設定できます。モデルプロバイダーを**OpenAI**に設定し、カスタムモデルを選択してモデルコードを入力し、対応するプロバイダーのAPI BaseとAPI Keyを入力してください:
|
||||
|
||||
<img src="https://cdn.link-ai.tech/doc/20260318113134.png" width="800"/>
|
||||
|
||||
`config.json`で直接設定することも可能です:
|
||||
|
||||
```json
|
||||
{
|
||||
"bot_type": "openai",
|
||||
"model": "MODEL_NAME",
|
||||
"open_ai_api_base": "PROVIDER_CODING_PLAN_API_BASE",
|
||||
"open_ai_api_key": "YOUR_API_KEY"
|
||||
}
|
||||
```
|
||||
|
||||
| パラメータ | 説明 |
|
||||
| --- | --- |
|
||||
| `bot_type` | `openai`を指定(OpenAI互換モード) |
|
||||
| `model` | プロバイダーがサポートするモデル名 |
|
||||
| `open_ai_api_base` | プロバイダーのCoding Plan API Base URL |
|
||||
| `open_ai_api_key` | プロバイダーのCoding Plan API Key |
|
||||
|
||||
---
|
||||
|
||||
## 阿里云
|
||||
|
||||
```json
|
||||
{
|
||||
"bot_type": "openai",
|
||||
"model": "qwen3.5-plus",
|
||||
"open_ai_api_base": "https://coding.dashscope.aliyuncs.com/v1",
|
||||
"open_ai_api_key": "YOUR_API_KEY"
|
||||
}
|
||||
```
|
||||
|
||||
| パラメータ | 説明 |
|
||||
| --- | --- |
|
||||
| `model` | `qwen3.5-plus`、`qwen3-max-2026-01-23`、`qwen3-coder-next`、`qwen3-coder-plus`、`glm-5`、`glm-4.7`、`kimi-k2.5`、`MiniMax-M2.5` |
|
||||
| `open_ai_api_base` | `https://coding.dashscope.aliyuncs.com/v1` |
|
||||
| `open_ai_api_key` | Coding Plan専用キー(従量課金とは共有不可) |
|
||||
|
||||
参考: [クイックスタート](https://help.aliyun.com/zh/model-studio/coding-plan-quickstart?spm=a2c4g.11186623.help-menu-2400256.d_0_2_1.70115203zi5Igc)、[モデル一覧](https://help.aliyun.com/zh/model-studio/coding-plan)
|
||||
|
||||
---
|
||||
|
||||
## MiniMax
|
||||
|
||||
```json
|
||||
{
|
||||
"bot_type": "openai",
|
||||
"model": "MiniMax-M2.5",
|
||||
"open_ai_api_base": "https://api.minimaxi.com/v1",
|
||||
"open_ai_api_key": "YOUR_API_KEY"
|
||||
}
|
||||
```
|
||||
|
||||
| パラメータ | 説明 |
|
||||
| --- | --- |
|
||||
| `model` | `MiniMax-M2.5`、`MiniMax-M2.5-highspeed`、`MiniMax-M2.1`、`MiniMax-M2` |
|
||||
| `open_ai_api_base` | 中国: `https://api.minimaxi.com/v1`、グローバル: `https://api.minimax.io/v1` |
|
||||
| `open_ai_api_key` | Coding Plan専用キー(従量課金とは共有不可) |
|
||||
|
||||
参考: [中国キー](https://platform.minimaxi.com/docs/coding-plan/quickstart)、[モデル一覧](https://platform.minimaxi.com/docs/guides/pricing-coding-plan)、[グローバルキー](https://platform.minimax.io/docs/coding-plan/quickstart)
|
||||
|
||||
---
|
||||
|
||||
## 智谱 GLM
|
||||
|
||||
```json
|
||||
{
|
||||
"bot_type": "openai",
|
||||
"model": "glm-4.7",
|
||||
"open_ai_api_base": "https://open.bigmodel.cn/api/coding/paas/v4",
|
||||
"open_ai_api_key": "YOUR_API_KEY"
|
||||
}
|
||||
```
|
||||
|
||||
| パラメータ | 説明 |
|
||||
| --- | --- |
|
||||
| `model` | `glm-5`、`glm-4.7`、`glm-4.6`、`glm-4.5`、`glm-4.5-air` |
|
||||
| `open_ai_api_base` | 中国: `https://open.bigmodel.cn/api/coding/paas/v4`、グローバル: `https://api.z.ai/api/coding/paas/v4` |
|
||||
| `open_ai_api_key` | 標準APIと共有 |
|
||||
|
||||
参考: [中国クイックスタート](https://docs.bigmodel.cn/cn/coding-plan/quick-start)、[グローバルクイックスタート](https://docs.z.ai/devpack/quick-start)
|
||||
|
||||
---
|
||||
|
||||
## Kimi
|
||||
|
||||
```json
|
||||
{
|
||||
"bot_type": "openai",
|
||||
"model": "kimi-for-coding",
|
||||
"open_ai_api_base": "https://api.kimi.com/coding/v1",
|
||||
"open_ai_api_key": "YOUR_API_KEY"
|
||||
}
|
||||
```
|
||||
|
||||
| パラメータ | 説明 |
|
||||
| --- | --- |
|
||||
| `model` | `kimi-for-coding` |
|
||||
| `open_ai_api_base` | `https://api.kimi.com/coding/v1` |
|
||||
| `open_ai_api_key` | Coding Plan専用キー(従量課金とは共有不可) |
|
||||
|
||||
参考: [キー & ドキュメント](https://www.kimi.com/code/docs/)
|
||||
|
||||
---
|
||||
|
||||
## 火山引擎
|
||||
|
||||
```json
|
||||
{
|
||||
"bot_type": "openai",
|
||||
"model": "Doubao-Seed-2.0-Code",
|
||||
"open_ai_api_base": "https://ark.cn-beijing.volces.com/api/coding/v3",
|
||||
"open_ai_api_key": "YOUR_API_KEY"
|
||||
}
|
||||
```
|
||||
|
||||
| パラメータ | 説明 |
|
||||
| --- | --- |
|
||||
| `model` | `Doubao-Seed-2.0-Code`、`Doubao-Seed-2.0-pro`、`Doubao-Seed-2.0-lite`、`Doubao-Seed-Code`、`MiniMax-M2.5`、`Kimi-K2.5`、`GLM-4.7`、`DeepSeek-V3.2` |
|
||||
| `open_ai_api_base` | `https://ark.cn-beijing.volces.com/api/coding/v3` |
|
||||
| `open_ai_api_key` | 標準APIと共有 |
|
||||
|
||||
参考: [クイックスタート](https://www.volcengine.com/docs/82379/1928261?lang=zh)
|
||||
22
docs/ja/models/deepseek.mdx
Normal file
22
docs/ja/models/deepseek.mdx
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
title: DeepSeek
|
||||
description: DeepSeekモデルの設定
|
||||
---
|
||||
|
||||
OpenAI互換の設定を使用します:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "deepseek-chat",
|
||||
"bot_type": "openai",
|
||||
"open_ai_api_key": "YOUR_API_KEY",
|
||||
"open_ai_api_base": "https://api.deepseek.com/v1"
|
||||
}
|
||||
```
|
||||
|
||||
| パラメータ | 説明 |
|
||||
| --- | --- |
|
||||
| `model` | `deepseek-chat` (DeepSeek-V3)、`deepseek-reasoner` (DeepSeek-R1) |
|
||||
| `bot_type` | `openai`を指定(OpenAI互換モード) |
|
||||
| `open_ai_api_key` | [DeepSeek Platform](https://platform.deepseek.com/api_keys)で作成 |
|
||||
| `open_ai_api_base` | DeepSeekプラットフォームのBASE URL |
|
||||
17
docs/ja/models/doubao.mdx
Normal file
17
docs/ja/models/doubao.mdx
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: Doubao (ByteDance)
|
||||
description: Doubao (火山方舟) モデルの設定
|
||||
---
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "doubao-seed-2-0-code-preview-260215",
|
||||
"ark_api_key": "YOUR_API_KEY"
|
||||
}
|
||||
```
|
||||
|
||||
| パラメータ | 説明 |
|
||||
| --- | --- |
|
||||
| `model` | `doubao-seed-2-0-code-preview-260215`、`doubao-seed-2-0-pro-260215`、`doubao-seed-2-0-lite-260215`などから選択可能 |
|
||||
| `ark_api_key` | [火山方舟 Console](https://console.volcengine.com/ark/region:ark+cn-beijing/apikey)で作成 |
|
||||
| `ark_base_url` | 任意。デフォルトは`https://ark.cn-beijing.volces.com/api/v3` |
|
||||
16
docs/ja/models/gemini.mdx
Normal file
16
docs/ja/models/gemini.mdx
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
title: Gemini
|
||||
description: Google Geminiモデルの設定
|
||||
---
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gemini-3.1-pro-preview",
|
||||
"gemini_api_key": "YOUR_API_KEY"
|
||||
}
|
||||
```
|
||||
|
||||
| パラメータ | 説明 |
|
||||
| --- | --- |
|
||||
| `model` | `gemini-3.1-flash-lite-preview`、`gemini-3.1-pro-preview`、`gemini-3-flash-preview`、`gemini-3-pro-preview`などから選択可能。[公式ドキュメント](https://ai.google.dev/gemini-api/docs/models)を参照 |
|
||||
| `gemini_api_key` | [Google AI Studio](https://aistudio.google.com/app/apikey)で作成 |
|
||||
27
docs/ja/models/glm.mdx
Normal file
27
docs/ja/models/glm.mdx
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
title: GLM (智谱AI)
|
||||
description: 智谱AI GLMモデルの設定
|
||||
---
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "glm-5-turbo",
|
||||
"zhipu_ai_api_key": "YOUR_API_KEY"
|
||||
}
|
||||
```
|
||||
|
||||
| パラメータ | 説明 |
|
||||
| --- | --- |
|
||||
| `model` | `glm-5-turbo`、`glm-5`、`glm-4.7`、`glm-4-plus`、`glm-4-flash`、`glm-4-air`などから選択可能。[モデルコード](https://bigmodel.cn/dev/api/normal-model/glm-4)を参照 |
|
||||
| `zhipu_ai_api_key` | [智谱AI Console](https://www.bigmodel.cn/usercenter/proj-mgmt/apikeys)で作成 |
|
||||
|
||||
OpenAI互換の設定もサポートしています:
|
||||
|
||||
```json
|
||||
{
|
||||
"bot_type": "openai",
|
||||
"model": "glm-5-turbo",
|
||||
"open_ai_api_base": "https://open.bigmodel.cn/api/paas/v4",
|
||||
"open_ai_api_key": "YOUR_API_KEY"
|
||||
}
|
||||
```
|
||||
55
docs/ja/models/index.mdx
Normal file
55
docs/ja/models/index.mdx
Normal file
@@ -0,0 +1,55 @@
|
||||
---
|
||||
title: モデル概要
|
||||
description: CowAgentがサポートするモデルとおすすめの選択肢
|
||||
---
|
||||
|
||||
CowAgentは国内外の主要なLLMをサポートしています。モデルインターフェースはプロジェクトの`models/`ディレクトリに実装されています。
|
||||
|
||||
<Note>
|
||||
Agent モードでは、品質とコストのバランスから以下のモデルをおすすめします: MiniMax-M2.7、glm-5-turbo、kimi-k2.5、qwen3.5-plus、claude-sonnet-4-6、gemini-3.1-pro-preview
|
||||
</Note>
|
||||
|
||||
## 設定
|
||||
|
||||
選択したモデルに応じて、`config.json`にモデル名とAPI Keyを設定してください。各モデルは`bot_type`を`openai`に設定し、`open_ai_api_base`と`open_ai_api_key`を設定することで、OpenAI互換アクセスもサポートしています。
|
||||
|
||||
また、[LinkAI](https://link-ai.tech)プラットフォームインターフェースを使用すると、ナレッジベース、ワークフロー、その他のAgent機能をサポートしながら、複数のモデルを柔軟に切り替えることができます。
|
||||
|
||||
## サポートモデル
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="MiniMax" href="/ja/models/minimax">
|
||||
MiniMax-M2.7およびその他のシリーズモデル
|
||||
</Card>
|
||||
<Card title="GLM (智谱AI)" href="/ja/models/glm">
|
||||
glm-5-turbo、glm-5およびその他のシリーズモデル
|
||||
</Card>
|
||||
<Card title="Qwen (通义千问)" href="/ja/models/qwen">
|
||||
qwen3.5-plus、qwen3-maxなど
|
||||
</Card>
|
||||
<Card title="Kimi" href="/ja/models/kimi">
|
||||
kimi-k2.5、kimi-k2など
|
||||
</Card>
|
||||
<Card title="Doubao (ByteDance)" href="/ja/models/doubao">
|
||||
doubao-seedシリーズモデル
|
||||
</Card>
|
||||
<Card title="Claude" href="/ja/models/claude">
|
||||
claude-sonnet-4-6など
|
||||
</Card>
|
||||
<Card title="Gemini" href="/ja/models/gemini">
|
||||
gemini-3.1-pro-previewなど
|
||||
</Card>
|
||||
<Card title="OpenAI" href="/ja/models/openai">
|
||||
gpt-5.4、gpt-4.1、oシリーズなど
|
||||
</Card>
|
||||
<Card title="DeepSeek" href="/ja/models/deepseek">
|
||||
deepseek-chat、deepseek-reasoner
|
||||
</Card>
|
||||
<Card title="LinkAI" href="/ja/models/linkai">
|
||||
統合マルチモデルインターフェース + ナレッジベース
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
<Tip>
|
||||
モデル名の完全なリストについては、プロジェクトの[`common/const.py`](https://github.com/zhayujie/chatgpt-on-wechat/blob/master/common/const.py)ファイルを参照してください。
|
||||
</Tip>
|
||||
27
docs/ja/models/kimi.mdx
Normal file
27
docs/ja/models/kimi.mdx
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
title: Kimi (Moonshot)
|
||||
description: Kimi (Moonshot) モデルの設定
|
||||
---
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "kimi-k2.5",
|
||||
"moonshot_api_key": "YOUR_API_KEY"
|
||||
}
|
||||
```
|
||||
|
||||
| パラメータ | 説明 |
|
||||
| --- | --- |
|
||||
| `model` | `kimi-k2.5`、`kimi-k2`、`moonshot-v1-8k`、`moonshot-v1-32k`、`moonshot-v1-128k`から選択可能 |
|
||||
| `moonshot_api_key` | [Moonshot Console](https://platform.moonshot.cn/console/api-keys)で作成 |
|
||||
|
||||
OpenAI互換の設定もサポートしています:
|
||||
|
||||
```json
|
||||
{
|
||||
"bot_type": "openai",
|
||||
"model": "kimi-k2.5",
|
||||
"open_ai_api_base": "https://api.moonshot.cn/v1",
|
||||
"open_ai_api_key": "YOUR_API_KEY"
|
||||
}
|
||||
```
|
||||
21
docs/ja/models/linkai.mdx
Normal file
21
docs/ja/models/linkai.mdx
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
title: LinkAI
|
||||
description: LinkAIプラットフォームで複数モデルに統合アクセス
|
||||
---
|
||||
|
||||
[LinkAI](https://link-ai.tech)プラットフォームでは、OpenAI、Claude、Gemini、DeepSeek、Qwen、Kimiなどのモデルを柔軟に切り替えることができ、ナレッジベース、ワークフロー、プラグイン、その他のAgent機能をサポートしています。
|
||||
|
||||
```json
|
||||
{
|
||||
"use_linkai": true,
|
||||
"linkai_api_key": "YOUR_API_KEY"
|
||||
}
|
||||
```
|
||||
|
||||
| パラメータ | 説明 |
|
||||
| --- | --- |
|
||||
| `use_linkai` | `true`に設定してLinkAIインターフェースを有効化 |
|
||||
| `linkai_api_key` | [LinkAI Console](https://link-ai.tech/console/interface)で作成 |
|
||||
| `model` | 空のままにするとAgentのデフォルトモデルを使用。プラットフォーム上で柔軟に切り替え可能。[モデル一覧](https://link-ai.tech/console/models)のすべてのモデルをサポート |
|
||||
|
||||
詳細は[APIドキュメント](https://docs.link-ai.tech/platform/api)を参照してください。
|
||||
27
docs/ja/models/minimax.mdx
Normal file
27
docs/ja/models/minimax.mdx
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
title: MiniMax
|
||||
description: MiniMaxモデルの設定
|
||||
---
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "MiniMax-M2.7",
|
||||
"minimax_api_key": "YOUR_API_KEY"
|
||||
}
|
||||
```
|
||||
|
||||
| パラメータ | 説明 |
|
||||
| --- | --- |
|
||||
| `model` | `MiniMax-M2.7`、`MiniMax-M2.5`、`MiniMax-M2.1`、`MiniMax-M2.1-lightning`、`MiniMax-M2`などから選択可能 |
|
||||
| `minimax_api_key` | [MiniMax Console](https://platform.minimaxi.com/user-center/basic-information/interface-key)で作成 |
|
||||
|
||||
OpenAI互換の設定もサポートしています:
|
||||
|
||||
```json
|
||||
{
|
||||
"bot_type": "openai",
|
||||
"model": "MiniMax-M2.7",
|
||||
"open_ai_api_base": "https://api.minimaxi.com/v1",
|
||||
"open_ai_api_key": "YOUR_API_KEY"
|
||||
}
|
||||
```
|
||||
19
docs/ja/models/openai.mdx
Normal file
19
docs/ja/models/openai.mdx
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
title: OpenAI
|
||||
description: OpenAIモデルの設定
|
||||
---
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-5.4",
|
||||
"open_ai_api_key": "YOUR_API_KEY",
|
||||
"open_ai_api_base": "https://api.openai.com/v1"
|
||||
}
|
||||
```
|
||||
|
||||
| パラメータ | 説明 |
|
||||
| --- | --- |
|
||||
| `model` | OpenAI APIの[modelパラメータ](https://platform.openai.com/docs/models)に対応。oシリーズ、gpt-5.4、gpt-5シリーズ、gpt-4.1などをサポート。Agentモードでは`gpt-5.4`を推奨 |
|
||||
| `open_ai_api_key` | [OpenAI Platform](https://platform.openai.com/api-keys)で作成 |
|
||||
| `open_ai_api_base` | 任意。サードパーティプロキシを使用する場合に変更 |
|
||||
| `bot_type` | 公式OpenAIモデルでは不要。Claudeなど非OpenAIモデルをプロキシ経由で使用する場合は`openai`に設定 |
|
||||
27
docs/ja/models/qwen.mdx
Normal file
27
docs/ja/models/qwen.mdx
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
title: Qwen (通义千问)
|
||||
description: 通义千问モデルの設定
|
||||
---
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "qwen3.5-plus",
|
||||
"dashscope_api_key": "YOUR_API_KEY"
|
||||
}
|
||||
```
|
||||
|
||||
| パラメータ | 説明 |
|
||||
| --- | --- |
|
||||
| `model` | `qwen3.5-plus`、`qwen3-max`、`qwen-max`、`qwen-plus`、`qwen-turbo`、`qwq-plus`などから選択可能 |
|
||||
| `dashscope_api_key` | [百炼 Console](https://bailian.console.aliyun.com/?tab=model#/api-key)で作成。[公式ドキュメント](https://bailian.console.aliyun.com/?tab=api#/api)を参照 |
|
||||
|
||||
OpenAI互換の設定もサポートしています:
|
||||
|
||||
```json
|
||||
{
|
||||
"bot_type": "openai",
|
||||
"model": "qwen3.5-plus",
|
||||
"open_ai_api_base": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
"open_ai_api_key": "YOUR_API_KEY"
|
||||
}
|
||||
```
|
||||
24
docs/ja/releases/overview.mdx
Normal file
24
docs/ja/releases/overview.mdx
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
title: 変更履歴
|
||||
description: CowAgent バージョン履歴
|
||||
---
|
||||
|
||||
| バージョン | 日付 | 説明 |
|
||||
| --- | --- | --- |
|
||||
| [2.0.4](/ja/releases/v2.0.4) | 2026.03.22 | 個人WeChatチャネル追加、新モデルサポート、日本語ドキュメント、スクリプトリファクタリングおよび複数修正 |
|
||||
| [2.0.2](/ja/releases/v2.0.2) | 2026.02.27 | Web Console アップグレード、マルチチャネル同時実行、セッション永続化 |
|
||||
| [2.0.1](/en/releases/v2.0.1) | 2026.02.27 | 組み込み Web Search ツール、スマートコンテキスト管理、複数の修正 |
|
||||
| [2.0.0](/en/releases/v2.0.0) | 2026.02.03 | AI スーパーアシスタントへの全面アップグレード |
|
||||
| 1.7.6 | 2025.05.23 | Web Channel 最適化、AgentMesh プラグイン |
|
||||
| 1.7.5 | 2025.04.11 | DeepSeek モデル |
|
||||
| 1.7.4 | 2024.12.13 | Gemini 2.0 モデル、Web Channel |
|
||||
| 1.7.3 | 2024.10.31 | 安定性の改善、データベース機能 |
|
||||
| 1.7.2 | 2024.09.26 | ワンクリックインストールスクリプト、o1 モデル |
|
||||
| 1.7.0 | 2024.08.02 | 讯飞 4.0 モデル、ナレッジベース参照 |
|
||||
| 1.6.9 | 2024.07.19 | gpt-4o-mini、阿里音声認識 |
|
||||
| 1.6.8 | 2024.07.05 | Claude 3.5、Gemini 1.5 Pro |
|
||||
| 1.6.0 | 2024.04.26 | Kimi 統合、gpt-4-turbo アップグレード |
|
||||
| 1.5.0 | 2023.11.10 | gpt-4-turbo、dall-e-3、tts マルチモーダル |
|
||||
| 1.0.0 | 2022.12.12 | プロジェクト作成、初の ChatGPT 統合 |
|
||||
|
||||
完全な履歴は [GitHub Releases](https://github.com/zhayujie/chatgpt-on-wechat/releases) をご覧ください。
|
||||
63
docs/ja/releases/v2.0.0.mdx
Normal file
63
docs/ja/releases/v2.0.0.mdx
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: v2.0.0
|
||||
description: CowAgent 2.0 - チャットボットから AI スーパーアシスタントへの全面アップグレード
|
||||
---
|
||||
|
||||
CowAgent 2.0 は、チャットボットから **AI スーパーアシスタント** への包括的なアップグレードです。自律的な思考とタスク計画、長期記憶、コンピューターの操作、Skill の作成と実行が可能です。
|
||||
|
||||
**リリース日**: 2026.02.03 | [GitHub Release](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/2.0.0)
|
||||
|
||||
## 主な更新内容
|
||||
|
||||
### Agent コア
|
||||
|
||||
- **複雑なタスク計画**: マルチターン推論による自律的な計画
|
||||
- **長期記憶**: キーワードおよびベクトル検索による永続的な記憶
|
||||
- **組み込みツール**: ファイル操作、Bash、ブラウザ、スケジューラなど 10 以上のツール
|
||||
- **Web 検索**: 組み込みの `web_search` ツール、複数の検索エンジンに対応、対応する API キーを設定して使用
|
||||
- **Skill システム**: 組み込みおよびカスタム Skill をサポートする Skill エンジン
|
||||
- **セキュリティとコスト**: シークレット管理、プロンプト制御、トークン制限
|
||||
|
||||
### その他
|
||||
|
||||
- **チャネル**: 飞书/钉钉 WebSocket 対応、画像・ファイルメッセージ
|
||||
- **モデル**: claude-sonnet-4-5、gemini-3-pro-preview、glm-4.7、MiniMax-M2.1、qwen3-max
|
||||
- **デプロイ**: ワンクリックでのインストール、設定、実行、および管理スクリプト
|
||||
|
||||
## 長期記憶
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260203000455.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
## タスク計画とツール
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260202181130.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260203121008.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260202195402.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
## Skill システム
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260202202247.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260202213219.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260202234350.png" width="750" />
|
||||
</Frame>
|
||||
|
||||
## コントリビューション
|
||||
|
||||
[フィードバックの送信](https://github.com/zhayujie/chatgpt-on-wechat/issues) や [コードのコントリビューション](https://github.com/zhayujie/chatgpt-on-wechat/pulls) を歓迎します。
|
||||
36
docs/ja/releases/v2.0.1.mdx
Normal file
36
docs/ja/releases/v2.0.1.mdx
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: v2.0.1
|
||||
description: CowAgent 2.0.1 - 組み込み Web Search、スマートコンテキスト管理、複数の修正
|
||||
---
|
||||
|
||||
**リリース日**: 2026.02.27 | [全変更履歴](https://github.com/zhayujie/chatgpt-on-wechat/compare/2.0.0..2.0.1)
|
||||
|
||||
## 新機能
|
||||
|
||||
- **組み込み Web Search ツール**: Web 検索を Agent の組み込みツールとして統合し、判断コストを削減 ([4f0ea5d](https://github.com/zhayujie/chatgpt-on-wechat/commit/4f0ea5d7568d61db91ff69c91c429e785fd1b1c2))
|
||||
- **Claude Opus 4.6 モデル対応**: Claude Opus 4.6 モデルのサポートを追加 ([#2661](https://github.com/zhayujie/chatgpt-on-wechat/pull/2661))
|
||||
- **企业微信の画像認識**: 企业微信チャネルでの画像メッセージ認識をサポート ([#2667](https://github.com/zhayujie/chatgpt-on-wechat/pull/2667))
|
||||
|
||||
## 改善
|
||||
|
||||
- **スマートコンテキスト管理**: インテリジェントなコンテキストトリミング戦略により、チャットコンテキストのオーバーフローを解決し、トークン制限超過を防止 ([cea7fb7](https://github.com/zhayujie/chatgpt-on-wechat/commit/cea7fb7490c53454602bf05955a0e9f059bcf0fd), [8acf2db](https://github.com/zhayujie/chatgpt-on-wechat/commit/8acf2dbdfe713b84ad74b761b7f86674b1c1904d)) [#2663](https://github.com/zhayujie/chatgpt-on-wechat/issues/2663)
|
||||
- **ランタイム情報の動的更新**: 動的関数によるシステムプロンプト内のタイムスタンプおよびその他のランタイム情報の自動更新 ([#2655](https://github.com/zhayujie/chatgpt-on-wechat/pull/2655), [#2657](https://github.com/zhayujie/chatgpt-on-wechat/pull/2657))
|
||||
- **Skill プロンプトの最適化**: Skill システムプロンプト生成を改善し、ツールの説明を簡素化して Agent のパフォーマンスを向上 ([6c21833](https://github.com/zhayujie/chatgpt-on-wechat/commit/6c218331b1f1208ea8be6bf226936d3b556ade3e))
|
||||
- **GLM カスタム API Base URL**: GLM モデルのカスタム API Base URL をサポート ([#2660](https://github.com/zhayujie/chatgpt-on-wechat/pull/2660))
|
||||
- **起動スクリプトの最適化**: `run.sh` スクリプトのインタラクションと設定フローを改善 ([#2656](https://github.com/zhayujie/chatgpt-on-wechat/pull/2656))
|
||||
- **判断ステップのログ記録**: デバッグ用の Agent 判断ステップログを追加 ([cb303e6](https://github.com/zhayujie/chatgpt-on-wechat/commit/cb303e6109c50c8dfef1f5e6c1ec47223bf3cd11))
|
||||
|
||||
## バグ修正
|
||||
|
||||
- **Scheduler の記憶喪失**: Scheduler ディスパッチャーによる記憶喪失を修正 ([a77a874](https://github.com/zhayujie/chatgpt-on-wechat/commit/a77a8741b500a408c6f5c8868856fb4b018fe9db))
|
||||
- **空のツール呼び出しと長い結果**: 空のツール呼び出しおよび過度に長いツール結果の処理を修正 ([0542700](https://github.com/zhayujie/chatgpt-on-wechat/commit/0542700f9091ebb08c1a56103b0f0f45f24aa621))
|
||||
- **OpenAI Function Call**: OpenAI モデルとの Function Call 互換性を修正 ([158c87a](https://github.com/zhayujie/chatgpt-on-wechat/commit/158c87ab8b05bae054cc1b4eacdbb64fc1062ba9))
|
||||
- **Claude ツール名フィールド**: Claude モデルのレスポンスから余分なツール名フィールドを削除 ([eec10cb](https://github.com/zhayujie/chatgpt-on-wechat/commit/eec10cb5db6a3d5bc12ef606606532237d2c5f6e))
|
||||
- **MiniMax 推論**: MiniMax モデルの推論コンテンツ処理を最適化し、思考プロセスの出力を非表示化 ([c72cda3](https://github.com/zhayujie/chatgpt-on-wechat/commit/c72cda33864bd1542012ee6e0a8bd8c6c88cb5ed), [72b1cac](https://github.com/zhayujie/chatgpt-on-wechat/commit/72b1cacea1ba0d1f3dedacbab2e088e98fd7e172))
|
||||
- **GLM 思考プロセス**: GLM モデルの思考プロセス表示を非表示化 ([72b1cac](https://github.com/zhayujie/chatgpt-on-wechat/commit/72b1cacea1ba0d1f3dedacbab2e088e98fd7e172))
|
||||
- **飞书の接続と SSL**: 飞书チャネルの SSL 証明書エラーおよび接続問題を修正 ([229b14b](https://github.com/zhayujie/chatgpt-on-wechat/commit/229b14b6fcabe7123d53cab1dea39f38dab26d6d), [8674421](https://github.com/zhayujie/chatgpt-on-wechat/commit/867442155e7f095b4f38b0856f8c1d8312b5fcf7))
|
||||
- **model_type バリデーション**: 非文字列の `model_type` による `AttributeError` を修正 ([#2666](https://github.com/zhayujie/chatgpt-on-wechat/pull/2666))
|
||||
|
||||
## プラットフォーム互換性
|
||||
|
||||
- **Windows 互換性**: 複数のツールモジュールにおける Windows でのパス処理、ファイルエンコーディング、および `os.getuid()` の利用不可問題を修正 ([051ffd7](https://github.com/zhayujie/chatgpt-on-wechat/commit/051ffd78a372f71a967fd3259e37fe19131f83cf), [5264f7c](https://github.com/zhayujie/chatgpt-on-wechat/commit/5264f7ce18360ee4db5dcb4ebe67307977d40014))
|
||||
98
docs/ja/releases/v2.0.2.mdx
Normal file
98
docs/ja/releases/v2.0.2.mdx
Normal file
@@ -0,0 +1,98 @@
|
||||
---
|
||||
title: v2.0.2
|
||||
description: CowAgent 2.0.2 - Web Console アップグレード、マルチチャネル同時実行、セッション永続化
|
||||
---
|
||||
|
||||
**リリース日**: 2026.02.27 | [全変更履歴](https://github.com/zhayujie/chatgpt-on-wechat/compare/2.0.1...master)
|
||||
|
||||
## ハイライト
|
||||
|
||||
### 🖥️ Web Console アップグレード
|
||||
|
||||
Web Console が全面的にアップグレードされ、ストリーミング会話出力、ツール実行と推論プロセスの視覚的表示、**モデル、Skill、記憶、チャネル、Agent 設定** のオンライン管理が可能になりました。
|
||||
|
||||
#### チャットインターフェース
|
||||
|
||||
ストリーミング出力に対応し、Agent の推論プロセスとツール呼び出しをリアルタイムに表示することで、Agent の意思決定を直感的に観察できます:
|
||||
|
||||
<img width="850" src="https://cdn.link-ai.tech/doc/20260227180120.png" />
|
||||
|
||||
#### モデル管理
|
||||
|
||||
設定ファイルを手動で編集せずに、モデル設定をオンラインで管理できます:
|
||||
|
||||
<img width="850" src="https://cdn.link-ai.tech/doc/20260227173811.png" />
|
||||
|
||||
#### Skill 管理
|
||||
|
||||
Agent の Skill をオンラインで表示・管理できます:
|
||||
|
||||
<img width="850" src="https://cdn.link-ai.tech/doc/20260227173403.png" />
|
||||
|
||||
#### 記憶管理
|
||||
|
||||
Agent の記憶をオンラインで表示・管理できます:
|
||||
|
||||
<img width="850" src="https://cdn.link-ai.tech/doc/20260227173349.png" />
|
||||
|
||||
#### チャネル管理
|
||||
|
||||
接続されたチャネルをオンラインで管理し、リアルタイムで接続・切断操作ができます:
|
||||
|
||||
<img width="850" src="https://cdn.link-ai.tech/doc/20260227173331.png" />
|
||||
|
||||
#### スケジュールタスク
|
||||
|
||||
ワンタイムタスク、固定間隔、Cron 式を含むスケジュールタスクをオンラインで表示・管理できます:
|
||||
|
||||
<img width="850" src="https://cdn.link-ai.tech/doc/20260227173704.png" />
|
||||
|
||||
#### ログ
|
||||
|
||||
Agent のランタイムログをリアルタイムで表示し、監視とトラブルシューティングに活用できます:
|
||||
|
||||
<img width="850" src="https://cdn.link-ai.tech/doc/20260227173514.png" />
|
||||
|
||||
関連コミット: [f1a1413](https://github.com/zhayujie/chatgpt-on-wechat/commit/f1a1413), [c0702c8](https://github.com/zhayujie/chatgpt-on-wechat/commit/c0702c8), [394853c](https://github.com/zhayujie/chatgpt-on-wechat/commit/394853c), [1c71c4e](https://github.com/zhayujie/chatgpt-on-wechat/commit/1c71c4e), [5e3eccb](https://github.com/zhayujie/chatgpt-on-wechat/commit/5e3eccb), [e1dc037](https://github.com/zhayujie/chatgpt-on-wechat/commit/e1dc037), [5edbf4c](https://github.com/zhayujie/chatgpt-on-wechat/commit/5edbf4c), [7d258b5](https://github.com/zhayujie/chatgpt-on-wechat/commit/7d258b5)
|
||||
|
||||
### 🔀 マルチチャネル同時実行
|
||||
|
||||
複数のチャネル(例:飞书、钉钉、企业微信、Web)を同時に実行できるようになりました。各チャネルは独立したスレッドで動作し、互いに干渉しません。
|
||||
|
||||
設定方法: `config.json` の `channel_type` にカンマ区切りで複数のチャネルを設定するか、Web Console のチャネル管理ページからリアルタイムでチャネルの接続・切断を行います。
|
||||
|
||||
```json
|
||||
{
|
||||
"channel_type": "web,feishu,dingtalk"
|
||||
}
|
||||
```
|
||||
|
||||
関連コミット: [4694594](https://github.com/zhayujie/chatgpt-on-wechat/commit/4694594), [7cce224](https://github.com/zhayujie/chatgpt-on-wechat/commit/7cce224), [7d258b5](https://github.com/zhayujie/chatgpt-on-wechat/commit/7d258b5), [c9adddb](https://github.com/zhayujie/chatgpt-on-wechat/commit/c9adddb)
|
||||
|
||||
### 💾 セッション永続化
|
||||
|
||||
セッション履歴がローカルの SQLite データベースに永続化されるようになりました。サービス再起動後も会話コンテキストが自動的に復元されます。Web Console の過去の会話も復元されます。
|
||||
|
||||
関連コミット: [29bfbec](https://github.com/zhayujie/chatgpt-on-wechat/commit/29bfbec), [9917552](https://github.com/zhayujie/chatgpt-on-wechat/commit/9917552), [925d728](https://github.com/zhayujie/chatgpt-on-wechat/commit/925d728)
|
||||
|
||||
## 新モデル
|
||||
|
||||
- **Gemini 3.1 Pro Preview**: `gemini-3.1-pro-preview` モデルのサポートを追加 ([52d7cad](https://github.com/zhayujie/chatgpt-on-wechat/commit/52d7cad))
|
||||
- **Claude 4.6 Sonnet**: `claude-4.6-sonnet` モデルのサポートを追加 ([52d7cad](https://github.com/zhayujie/chatgpt-on-wechat/commit/52d7cad))
|
||||
- **Qwen3.5 Plus**: `qwen3.5-plus` モデルのサポートを追加 ([e59a289](https://github.com/zhayujie/chatgpt-on-wechat/commit/e59a289))
|
||||
- **MiniMax M2.5**: `Minimax-M2.5` モデルのサポートを追加 ([48db538](https://github.com/zhayujie/chatgpt-on-wechat/commit/48db538))
|
||||
- **GLM-5**: `glm-5` モデルのサポートを追加 ([48db538](https://github.com/zhayujie/chatgpt-on-wechat/commit/48db538))
|
||||
- **Kimi K2.5**: `kimi-k2.5` モデルのサポートを追加 ([48db538](https://github.com/zhayujie/chatgpt-on-wechat/commit/48db538))
|
||||
- **Doubao 2.0 Code**: コーディング特化型 `doubao-2.0-code` モデルを追加 ([ab28ee5](https://github.com/zhayujie/chatgpt-on-wechat/commit/ab28ee5))
|
||||
- **DashScope モデル**: 阿里云 DashScope モデル名のサポートを追加 ([ce58f23](https://github.com/zhayujie/chatgpt-on-wechat/commit/ce58f23))
|
||||
|
||||
## ウェブサイトとドキュメント
|
||||
|
||||
- **公式サイト**: [cowagent.ai](https://cowagent.ai/)
|
||||
- **ドキュメント**: [docs.cowagent.ai](https://docs.cowagent.ai/)
|
||||
|
||||
## バグ修正
|
||||
|
||||
- **Gemini 钉钉画像認識**: 钉钉チャネルで Gemini が画像マーカーを処理できない問題を修正 ([05a3304](https://github.com/zhayujie/chatgpt-on-wechat/commit/05a3304)) ([#2670](https://github.com/zhayujie/chatgpt-on-wechat/pull/2670)) Thanks [@SgtPepper114](https://github.com/SgtPepper114)
|
||||
- **起動スクリプトの依存関係**: `run.sh` スクリプトの依存関係インストール問題を修正 ([b6fc9fa](https://github.com/zhayujie/chatgpt-on-wechat/commit/b6fc9fa))
|
||||
- **bare except の整理**: より適切な例外処理のため `bare except` を `except Exception` に置換 ([adca89b](https://github.com/zhayujie/chatgpt-on-wechat/commit/adca89b)) ([#2674](https://github.com/zhayujie/chatgpt-on-wechat/pull/2674)) Thanks [@haosenwang1018](https://github.com/haosenwang1018)
|
||||
91
docs/ja/releases/v2.0.3.mdx
Normal file
91
docs/ja/releases/v2.0.3.mdx
Normal file
@@ -0,0 +1,91 @@
|
||||
---
|
||||
title: v2.0.3
|
||||
description: CowAgent 2.0.3 - 企業微信スマートボットとQQチャネルの追加、Webコンソールファイル処理、メモリシステムのアップグレード
|
||||
---
|
||||
|
||||
## 🔌 新規チャネル
|
||||
|
||||
### 企業微信スマートボット
|
||||
|
||||
企業微信スマートボット(`wecom_bot`)チャネルを追加しました。ストリーミングカードメッセージ出力、テキストと画像メッセージの送受信をサポートし、Webコンソールでチャネルの設定と管理が可能です。
|
||||
|
||||
接続ドキュメント:[企業微信スマートボット接続](https://docs.cowagent.ai/channels/wecom-bot)。
|
||||
|
||||
関連コミット:[d4480b6](https://github.com/zhayujie/chatgpt-on-wechat/commit/d4480b6), [a42f31f](https://github.com/zhayujie/chatgpt-on-wechat/commit/a42f31f), [4ecd4df](https://github.com/zhayujie/chatgpt-on-wechat/commit/4ecd4df), [8b45d6c](https://github.com/zhayujie/chatgpt-on-wechat/commit/8b45d6c)
|
||||
|
||||
### QQ チャネル
|
||||
|
||||
QQ 公式ボット(`qq`)チャネルを追加しました。テキストと画像メッセージの送受信をサポートし、プライベートチャットとグループチャットに対応しています。
|
||||
|
||||
接続ドキュメント:[QQボット接続](https://docs.cowagent.ai/channels/qq)。
|
||||
|
||||
関連コミット:[005a0e1](https://github.com/zhayujie/chatgpt-on-wechat/commit/005a0e1), [a4d54f5](https://github.com/zhayujie/chatgpt-on-wechat/commit/a4d54f5)
|
||||
|
||||
## 🖥️ Web コンソールのファイル入力・処理対応
|
||||
|
||||
Web コンソールのチャット画面でファイルや画像のアップロードが可能になり、Agent に直接ファイルを送信して処理できます。また、Read ツールに Office ドキュメント(Word、Excel、PPT)の解析機能を追加しました。
|
||||
|
||||
関連コミット:[30c6d9b](https://github.com/zhayujie/chatgpt-on-wechat/commit/30c6d9b)
|
||||
|
||||
## 🤖 新規モデル
|
||||
|
||||
- **GPT-5.4 シリーズ**:`gpt-5.4`、`gpt-5.4-mini`、`gpt-5.4-nano` モデルのサポートを追加 ([1623deb](https://github.com/zhayujie/chatgpt-on-wechat/commit/1623deb))
|
||||
- **Gemini 3.1 Flash Lite Preview**:`gemini-3.1-flash-lite-preview` モデルのサポートを追加 ([ba915f2](https://github.com/zhayujie/chatgpt-on-wechat/commit/ba915f2))
|
||||
|
||||
## 💰 Coding Plan サポート
|
||||
|
||||
各ベンダーの Coding Plan(プログラミング月額プラン)への接続をサポートしました。OpenAI 互換方式で統一的に接続できます。現在、阿里雲、MiniMax、智譜 GLM、Kimi、火山エンジンなどのベンダーに対応しています。
|
||||
|
||||
詳細設定は [Coding Plan ドキュメント](https://docs.cowagent.ai/models/coding-plan) を参照してください。
|
||||
|
||||
## 🧠 メモリシステムのアップグレード
|
||||
|
||||
メモリ書き込み(Memory Flush)のアップグレード:
|
||||
|
||||
- LLM を使用してコンテキストウィンドウを超えた会話内容をインテリジェントに要約し、精製された日次メモリエントリを生成
|
||||
- 要約はバックグラウンドスレッドで非同期実行され、応答をブロックしない
|
||||
- コンテキストの一括トリミング戦略を最適化し、フラッシュ頻度を低減
|
||||
- 日次定期フラッシュのフォールバック機能を追加し、低アクティビティシナリオでのメモリ損失を防止
|
||||
- コンテキストメモリの損失問題を修正
|
||||
|
||||
関連コミット:[022c13f](https://github.com/zhayujie/chatgpt-on-wechat/commit/022c13f), [c116235](https://github.com/zhayujie/chatgpt-on-wechat/commit/c116235)
|
||||
|
||||
## 🔧 ツールリファクタリング
|
||||
|
||||
- **画像認識**:画像認識(Image Vision)を Skill から内蔵 Tool にリファクタリングし、独立した画像ビジョンプロバイダー(Vision Provider)設定を追加。安定性と保守性を向上 ([a50fafa](https://github.com/zhayujie/chatgpt-on-wechat/commit/a50fafa), [3b8b562](https://github.com/zhayujie/chatgpt-on-wechat/commit/3b8b562))
|
||||
- **Webスクレイピング**:Webスクレイピング(Web Fetch)を Skill から内蔵 Tool にリファクタリング。リモートドキュメントファイル(PDF、Word、Excel、PPT)のダウンロードと解析をサポート ([ccb9030](https://github.com/zhayujie/chatgpt-on-wechat/commit/ccb9030), [fa61744](https://github.com/zhayujie/chatgpt-on-wechat/commit/fa61744))
|
||||
|
||||
## 🐳 Docker デプロイメントの最適化
|
||||
|
||||
- **設定テンプレートの整合**:`docker-compose.yml` の環境変数を `config-template.json` と整合し、モデル API Key と Agent 設定項目を完備
|
||||
- **Web コンソールポートマッピング**:`9899` ポートマッピングを追加。Docker デプロイ後にブラウザから Web コンソールにアクセス可能
|
||||
- **設定のホットリロード**:各モデル Bot の API Key と API Base をリアルタイム読み込みに変更。Web コンソールで設定変更後、再起動不要で即時反映
|
||||
- **ワークスペースの永続化**:`./cow` Volume マウントを追加。Agent ワークスペースデータ(メモリ、ペルソナ、スキルなど)をホストマシンに永続化し、コンテナの再構築やアップグレードでデータが失われない
|
||||
|
||||
## ⚡ パフォーマンス最適化
|
||||
|
||||
- **起動高速化**:飛書チャネルで依存関係の遅延読み込みを採用し、4-10秒の起動遅延を回避 ([924dc79](https://github.com/zhayujie/chatgpt-on-wechat/commit/924dc79))
|
||||
- **チャネルの安定性**:チャネル接続の安定性を最適化し、環境変数によるチャネル設定をサポート ([f1c04bc](https://github.com/zhayujie/chatgpt-on-wechat/commit/f1c04bc), [46d97fd](https://github.com/zhayujie/chatgpt-on-wechat/commit/46d97fd))
|
||||
|
||||
## 🐛 バグ修正
|
||||
|
||||
- **bot_type 設定**:Agent モードでの `bot_type` 設定の受け渡し問題を修正 ([#2691](https://github.com/zhayujie/chatgpt-on-wechat/pull/2691)) Thanks [@Weikjssss](https://github.com/Weikjssss)
|
||||
- **bot_type 優先順位**:Agent モードでの `bot_type` の解析優先順位を調整 ([#2692](https://github.com/zhayujie/chatgpt-on-wechat/pull/2692)) Thanks [@6vision](https://github.com/6vision)
|
||||
- **智譜モデル設定**:智譜の `bot_type` 命名、Web コンソールの永続化、正規表現エスケープの問題を修正 ([#2693](https://github.com/zhayujie/chatgpt-on-wechat/pull/2693)) Thanks [@6vision](https://github.com/6vision)
|
||||
- **OpenAI 互換レイヤー**:`openai_compat` レイヤーによる統一エラー処理 ([#2688](https://github.com/zhayujie/chatgpt-on-wechat/pull/2688)) Thanks [@JasonOA888](https://github.com/JasonOA888)
|
||||
- **OpenAI 互換移行**:全モデル Bot の `openai_compat` 移行を完了 ([#2689](https://github.com/zhayujie/chatgpt-on-wechat/pull/2689))
|
||||
- **Gemini ツール呼び出し**:Gemini モデルのツール呼び出しマッチング問題を修正 ([eda82ba](https://github.com/zhayujie/chatgpt-on-wechat/commit/eda82ba))
|
||||
- **セッション並行処理**:セッション並行シナリオでの競合条件の問題を修正 ([9879878](https://github.com/zhayujie/chatgpt-on-wechat/commit/9879878))
|
||||
- **履歴メッセージの復元**:履歴セッションメッセージの不完全な問題を修正。user/assistant のテキストメッセージのみを復元し、ツール呼び出しを除外 ([b788a3d](https://github.com/zhayujie/chatgpt-on-wechat/commit/b788a3d), [a33ce97](https://github.com/zhayujie/chatgpt-on-wechat/commit/a33ce97))
|
||||
- **飛書グループチャット**:飛書グループチャットシナリオでの `bot_name` 依存を削除 ([b641bff](https://github.com/zhayujie/chatgpt-on-wechat/commit/b641bff))
|
||||
- **Safari 互換性**:Safari ブラウザでの IME Enter キーによるメッセージ誤送信の問題を修正 ([0687916](https://github.com/zhayujie/chatgpt-on-wechat/commit/0687916))
|
||||
- **Windows 互換性**:Windows での bash スタイル `$VAR` 環境変数を `%VAR%` に変換する問題を修正 ([7c67513](https://github.com/zhayujie/chatgpt-on-wechat/commit/7c67513))
|
||||
- **MiniMax パラメータ**:MiniMax モデルの `max_tokens` 制限を追加 ([1767413](https://github.com/zhayujie/chatgpt-on-wechat/commit/1767413))
|
||||
- **.gitignore 更新**:Python ディレクトリの無視ルールを追加 ([#2683](https://github.com/zhayujie/chatgpt-on-wechat/pull/2683)) Thanks [@pelioo](https://github.com/pelioo)
|
||||
- **AGENT.md の能動的進化**:システムプロンプトでの AGENT.md 更新ガイダンスを最適化。受動的な「ユーザーが変更した時に更新」から、会話中の性格やスタイルの変化を能動的に検出して自動更新するように改善
|
||||
|
||||
## 📦 アップグレード方法
|
||||
|
||||
ソースコードデプロイの場合は `./run.sh update` でワンクリックアップグレードできます。または手動でコードをプルして再起動してください。詳細は [アップデートドキュメント](https://docs.cowagent.ai/guide/upgrade) を参照。
|
||||
|
||||
**リリース日**:2026.03.18 | [Full Changelog](https://github.com/zhayujie/chatgpt-on-wechat/compare/2.0.2...master)
|
||||
55
docs/ja/releases/v2.0.4.mdx
Normal file
55
docs/ja/releases/v2.0.4.mdx
Normal file
@@ -0,0 +1,55 @@
|
||||
---
|
||||
title: v2.0.4
|
||||
description: CowAgent 2.0.4 - 個人WeChat チャネルの追加、新モデルサポート、日本語ドキュメント、スクリプトリファクタリングおよび複数の修正
|
||||
---
|
||||
|
||||
## 🔌 個人WeChat チャネルの追加
|
||||
|
||||
個人WeChat(`weixin`)チャネルを追加しました。本バージョンの最も重要なアップデートです。QRコードをスキャンするだけで CowAgent を個人WeChatに接続でき、以下の機能をサポートします:
|
||||
|
||||
- **メッセージ送受信**:テキスト、画像、ファイル、動画メッセージの送受信、音声メッセージの受信をサポート
|
||||
- **QRコードログイン**:ターミナルにQRコードを表示、WeChatでスキャンして確認するだけでログイン完了。QRコード期限切れ時は自動更新
|
||||
- **認証情報の永続化**:ログイン認証情報を `~/.weixin_cow_credentials.json` に自動保存、再起動時に再スキャン不要
|
||||
- **Session 自動再接続**:Session 期限切れ時に旧認証情報を自動クリアし、QRコードログインを再開
|
||||
- **Web コンソール接続**:Web コンソールからWeChatチャネルを追加可能、QRコードログインフローを同期表示
|
||||
- **Docker・スクリプト対応**:`run.sh` と `docker-compose.yml` がWeChat チャネルに対応
|
||||
|
||||
接続ドキュメント:[WeChat 接続](https://docs.cowagent.ai/channels/weixin)。
|
||||
|
||||
関連コミット:[ce89869](https://github.com/zhayujie/chatgpt-on-wechat/commit/ce89869), [a483ec0](https://github.com/zhayujie/chatgpt-on-wechat/commit/a483ec0), [c1421e0](https://github.com/zhayujie/chatgpt-on-wechat/commit/c1421e0)
|
||||
|
||||
## 🤖 新規モデル
|
||||
|
||||
- **MiniMax-M2.7**:MiniMax-M2.7 モデルのサポートを追加
|
||||
- **GLM-5-Turbo**:智譜 glm-5-turbo モデルのサポートを追加
|
||||
|
||||
関連コミット:[9192f6f](https://github.com/zhayujie/chatgpt-on-wechat/commit/9192f6f)
|
||||
|
||||
## 🔧 スクリプトリファクタリング
|
||||
|
||||
- **run.sh リファクタリング**:共通ロジックを抽出し、大量の重複コードを削除。スクリプトの行数を 600+ 行から 177 行に圧縮 ([49d8707](https://github.com/zhayujie/chatgpt-on-wechat/commit/49d8707))
|
||||
- **実行権限**:`run.sh` ファイルの権限問題を修正 ([652156e](https://github.com/zhayujie/chatgpt-on-wechat/commit/652156e))
|
||||
|
||||
## ⚡ 最適化
|
||||
|
||||
- **リクエストヘッダー統一**:Agent の各サービス(Chat、Embedding、Vision、WebSearch 等)の外部リクエストに統一的な識別ヘッダーを追加 ([b4e711f](https://github.com/zhayujie/chatgpt-on-wechat/commit/b4e711f))
|
||||
- **メッセージ自動修復**:メッセージプロトコルのフォールトトレランスを強化し、フォーマット異常なメッセージシーケンスを自動修復 ([b8b57e3](https://github.com/zhayujie/chatgpt-on-wechat/commit/b8b57e3))
|
||||
|
||||
## 🌍 日本語ドキュメント
|
||||
|
||||
完全な日本語ドキュメントを追加しました。入門ガイド、チャネル接続、モデル設定などの主要セクションをカバーしています。Thanks [@Ikko Ashimine](https://github.com/ikoamu)
|
||||
|
||||
関連コミット:[5487c0b](https://github.com/zhayujie/chatgpt-on-wechat/commit/5487c0b)
|
||||
|
||||
## 🐛 バグ修正
|
||||
|
||||
- **企業微信ボット互換性**:旧バージョンの `websocket-client` との互換性問題を修正し、統一的な WebSocket 互換レイヤーを追加 ([bc7f627](https://github.com/zhayujie/chatgpt-on-wechat/commit/bc7f627))
|
||||
- **run.sh PID 取得**:`run.sh` でのプロセス PID 取得エラーを修正 ([9febb07](https://github.com/zhayujie/chatgpt-on-wechat/commit/9febb07))
|
||||
- **飛書エンコーディング**:飛書チャネルのメッセージとログのエンコーディング問題を修正 ([7d0e156](https://github.com/zhayujie/chatgpt-on-wechat/commit/7d0e156))
|
||||
- **飛書設定**:`run.sh` での `feishu_bot_name` への冗長な依存を削除 ([1b5be1b](https://github.com/zhayujie/chatgpt-on-wechat/commit/1b5be1b))
|
||||
|
||||
## 📦 アップグレード方法
|
||||
|
||||
ソースコードデプロイの場合は `./run.sh update` でワンクリックアップグレードできます。または手動でコードをプルして再起動してください。詳細は [アップデートドキュメント](https://docs.cowagent.ai/guide/upgrade) を参照。
|
||||
|
||||
**リリース日**:2026.03.22 | [Full Changelog](https://github.com/zhayujie/chatgpt-on-wechat/compare/2.0.3...master)
|
||||
31
docs/ja/skills/image-vision.mdx
Normal file
31
docs/ja/skills/image-vision.mdx
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
title: Image Vision
|
||||
description: OpenAI の Vision モデルを使用して画像を認識
|
||||
---
|
||||
|
||||
OpenAI の GPT-4 Vision API を使用して画像の内容を分析し、画像内のオブジェクト、テキスト、色などの要素を理解します。
|
||||
|
||||
## 依存関係
|
||||
|
||||
| 依存関係 | 説明 |
|
||||
| --- | --- |
|
||||
| `OPENAI_API_KEY` | OpenAI API キー |
|
||||
| `curl`, `base64` | システムコマンド(通常プリインストール済み) |
|
||||
|
||||
設定方法:
|
||||
|
||||
- `env_config` Tool で `OPENAI_API_KEY` を設定
|
||||
- または `config.json` で `open_ai_api_key` を設定
|
||||
|
||||
## 対応モデル
|
||||
|
||||
- `gpt-4.1-mini`(推奨、コストパフォーマンスに優れる)
|
||||
- `gpt-4.1`
|
||||
|
||||
## 使い方
|
||||
|
||||
設定が完了したら、Agent に画像を送信すると自動的に画像認識がトリガーされます。
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260202213219.png" width="800" />
|
||||
</Frame>
|
||||
67
docs/ja/skills/index.mdx
Normal file
67
docs/ja/skills/index.mdx
Normal file
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: Skill 概要
|
||||
description: CowAgent の Skill システム紹介
|
||||
---
|
||||
|
||||
Skill は Agent に無限の拡張性を提供します。各 Skill は説明ファイル(`SKILL.md`)、実行スクリプト(任意)、リソース(任意)で構成され、特定のタスクをどのように遂行するかを記述します。
|
||||
|
||||
Skill と Tool の違い:Tool はコードで実装された原子的な操作(例:ファイルの読み書き、コマンドの実行)であるのに対し、Skill は説明ファイルに基づく高レベルなワークフローであり、複数の Tool を組み合わせて複雑なタスクを完遂できます。
|
||||
|
||||
## 組み込み Skill
|
||||
|
||||
プロジェクトの `skills/` ディレクトリに配置されており、依存条件に基づいて自動的に有効化されます:
|
||||
|
||||
| Skill | 説明 | 依存関係 |
|
||||
| --- | --- | --- |
|
||||
| [`skill-creator`](/ja/skills/skill-creator) | 会話を通じてカスタム Skill を作成 | なし |
|
||||
| [`openai-image-vision`](/ja/skills/image-vision) | OpenAI の Vision モデルを使用して画像を認識 | `OPENAI_API_KEY` |
|
||||
| [`linkai-agent`](/ja/skills/linkai-agent) | LinkAI プラットフォームの Agent を統合 | `LINKAI_API_KEY` |
|
||||
| [`web-fetch`](/ja/skills/web-fetch) | Web ページのテキストコンテンツを取得 | `curl`(デフォルトで有効) |
|
||||
|
||||
## カスタム Skill
|
||||
|
||||
ユーザーが会話を通じて作成し、ワークスペース(`~/cow/skills/`)に保存されます。任意の複雑なビジネスプロセスやサードパーティシステムとの連携を実装できます。
|
||||
|
||||
## Skill の読み込み優先順位
|
||||
|
||||
1. **ワークスペースの Skill**(最高優先):`~/cow/skills/`
|
||||
2. **プロジェクト組み込み Skill**(最低優先):`skills/`
|
||||
|
||||
同名の Skill は優先順位に従って上書きされます。
|
||||
|
||||
## Skill のファイル構成
|
||||
|
||||
```
|
||||
skills/
|
||||
├── my-skill/
|
||||
│ ├── SKILL.md # Skill の説明(frontmatter + 手順)
|
||||
│ ├── scripts/ # 実行スクリプト(任意)
|
||||
│ └── resources/ # 追加リソース(任意)
|
||||
```
|
||||
|
||||
### SKILL.md のフォーマット
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: my-skill
|
||||
description: Brief description of the skill
|
||||
metadata:
|
||||
emoji: 🔧
|
||||
requires:
|
||||
bins: ["curl"]
|
||||
env: ["MY_API_KEY"]
|
||||
primaryEnv: "MY_API_KEY"
|
||||
---
|
||||
|
||||
# My Skill
|
||||
|
||||
Detailed instructions...
|
||||
```
|
||||
|
||||
| フィールド | 説明 |
|
||||
| --- | --- |
|
||||
| `name` | Skill 名。ディレクトリ名と一致する必要があります |
|
||||
| `description` | Skill の説明。Agent はこれに基づいて呼び出すかどうかを判断します |
|
||||
| `metadata.requires.bins` | 必要なシステムコマンド |
|
||||
| `metadata.requires.env` | 必要な環境変数 |
|
||||
| `metadata.always` | 常に読み込む(デフォルトは false) |
|
||||
47
docs/ja/skills/linkai-agent.mdx
Normal file
47
docs/ja/skills/linkai-agent.mdx
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
title: LinkAI Agent
|
||||
description: LinkAI プラットフォームのマルチ Agent Skill を統合
|
||||
---
|
||||
|
||||
[LinkAI](https://link-ai.tech/) プラットフォームの Agent を Skill として使用し、マルチ Agent の意思決定を行います。Agent は Agent 名と説明に基づいてインテリジェントに選択し、`app_code` を通じて対応するアプリケーションやワークフローを呼び出します。
|
||||
|
||||
## 依存関係
|
||||
|
||||
| 依存関係 | 説明 |
|
||||
| --- | --- |
|
||||
| `LINKAI_API_KEY` | LinkAI プラットフォームの API キー。[コンソール](https://link-ai.tech/console/interface)で作成 |
|
||||
| `curl` | システムコマンド(通常プリインストール済み) |
|
||||
|
||||
設定方法:
|
||||
|
||||
- `env_config` Tool で `LINKAI_API_KEY` を設定
|
||||
- または `config.json` で `linkai_api_key` を設定
|
||||
|
||||
## Agent の設定
|
||||
|
||||
`skills/linkai-agent/config.json` で利用可能な Agent を追加します:
|
||||
|
||||
```json
|
||||
{
|
||||
"apps": [
|
||||
{
|
||||
"app_code": "G7z6vKwp",
|
||||
"app_name": "LinkAI Customer Support",
|
||||
"app_description": "Select this assistant only when the user needs help with LinkAI platform questions"
|
||||
},
|
||||
{
|
||||
"app_code": "SFY5x7JR",
|
||||
"app_name": "Content Creator",
|
||||
"app_description": "Use this assistant only when the user needs to create images or videos"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 使い方
|
||||
|
||||
設定が完了すると、Agent はユーザーの質問に基づいて適切な LinkAI Agent を自動的に選択します。
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260202234350.png" width="750" />
|
||||
</Frame>
|
||||
31
docs/ja/skills/skill-creator.mdx
Normal file
31
docs/ja/skills/skill-creator.mdx
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
title: Skill Creator
|
||||
description: 会話を通じてカスタム Skill を作成
|
||||
---
|
||||
|
||||
自然言語の会話を通じて、Skill の作成、インストール、更新を素早く行えます。
|
||||
|
||||
## 依存関係
|
||||
|
||||
追加の依存関係は不要で、常に利用可能です。
|
||||
|
||||
## 使い方
|
||||
|
||||
- ワークフローを Skill 化:「このデプロイプロセスから Skill を作成して」
|
||||
- サードパーティ API の統合:「この API ドキュメントに基づいて Skill を作成して」
|
||||
- リモート Skill のインストール:「xxx Skill をインストールして」
|
||||
|
||||
## 作成フロー
|
||||
|
||||
1. 作成したい Skill を Agent に伝えます
|
||||
2. Agent が自動的に `SKILL.md` の説明と実行スクリプトを生成します
|
||||
3. Skill はワークスペースの `~/cow/skills/` ディレクトリに保存されます
|
||||
4. 以降の会話で Agent が自動的にその Skill を認識し使用します
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260202202247.png" width="800" />
|
||||
</Frame>
|
||||
|
||||
<Tip>
|
||||
詳細は [Skill Creator のドキュメント](https://github.com/zhayujie/chatgpt-on-wechat/blob/master/skills/skill-creator/SKILL.md)をご覧ください。
|
||||
</Tip>
|
||||
31
docs/ja/skills/web-fetch.mdx
Normal file
31
docs/ja/skills/web-fetch.mdx
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
title: Web Fetch
|
||||
description: Web ページのテキストコンテンツを取得
|
||||
---
|
||||
|
||||
curl を使用して Web ページを取得し、読み取り可能なテキストコンテンツを抽出します。ブラウザ自動化を必要としない軽量な Web アクセス方法です。
|
||||
|
||||
## 依存関係
|
||||
|
||||
| 依存関係 | 説明 |
|
||||
| --- | --- |
|
||||
| `curl` | システムコマンド(通常プリインストール済み) |
|
||||
|
||||
この Skill は `always: true` が設定されており、システムに `curl` コマンドがあればデフォルトで有効になります。
|
||||
|
||||
## 使い方
|
||||
|
||||
Agent が URL からコンテンツを取得する必要がある場合に自動的に呼び出されます。追加の設定は不要です。
|
||||
|
||||
## browser Tool との比較
|
||||
|
||||
| 機能 | web-fetch (Skill) | browser (Tool) |
|
||||
| --- | --- | --- |
|
||||
| 依存関係 | curl のみ | browser-use + playwright |
|
||||
| JS レンダリング | 非対応 | 対応 |
|
||||
| ページ操作 | 非対応 | クリック、入力などに対応 |
|
||||
| 最適な用途 | 静的ページのテキスト | 動的な Web ページ |
|
||||
|
||||
<Tip>
|
||||
ほとんどの Web コンテンツ取得シナリオでは、web-fetch で十分です。JS レンダリングやページ操作が必要な場合にのみ browser Tool を使用してください。
|
||||
</Tip>
|
||||
28
docs/ja/tools/bash.mdx
Normal file
28
docs/ja/tools/bash.mdx
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
title: bash - ターミナル
|
||||
description: システムコマンドの実行
|
||||
---
|
||||
|
||||
現在の作業ディレクトリでBashコマンドを実行し、stdoutとstderrを返します。`env_config` で設定されたAPIキーは自動的に環境変数に注入されます。
|
||||
|
||||
## 依存関係
|
||||
|
||||
追加の依存関係は不要で、デフォルトで利用可能です。
|
||||
|
||||
## パラメータ
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `command` | string | はい | 実行するコマンド |
|
||||
| `timeout` | integer | いいえ | タイムアウト(秒) |
|
||||
|
||||
## ユースケース
|
||||
|
||||
- パッケージや依存関係のインストール
|
||||
- コードやテストの実行
|
||||
- アプリケーションやサービスのデプロイ(Nginx設定、プロセス管理など)
|
||||
- システム管理とトラブルシューティング
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260203121008.png" width="800" />
|
||||
</Frame>
|
||||
25
docs/ja/tools/browser.mdx
Normal file
25
docs/ja/tools/browser.mdx
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
title: browser - ブラウザ
|
||||
description: Webページへのアクセスと操作
|
||||
---
|
||||
|
||||
ブラウザを使用してWebページにアクセス・操作します。JavaScriptでレンダリングされる動的ページにも対応しています。
|
||||
|
||||
## 依存関係
|
||||
|
||||
| 依存関係 | インストールコマンド |
|
||||
| --- | --- |
|
||||
| `browser-use` ≥ 0.1.40 | `pip install browser-use` |
|
||||
| `markdownify` | `pip install markdownify` |
|
||||
| `playwright` + chromium | `pip install playwright && playwright install chromium` |
|
||||
|
||||
## ユースケース
|
||||
|
||||
- 特定のURLにアクセスしてページ内容を取得
|
||||
- Webページの要素を操作(クリック、入力など)
|
||||
- デプロイされたWebページの検証
|
||||
- JSレンダリングが必要な動的コンテンツのスクレイピング
|
||||
|
||||
<Note>
|
||||
ブラウザToolは依存関係が大きいため、不要な場合はインストールを省略できます。軽量なWebコンテンツ取得には、代わりに `web-fetch` Skillをご利用ください。
|
||||
</Note>
|
||||
24
docs/ja/tools/edit.mdx
Normal file
24
docs/ja/tools/edit.mdx
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
title: edit - ファイル編集
|
||||
description: テキスト置換によるファイル編集
|
||||
---
|
||||
|
||||
テキスト置換によるファイル編集を行います。`oldText` が空の場合、ファイル末尾に追記します。
|
||||
|
||||
## 依存関係
|
||||
|
||||
追加の依存関係は不要で、デフォルトで利用可能です。
|
||||
|
||||
## パラメータ
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `path` | string | はい | ファイルパス |
|
||||
| `oldText` | string | はい | 置換対象の元テキスト(空の場合は追記) |
|
||||
| `newText` | string | はい | 置換後のテキスト |
|
||||
|
||||
## ユースケース
|
||||
|
||||
- 設定ファイルの特定パラメータの変更
|
||||
- コードのバグ修正
|
||||
- ファイル内の特定位置へのコンテンツ挿入
|
||||
36
docs/ja/tools/env-config.mdx
Normal file
36
docs/ja/tools/env-config.mdx
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: env_config - 環境設定
|
||||
description: APIキーとシークレットの管理
|
||||
---
|
||||
|
||||
ワークスペースの `.env` ファイルで環境変数(APIキーやシークレット)を管理し、会話形式で安全に更新できます。セキュリティ保護とマスキング機能を内蔵しています。
|
||||
|
||||
## 依存関係
|
||||
|
||||
| 依存関係 | インストールコマンド |
|
||||
| --- | --- |
|
||||
| `python-dotenv` ≥ 1.0.0 | `pip install python-dotenv>=1.0.0` |
|
||||
|
||||
オプション依存関係のインストールに含まれています:`pip3 install -r requirements-optional.txt`
|
||||
|
||||
## パラメータ
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `action` | string | はい | 操作タイプ:`get`、`set`、`list`、`delete` |
|
||||
| `key` | string | いいえ | 環境変数名 |
|
||||
| `value` | string | いいえ | 環境変数の値(`set` の場合のみ) |
|
||||
|
||||
## 使い方
|
||||
|
||||
設定したいキーをAgentに伝えると、自動的にこのToolが呼び出されます:
|
||||
|
||||
- 「BOCHA_API_KEYを設定して」
|
||||
- 「OPENAI_API_KEYをsk-xxxに設定して」
|
||||
- 「設定済みの環境変数を表示して」
|
||||
|
||||
設定されたキーは `bash` Toolの実行環境に自動的に注入されます。
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260202234939.png" width="800" />
|
||||
</Frame>
|
||||
50
docs/ja/tools/index.mdx
Normal file
50
docs/ja/tools/index.mdx
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: Tools 概要
|
||||
description: CowAgent 組み込みToolシステム
|
||||
---
|
||||
|
||||
Toolは、AgentがOSリソースにアクセスするための中核機能です。Agentはタスクの要件に基づいてToolをインテリジェントに選択・呼び出し、ファイル操作、コマンド実行、Web検索、スケジュールタスクなどを実行します。Toolは `agent/tools/` ディレクトリに実装されています。
|
||||
|
||||
## 組み込みTool
|
||||
|
||||
以下のToolは追加設定なしでデフォルトで利用可能です:
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="read - ファイル読み取り" icon="file" href="/ja/tools/read">
|
||||
ファイル内容を読み取り、テキスト・画像・PDFに対応
|
||||
</Card>
|
||||
<Card title="write - ファイル書き込み" icon="pen" href="/ja/tools/write">
|
||||
ファイルの作成または上書き
|
||||
</Card>
|
||||
<Card title="edit - ファイル編集" icon="pen-to-square" href="/ja/tools/edit">
|
||||
テキスト置換によるファイル編集
|
||||
</Card>
|
||||
<Card title="ls - ディレクトリ一覧" icon="folder-open" href="/ja/tools/ls">
|
||||
ディレクトリの内容を一覧表示
|
||||
</Card>
|
||||
<Card title="bash - ターミナル" icon="terminal" href="/ja/tools/bash">
|
||||
システムコマンドの実行
|
||||
</Card>
|
||||
<Card title="send - ファイル送信" icon="paper-plane" href="/ja/tools/send">
|
||||
ファイルや画像をユーザーに送信
|
||||
</Card>
|
||||
<Card title="memory - メモリ" icon="brain" href="/ja/tools/memory">
|
||||
長期メモリの検索と読み取り
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## オプションTool
|
||||
|
||||
以下のToolは追加の依存関係またはAPIキーの設定が必要です:
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="env_config - 環境設定" icon="key" href="/ja/tools/env-config">
|
||||
APIキーとシークレットの管理
|
||||
</Card>
|
||||
<Card title="scheduler - スケジューラ" icon="clock" href="/ja/tools/scheduler">
|
||||
スケジュールタスクの作成と管理
|
||||
</Card>
|
||||
<Card title="web_search - Web検索" icon="magnifying-glass" href="/ja/tools/web-search">
|
||||
インターネットからリアルタイム情報を検索
|
||||
</Card>
|
||||
</CardGroup>
|
||||
23
docs/ja/tools/ls.mdx
Normal file
23
docs/ja/tools/ls.mdx
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
title: ls - ディレクトリ一覧
|
||||
description: ディレクトリの内容を一覧表示
|
||||
---
|
||||
|
||||
ディレクトリの内容をアルファベット順にソートして一覧表示します。ディレクトリには `/` が付与され、隠しファイルも含まれます。
|
||||
|
||||
## 依存関係
|
||||
|
||||
追加の依存関係は不要で、デフォルトで利用可能です。
|
||||
|
||||
## パラメータ
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `path` | string | はい | ディレクトリパス。相対パスはワークスペースディレクトリを基準とします |
|
||||
| `limit` | integer | いいえ | 返すエントリの最大数、デフォルト500 |
|
||||
|
||||
## ユースケース
|
||||
|
||||
- プロジェクト構造の閲覧
|
||||
- 特定ファイルの検索
|
||||
- ディレクトリの存在確認
|
||||
36
docs/ja/tools/memory.mdx
Normal file
36
docs/ja/tools/memory.mdx
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: memory - メモリ
|
||||
description: 長期メモリの検索と読み取り
|
||||
---
|
||||
|
||||
メモリToolには `memory_search`(メモリ検索)と `memory_get`(メモリファイル読み取り)の2つのサブToolがあります。
|
||||
|
||||
## 依存関係
|
||||
|
||||
追加の依存関係は不要で、デフォルトで利用可能です。Agent Coreのメモリシステムによって管理されます。
|
||||
|
||||
## memory_search
|
||||
|
||||
キーワードとベクトルのハイブリッド検索で過去のメモリを検索します。
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `query` | string | はい | 検索クエリ |
|
||||
|
||||
## memory_get
|
||||
|
||||
特定のメモリファイルの内容を読み取ります。
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `path` | string | はい | メモリファイルの相対パス(例:`MEMORY.md`、`memory/2026-01-01.md`) |
|
||||
| `start_line` | integer | いいえ | 開始行番号 |
|
||||
| `end_line` | integer | いいえ | 終了行番号 |
|
||||
|
||||
## 仕組み
|
||||
|
||||
Agentは以下のシナリオでメモリToolを自動的に呼び出します:
|
||||
|
||||
- ユーザーが重要な情報を共有した場合 → メモリに保存
|
||||
- 過去のコンテキストが必要な場合 → 関連するメモリを検索
|
||||
- 会話が一定の長さに達した場合 → 要約を抽出して保存
|
||||
24
docs/ja/tools/read.mdx
Normal file
24
docs/ja/tools/read.mdx
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
title: read - ファイル読み取り
|
||||
description: ファイル内容の読み取り
|
||||
---
|
||||
|
||||
ファイルの内容を読み取ります。テキストファイル、PDFファイル、画像(メタデータを返す)などに対応しています。
|
||||
|
||||
## 依存関係
|
||||
|
||||
追加の依存関係は不要で、デフォルトで利用可能です。
|
||||
|
||||
## パラメータ
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `path` | string | はい | ファイルパス。相対パスはワークスペースディレクトリを基準とします |
|
||||
| `offset` | integer | いいえ | 開始行番号(1始まり)。負の値は末尾からの読み取り |
|
||||
| `limit` | integer | いいえ | 読み取る行数 |
|
||||
|
||||
## ユースケース
|
||||
|
||||
- 設定ファイルやログファイルの閲覧
|
||||
- コードファイルの読み取りと分析
|
||||
- 画像・動画ファイルの情報確認
|
||||
40
docs/ja/tools/scheduler.mdx
Normal file
40
docs/ja/tools/scheduler.mdx
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: scheduler - スケジューラ
|
||||
description: スケジュールタスクの作成と管理
|
||||
---
|
||||
|
||||
柔軟なスケジュール設定と実行モードを備えた、動的スケジュールタスクの作成と管理を行います。
|
||||
|
||||
## 依存関係
|
||||
|
||||
| 依存関係 | インストールコマンド |
|
||||
| --- | --- |
|
||||
| `croniter` ≥ 2.0.0 | `pip install croniter>=2.0.0` |
|
||||
|
||||
コア依存関係に含まれています:`pip3 install -r requirements.txt`
|
||||
|
||||
## スケジュールモード
|
||||
|
||||
| モード | 説明 |
|
||||
| --- | --- |
|
||||
| ワンタイム | 指定した時刻に1回だけ実行 |
|
||||
| 固定間隔 | 一定の時間間隔で繰り返し実行 |
|
||||
| Cron式 | Cron構文を使用した複雑なスケジュール定義 |
|
||||
|
||||
## 実行モード
|
||||
|
||||
- **固定メッセージ**: トリガー時にプリセットメッセージを送信
|
||||
- **Agent動的タスク**: トリガー時にAgentがインテリジェントにタスクを実行
|
||||
|
||||
## 使い方
|
||||
|
||||
自然言語でスケジュールタスクを作成・管理できます:
|
||||
|
||||
- 「毎朝9時に天気予報を送って」
|
||||
- 「2時間ごとにサーバーのステータスを確認して」
|
||||
- 「明日の午後3時に会議のリマインドをして」
|
||||
- 「すべてのスケジュールタスクを表示して」
|
||||
|
||||
<Frame>
|
||||
<img src="https://cdn.link-ai.tech/doc/20260202195402.png" width="800" />
|
||||
</Frame>
|
||||
23
docs/ja/tools/send.mdx
Normal file
23
docs/ja/tools/send.mdx
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
title: send - ファイル送信
|
||||
description: ユーザーへのファイル送信
|
||||
---
|
||||
|
||||
ユーザーにファイル(画像、動画、音声、ドキュメントなど)を送信します。ユーザーが明示的にファイルの送信・共有を要求した場合に使用されます。
|
||||
|
||||
## 依存関係
|
||||
|
||||
追加の依存関係は不要で、デフォルトで利用可能です。
|
||||
|
||||
## パラメータ
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `path` | string | はい | ファイルパス。絶対パスまたはワークスペースからの相対パス |
|
||||
| `message` | string | いいえ | 添付メッセージ |
|
||||
|
||||
## ユースケース
|
||||
|
||||
- 生成したコードやドキュメントをユーザーに送信
|
||||
- スクリーンショットやチャートの送信
|
||||
- ダウンロードしたファイルの共有
|
||||
32
docs/ja/tools/web-search.mdx
Normal file
32
docs/ja/tools/web-search.mdx
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: web_search - Web検索
|
||||
description: インターネットからリアルタイム情報を検索
|
||||
---
|
||||
|
||||
インターネットからリアルタイムの情報、ニュース、リサーチなどを検索します。2つの検索バックエンドに対応し、自動フォールバック機能を備えています。
|
||||
|
||||
## 依存関係
|
||||
|
||||
少なくとも1つの検索APIキーが必要です(`env_config` Toolまたはワークスペースの `.env` ファイルで設定):
|
||||
|
||||
| バックエンド | 環境変数 | 優先度 | 取得方法 |
|
||||
| --- | --- | --- | --- |
|
||||
| Bocha Search | `BOCHA_API_KEY` | プライマリ | [Bocha Open Platform](https://open.bochaai.com/) |
|
||||
| LinkAI Search | `LINKAI_API_KEY` | フォールバック | [LinkAI Console](https://link-ai.tech/console/interface) |
|
||||
|
||||
## パラメータ
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `query` | string | はい | 検索キーワード |
|
||||
| `count` | integer | いいえ | 結果件数(1-50、デフォルト10) |
|
||||
| `freshness` | string | いいえ | 期間指定:`noLimit`、`oneDay`、`oneWeek`、`oneMonth`、`oneYear`、または `2025-01-01..2025-02-01` のような日付範囲 |
|
||||
| `summary` | boolean | いいえ | ページ要約を返す(デフォルトfalse) |
|
||||
|
||||
## ユースケース
|
||||
|
||||
ユーザーが最新情報について質問したり、事実確認やリアルタイムデータが必要な場合、AgentはこのToolを自動的に呼び出します。
|
||||
|
||||
<Note>
|
||||
検索APIキーが設定されていない場合、このToolは読み込まれません。
|
||||
</Note>
|
||||
27
docs/ja/tools/write.mdx
Normal file
27
docs/ja/tools/write.mdx
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
title: write - ファイル書き込み
|
||||
description: ファイルの作成または上書き
|
||||
---
|
||||
|
||||
ファイルにコンテンツを書き込みます。ファイルが存在しない場合は新規作成し、存在する場合は上書きします。親ディレクトリは自動的に作成されます。
|
||||
|
||||
## 依存関係
|
||||
|
||||
追加の依存関係は不要で、デフォルトで利用可能です。
|
||||
|
||||
## パラメータ
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `path` | string | はい | ファイルパス |
|
||||
| `content` | string | はい | 書き込む内容 |
|
||||
|
||||
## ユースケース
|
||||
|
||||
- 新しいコードファイルやスクリプトの作成
|
||||
- 設定ファイルの生成
|
||||
- 処理結果の保存
|
||||
|
||||
<Note>
|
||||
1回の書き込みは10KBを超えないようにしてください。大きなファイルの場合は、まずスケルトンを作成し、editツールを使用してチャンクごとにコンテンツを追加してください。
|
||||
</Note>
|
||||
@@ -5,14 +5,14 @@ description: 智谱AI GLM 模型配置
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "glm-5",
|
||||
"model": "glm-5-turbo",
|
||||
"zhipu_ai_api_key": "YOUR_API_KEY"
|
||||
}
|
||||
```
|
||||
|
||||
| 参数 | 说明 |
|
||||
| --- | --- |
|
||||
| `model` | 可填 `glm-5`、`glm-4.7`、`glm-4-plus`、`glm-4-flash`、`glm-4-air` 等,参考 [模型编码](https://bigmodel.cn/dev/api/normal-model/glm-4) |
|
||||
| `model` | 可填 `glm-5-turbo`、`glm-5`、`glm-4.7`、`glm-4-plus`、`glm-4-flash`、`glm-4-air` 等,参考 [模型编码](https://bigmodel.cn/dev/api/normal-model/glm-4) |
|
||||
| `zhipu_ai_api_key` | 在 [智谱AI 控制台](https://www.bigmodel.cn/usercenter/proj-mgmt/apikeys) 创建 |
|
||||
|
||||
也支持 OpenAI 兼容方式接入:
|
||||
@@ -20,7 +20,7 @@ description: 智谱AI GLM 模型配置
|
||||
```json
|
||||
{
|
||||
"bot_type": "openai",
|
||||
"model": "glm-5",
|
||||
"model": "glm-5-turbo",
|
||||
"open_ai_api_base": "https://open.bigmodel.cn/api/paas/v4",
|
||||
"open_ai_api_key": "YOUR_API_KEY"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ description: CowAgent 支持的模型及推荐选择
|
||||
CowAgent 支持国内外主流厂商的大语言模型,模型接口实现在项目的 `models/` 目录下。
|
||||
|
||||
<Note>
|
||||
Agent 模式下推荐使用以下模型,可根据效果及成本综合选择:MiniMax-M2.5、glm-5、kimi-k2.5、qwen3.5-plus、claude-sonnet-4-6、gemini-3.1-pro-preview
|
||||
Agent 模式下推荐使用以下模型,可根据效果及成本综合选择:MiniMax-M2.7、glm-5-turbo、kimi-k2.5、qwen3.5-plus、claude-sonnet-4-6、gemini-3.1-pro-preview
|
||||
</Note>
|
||||
|
||||
## 配置方式
|
||||
@@ -23,10 +23,10 @@ CowAgent 支持国内外主流厂商的大语言模型,模型接口实现在
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="MiniMax" href="/models/minimax">
|
||||
MiniMax-M2.5 等系列模型
|
||||
MiniMax-M2.7 等系列模型
|
||||
</Card>
|
||||
<Card title="智谱 GLM" href="/models/glm">
|
||||
glm-5、glm-4.7 等系列模型
|
||||
glm-5-turbo、glm-5 等系列模型
|
||||
</Card>
|
||||
<Card title="通义千问 Qwen" href="/models/qwen">
|
||||
qwen3.5-plus、qwen3-max 等
|
||||
|
||||
@@ -5,14 +5,14 @@ description: MiniMax 模型配置
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "MiniMax-M2.5",
|
||||
"model": "MiniMax-M2.7",
|
||||
"minimax_api_key": "YOUR_API_KEY"
|
||||
}
|
||||
```
|
||||
|
||||
| 参数 | 说明 |
|
||||
| --- | --- |
|
||||
| `model` | 可填 `MiniMax-M2.5`、`MiniMax-M2.1`、`MiniMax-M2.1-lightning`、`MiniMax-M2` 等 |
|
||||
| `model` | 可填 `MiniMax-M2.7`、`MiniMax-M2.5`、`MiniMax-M2.1`、`MiniMax-M2.1-lightning`、`MiniMax-M2` 等 |
|
||||
| `minimax_api_key` | 在 [MiniMax 控制台](https://platform.minimaxi.com/user-center/basic-information/interface-key) 创建 |
|
||||
|
||||
也支持 OpenAI 兼容方式接入:
|
||||
@@ -20,7 +20,7 @@ description: MiniMax 模型配置
|
||||
```json
|
||||
{
|
||||
"bot_type": "openai",
|
||||
"model": "MiniMax-M2.5",
|
||||
"model": "MiniMax-M2.7",
|
||||
"open_ai_api_base": "https://api.minimaxi.com/v1",
|
||||
"open_ai_api_key": "YOUR_API_KEY"
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ description: CowAgent 版本更新历史
|
||||
|
||||
| 版本 | 日期 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| [2.0.4](/releases/v2.0.4) | 2026.03.22 | 新增个人微信通道、新模型支持、日文文档、脚本重构及多项修复 |
|
||||
| [2.0.3](/releases/v2.0.3) | 2026.03.18 | 新增企微智能机器人和 QQ 通道、支持Coding Plan、新增多个模型、Web端文件处理、记忆系统升级 |
|
||||
| [2.0.2](/releases/v2.0.2) | 2026.02.27 | Web 控制台升级、多通道同时运行、会话持久化 |
|
||||
| [2.0.1](/releases/v2.0.1) | 2026.02.13 | 内置 Web Search 工具、智能上下文管理、多项修复 |
|
||||
|
||||
51
docs/releases/v2.0.4.mdx
Normal file
51
docs/releases/v2.0.4.mdx
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
title: v2.0.4
|
||||
description: CowAgent 2.0.4 - 新增个人微信通道、新模型支持、日文文档、脚本重构及多项修复
|
||||
---
|
||||
|
||||
## 🔌 新增个人微信通道
|
||||
|
||||
新增个人微信(`weixin`)通道,微信扫描二维码即可将 CowAgent 接入个人微信,支持以下功能:
|
||||
|
||||
- **消息收发**:支持文本、图片、文件、视频消息的接收与回复,支持语音消息接收和识别
|
||||
- **扫码登录**:终端显示二维码,微信扫码确认即可登录,二维码过期自动刷新
|
||||
- **凭证持久化**:登录凭证自动保存至 `~/.weixin_cow_credentials.json`,重启无需重新扫码
|
||||
- **Session 自动重连**:Session 过期后自动清除旧凭证并重新发起扫码登录
|
||||
- **Web 控制台接入**:支持在 Web 控制台中添加微信通道,扫码登录流程同步展示
|
||||
- **Docker 和脚本支持**:`run.sh` 和 `docker-compose.yml` 均已适配微信通道
|
||||
|
||||
接入文档:[微信接入](https://docs.cowagent.ai/channels/weixin)。
|
||||
|
||||
相关提交:[ce89869](https://github.com/zhayujie/chatgpt-on-wechat/commit/ce89869)
|
||||
|
||||
## 🤖 新增模型
|
||||
|
||||
- **MiniMax-M2.7**:新增 MiniMax-M2.7 模型支持
|
||||
- **GLM-5-Turbo**:新增智谱 glm-5-turbo 模型支持
|
||||
|
||||
相关提交:[9192f6f](https://github.com/zhayujie/chatgpt-on-wechat/commit/9192f6f)
|
||||
|
||||
## 🔧 脚本重构
|
||||
|
||||
- **run.sh 重构**:提取公共逻辑,精简脚本代码([49d8707](https://github.com/zhayujie/chatgpt-on-wechat/commit/49d8707))
|
||||
- **可执行权限**:修复 `run.sh` 文件权限问题 ([652156e](https://github.com/zhayujie/chatgpt-on-wechat/commit/652156e))
|
||||
- **PID 获取**:修复 `run.sh` 中进程 PID 获取错误的问题 ([9febb07](https://github.com/zhayujie/chatgpt-on-wechat/commit/9febb07))
|
||||
|
||||
## 🌍 文档更新
|
||||
|
||||
新增完整的日文文档,覆盖入门指南、通道接入、模型配置等主要章节。Thanks [@Ikko Ashimine](https://github.com/ikoamu)
|
||||
|
||||
相关提交:[5487c0b](https://github.com/zhayujie/chatgpt-on-wechat/commit/5487c0b)
|
||||
|
||||
## 🐛 问题修复
|
||||
|
||||
- **企微机器人兼容**:修复旧版 `websocket-client` 的兼容性问题,新增统一的 WebSocket 兼容层 ([bc7f627](https://github.com/zhayujie/chatgpt-on-wechat/commit/bc7f627))
|
||||
- **消息自动修复**:增强消息协议的容错能力,自动修复格式异常的消息序列 ([b8b57e3](https://github.com/zhayujie/chatgpt-on-wechat/commit/b8b57e3))
|
||||
- **飞书编码**:修复飞书通道消息和日志的编码问题 ([7d0e156](https://github.com/zhayujie/chatgpt-on-wechat/commit/7d0e156))
|
||||
- **飞书配置**:移除 `run.sh` 中对 `feishu_bot_name` 的冗余依赖 ([1b5be1b](https://github.com/zhayujie/chatgpt-on-wechat/commit/1b5be1b))
|
||||
|
||||
## 📦 升级方式
|
||||
|
||||
源码部署可执行 `./run.sh update` 一键升级,或手动拉取代码后重启。详见 [更新升级文档](https://docs.cowagent.ai/guide/upgrade)。
|
||||
|
||||
**发布日期**:2026.03.22 | [Full Changelog](https://github.com/zhayujie/chatgpt-on-wechat/compare/2.0.3...master)
|
||||
@@ -534,6 +534,7 @@ def _linkai_call_with_tools(self, messages, tools=None, stream=False, **kwargs):
|
||||
else:
|
||||
channel_type = raw_ct
|
||||
|
||||
session_id = kwargs.get("session_id", "")
|
||||
body = {
|
||||
"messages": messages,
|
||||
"model": kwargs.get("model", conf().get("model") or "gpt-3.5-turbo"),
|
||||
@@ -543,12 +544,22 @@ def _linkai_call_with_tools(self, messages, tools=None, stream=False, **kwargs):
|
||||
"presence_penalty": kwargs.get("presence_penalty", conf().get("presence_penalty", 0.0)),
|
||||
"stream": stream,
|
||||
"channel_type": kwargs.get("channel_type", channel_type),
|
||||
"session_id": session_id,
|
||||
"sender_id": session_id,
|
||||
}
|
||||
|
||||
try:
|
||||
from linkai import LinkAIClient
|
||||
client_id = LinkAIClient.fetch_client_id()
|
||||
if client_id:
|
||||
body["client_id"] = client_id
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if tools:
|
||||
body["tools"] = tools
|
||||
body["tool_choice"] = kwargs.get("tool_choice", "auto")
|
||||
|
||||
|
||||
# Prepare headers
|
||||
headers = {"Authorization": "Bearer " + conf().get("linkai_api_key")}
|
||||
base_url = conf().get("linkai_api_base", "https://api.link-ai.tech")
|
||||
|
||||
@@ -9,6 +9,7 @@ agentmesh-sdk>=0.1.3
|
||||
python-dotenv>=1.0.0
|
||||
PyYAML>=6.0
|
||||
croniter>=2.0.0
|
||||
qrcode
|
||||
|
||||
# wechatcom & wechatmp
|
||||
wechatpy
|
||||
@@ -23,5 +24,5 @@ lark-oapi
|
||||
# dingtalk
|
||||
dingtalk_stream
|
||||
# wecom bot websocket mode
|
||||
websocket-client
|
||||
websocket-client>=1.4.0
|
||||
pycryptodome
|
||||
|
||||
661
run.sh
Normal file → Executable file
661
run.sh
Normal file → Executable file
@@ -9,7 +9,6 @@ set -e
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
@@ -159,20 +158,18 @@ clone_project() {
|
||||
|
||||
if ! command -v git &> /dev/null; then
|
||||
echo -e "${YELLOW}⚠️ Git not available. Trying wget/curl...${NC}"
|
||||
local zip_url="https://gitee.com/zhayujie/chatgpt-on-wechat/repository/archive/master.zip"
|
||||
if command -v wget &> /dev/null; then
|
||||
wget https://gitee.com/zhayujie/chatgpt-on-wechat/repository/archive/master.zip -O chatgpt-on-wechat.zip
|
||||
unzip chatgpt-on-wechat.zip
|
||||
mv chatgpt-on-wechat-master chatgpt-on-wechat
|
||||
rm chatgpt-on-wechat.zip
|
||||
wget "$zip_url" -O chatgpt-on-wechat.zip
|
||||
elif command -v curl &> /dev/null; then
|
||||
curl -L https://gitee.com/zhayujie/chatgpt-on-wechat/repository/archive/master.zip -o chatgpt-on-wechat.zip
|
||||
unzip chatgpt-on-wechat.zip
|
||||
mv chatgpt-on-wechat-master chatgpt-on-wechat
|
||||
rm chatgpt-on-wechat.zip
|
||||
curl -L "$zip_url" -o chatgpt-on-wechat.zip
|
||||
else
|
||||
echo -e "${RED}❌ Cannot download project. Please install Git, wget, or curl.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
unzip chatgpt-on-wechat.zip
|
||||
mv chatgpt-on-wechat-master chatgpt-on-wechat
|
||||
rm chatgpt-on-wechat.zip
|
||||
else
|
||||
git clone https://github.com/zhayujie/chatgpt-on-wechat.git || \
|
||||
git clone https://gitee.com/zhayujie/chatgpt-on-wechat.git
|
||||
@@ -198,69 +195,52 @@ clone_project() {
|
||||
# Install dependencies
|
||||
install_dependencies() {
|
||||
echo -e "${GREEN}📦 Installing dependencies...${NC}"
|
||||
|
||||
# For Python 3.11+, use --break-system-packages to avoid externally-managed-environment errors
|
||||
local PIP_MIRROR="-i https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
|
||||
PIP_EXTRA_ARGS=""
|
||||
if $PYTHON_CMD -c "import sys; exit(0 if sys.version_info >= (3, 11) else 1)" 2>/dev/null; then
|
||||
PIP_EXTRA_ARGS="--break-system-packages"
|
||||
echo -e "${YELLOW}Python 3.11+ detected, using --break-system-packages for pip installations${NC}"
|
||||
fi
|
||||
|
||||
# Upgrade pip and basic tools (ignore existing system packages to avoid conflicts)
|
||||
|
||||
echo -e "${YELLOW}Upgrading pip and basic tools...${NC}"
|
||||
set +e
|
||||
$PYTHON_CMD -m pip install --upgrade pip setuptools wheel importlib_metadata --ignore-installed $PIP_EXTRA_ARGS -i https://pypi.tuna.tsinghua.edu.cn/simple > /tmp/pip_upgrade.log 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${YELLOW}⚠️ Some tools failed to upgrade, but continuing...${NC}"
|
||||
cat /tmp/pip_upgrade.log | head -20
|
||||
fi
|
||||
$PYTHON_CMD -m pip install --upgrade pip setuptools wheel importlib_metadata --ignore-installed $PIP_EXTRA_ARGS $PIP_MIRROR > /tmp/pip_upgrade.log 2>&1
|
||||
[ $? -ne 0 ] && echo -e "${YELLOW}⚠️ Some tools failed to upgrade, but continuing...${NC}"
|
||||
set -e
|
||||
rm -f /tmp/pip_upgrade.log
|
||||
|
||||
# Common packages that may have distutils/system conflicts
|
||||
COMMON_CONFLICT_PACKAGES="PyYAML setuptools wheel certifi charset-normalizer"
|
||||
|
||||
# Try normal installation first
|
||||
|
||||
echo -e "${YELLOW}Installing project dependencies...${NC}"
|
||||
|
||||
# Save output and capture exit code correctly
|
||||
set +e # Temporarily disable exit on error
|
||||
$PYTHON_CMD -m pip install -r requirements.txt $PIP_EXTRA_ARGS -i https://pypi.tuna.tsinghua.edu.cn/simple > /tmp/pip_install.log 2>&1
|
||||
INSTALL_EXIT_CODE=$?
|
||||
set -e # Re-enable exit on error
|
||||
|
||||
# Show output
|
||||
set +e
|
||||
$PYTHON_CMD -m pip install -r requirements.txt $PIP_EXTRA_ARGS $PIP_MIRROR > /tmp/pip_install.log 2>&1
|
||||
local exit_code=$?
|
||||
set -e
|
||||
cat /tmp/pip_install.log
|
||||
|
||||
if [ $INSTALL_EXIT_CODE -eq 0 ]; then
|
||||
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
echo -e "${GREEN}✅ Dependencies installed successfully.${NC}"
|
||||
elif grep -qE "distutils installed project|uninstall-no-record-file|installed by debian" /tmp/pip_install.log; then
|
||||
echo -e "${YELLOW}⚠️ Detected system package conflict, retrying with workaround...${NC}"
|
||||
local IGNORE_PACKAGES=""
|
||||
for pkg in PyYAML setuptools wheel certifi charset-normalizer; do
|
||||
IGNORE_PACKAGES="$IGNORE_PACKAGES --ignore-installed $pkg"
|
||||
done
|
||||
set +e
|
||||
$PYTHON_CMD -m pip install -r requirements.txt $IGNORE_PACKAGES $PIP_EXTRA_ARGS $PIP_MIRROR \
|
||||
&& echo -e "${GREEN}✅ Dependencies installed successfully (workaround applied).${NC}" \
|
||||
|| echo -e "${YELLOW}⚠️ Some dependencies may have issues, but continuing...${NC}"
|
||||
set -e
|
||||
elif grep -q "externally-managed-environment" /tmp/pip_install.log; then
|
||||
echo -e "${YELLOW}⚠️ Detected externally-managed environment, retrying with --break-system-packages...${NC}"
|
||||
set +e
|
||||
$PYTHON_CMD -m pip install -r requirements.txt --break-system-packages $PIP_MIRROR \
|
||||
&& echo -e "${GREEN}✅ Dependencies installed successfully (system packages override applied).${NC}" \
|
||||
|| echo -e "${YELLOW}⚠️ Some dependencies may have issues, but continuing...${NC}"
|
||||
set -e
|
||||
else
|
||||
# Check if it's a distutils/system package conflict error
|
||||
if grep -qE "distutils installed project|uninstall-no-record-file|installed by debian" /tmp/pip_install.log; then
|
||||
echo -e "${YELLOW}⚠️ Detected system package conflict, retrying with workaround...${NC}"
|
||||
# Only ignore common conflict packages
|
||||
IGNORE_PACKAGES=""
|
||||
for pkg in $COMMON_CONFLICT_PACKAGES; do
|
||||
IGNORE_PACKAGES="$IGNORE_PACKAGES --ignore-installed $pkg"
|
||||
done
|
||||
|
||||
if $PYTHON_CMD -m pip install -r requirements.txt $IGNORE_PACKAGES $PIP_EXTRA_ARGS -i https://pypi.tuna.tsinghua.edu.cn/simple; then
|
||||
echo -e "${GREEN}✅ Dependencies installed successfully (workaround applied).${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Some dependencies may have issues, but continuing...${NC}"
|
||||
fi
|
||||
elif grep -q "externally-managed-environment" /tmp/pip_install.log; then
|
||||
echo -e "${YELLOW}⚠️ Detected externally-managed environment, retrying with --break-system-packages...${NC}"
|
||||
if $PYTHON_CMD -m pip install -r requirements.txt --break-system-packages -i https://pypi.tuna.tsinghua.edu.cn/simple; then
|
||||
echo -e "${GREEN}✅ Dependencies installed successfully (system packages override applied).${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Some dependencies may have issues, but continuing...${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Installation had errors, but continuing...${NC}"
|
||||
fi
|
||||
echo -e "${YELLOW}⚠️ Installation had errors, but continuing...${NC}"
|
||||
fi
|
||||
|
||||
|
||||
rm -f /tmp/pip_install.log
|
||||
}
|
||||
|
||||
@@ -270,8 +250,8 @@ select_model() {
|
||||
echo -e "${CYAN}${BOLD}=========================================${NC}"
|
||||
echo -e "${CYAN}${BOLD} Select AI Model${NC}"
|
||||
echo -e "${CYAN}${BOLD}=========================================${NC}"
|
||||
echo -e "${YELLOW}1) MiniMax (MiniMax-M2.5, MiniMax-M2.1, etc.)${NC}"
|
||||
echo -e "${YELLOW}2) Zhipu AI (glm-5, glm-4.7, etc.)${NC}"
|
||||
echo -e "${YELLOW}1) MiniMax (MiniMax-M2.7, MiniMax-M2.5, etc.)${NC}"
|
||||
echo -e "${YELLOW}2) Zhipu AI (glm-5-turbo, glm-5, etc.)${NC}"
|
||||
echo -e "${YELLOW}3) Kimi (kimi-k2.5, kimi-k2, etc.)${NC}"
|
||||
echo -e "${YELLOW}4) Doubao (doubao-seed-2-0-code-preview-260215, etc.)${NC}"
|
||||
echo -e "${YELLOW}5) Qwen (qwen3.5-plus, qwen3-max, qwq-plus, etc.)${NC}"
|
||||
@@ -295,108 +275,48 @@ select_model() {
|
||||
done
|
||||
}
|
||||
|
||||
# Read model config: provider, default_model, key_variable_name
|
||||
read_model_config() {
|
||||
local provider=$1 default_model=$2 key_var=$3
|
||||
echo -e "${GREEN}Configuring ${provider}...${NC}"
|
||||
read -p "Enter ${provider} API Key: " _api_key
|
||||
read -p "Enter model name [press Enter for default: ${default_model}]: " model_name
|
||||
model_name=${model_name:-$default_model}
|
||||
MODEL_NAME="$model_name"
|
||||
eval "${key_var}=\"\$_api_key\""
|
||||
}
|
||||
|
||||
# Read optional API base URL
|
||||
read_api_base() {
|
||||
local base_var=$1 default_url=$2
|
||||
read -p "Enter API Base URL [press Enter for default: ${default_url}]: " api_base
|
||||
api_base=${api_base:-$default_url}
|
||||
eval "${base_var}=\"\$api_base\""
|
||||
}
|
||||
|
||||
# Configure model
|
||||
configure_model() {
|
||||
case "$model_choice" in
|
||||
1)
|
||||
# MiniMax
|
||||
echo -e "${GREEN}Configuring MiniMax...${NC}"
|
||||
read -p "Enter MiniMax API Key: " minimax_key
|
||||
read -p "Enter model name [press Enter for default: MiniMax-M2.5]: " model_name
|
||||
model_name=${model_name:-MiniMax-M2.5}
|
||||
|
||||
MODEL_NAME="$model_name"
|
||||
MINIMAX_KEY="$minimax_key"
|
||||
;;
|
||||
2)
|
||||
# Zhipu AI
|
||||
echo -e "${GREEN}Configuring Zhipu AI...${NC}"
|
||||
read -p "Enter Zhipu AI API Key: " zhipu_key
|
||||
read -p "Enter model name [press Enter for default: glm-5]: " model_name
|
||||
model_name=${model_name:-glm-5}
|
||||
|
||||
MODEL_NAME="$model_name"
|
||||
ZHIPU_KEY="$zhipu_key"
|
||||
;;
|
||||
3)
|
||||
# Kimi (Moonshot)
|
||||
echo -e "${GREEN}Configuring Kimi (Moonshot)...${NC}"
|
||||
read -p "Enter Moonshot API Key: " moonshot_key
|
||||
read -p "Enter model name [press Enter for default: kimi-k2.5]: " model_name
|
||||
model_name=${model_name:-kimi-k2.5}
|
||||
|
||||
MODEL_NAME="$model_name"
|
||||
MOONSHOT_KEY="$moonshot_key"
|
||||
;;
|
||||
4)
|
||||
# Doubao (Volcengine Ark)
|
||||
echo -e "${GREEN}Configuring Doubao (Volcengine Ark)...${NC}"
|
||||
read -p "Enter Ark API Key: " ark_key
|
||||
read -p "Enter model name [press Enter for default: doubao-seed-2-0-code-preview-260215]: " model_name
|
||||
model_name=${model_name:-doubao-seed-2-0-code-preview-260215}
|
||||
|
||||
MODEL_NAME="$model_name"
|
||||
ARK_KEY="$ark_key"
|
||||
;;
|
||||
5)
|
||||
# Qwen (DashScope)
|
||||
echo -e "${GREEN}Configuring Qwen (DashScope)...${NC}"
|
||||
read -p "Enter DashScope API Key: " dashscope_key
|
||||
read -p "Enter model name [press Enter for default: qwen3.5-plus]: " model_name
|
||||
model_name=${model_name:-qwen3.5-plus}
|
||||
|
||||
MODEL_NAME="$model_name"
|
||||
DASHSCOPE_KEY="$dashscope_key"
|
||||
;;
|
||||
1) read_model_config "MiniMax" "MiniMax-M2.7" "MINIMAX_KEY" ;;
|
||||
2) read_model_config "Zhipu AI" "glm-5-turbo" "ZHIPU_KEY" ;;
|
||||
3) read_model_config "Kimi (Moonshot)" "kimi-k2.5" "MOONSHOT_KEY" ;;
|
||||
4) read_model_config "Doubao (Volcengine Ark)" "doubao-seed-2-0-code-preview-260215" "ARK_KEY" ;;
|
||||
5) read_model_config "Qwen (DashScope)" "qwen3.5-plus" "DASHSCOPE_KEY" ;;
|
||||
6)
|
||||
# Claude
|
||||
echo -e "${GREEN}Configuring Claude...${NC}"
|
||||
read -p "Enter Claude API Key: " claude_key
|
||||
read -p "Enter model name [press Enter for default: claude-sonnet-4-6]: " model_name
|
||||
model_name=${model_name:-claude-sonnet-4-6}
|
||||
read -p "Enter API Base URL [press Enter for default: https://api.anthropic.com/v1]: " api_base
|
||||
api_base=${api_base:-https://api.anthropic.com/v1}
|
||||
|
||||
MODEL_NAME="$model_name"
|
||||
CLAUDE_KEY="$claude_key"
|
||||
CLAUDE_BASE="$api_base"
|
||||
read_model_config "Claude" "claude-sonnet-4-6" "CLAUDE_KEY"
|
||||
read_api_base "CLAUDE_BASE" "https://api.anthropic.com/v1"
|
||||
;;
|
||||
7)
|
||||
# Gemini
|
||||
echo -e "${GREEN}Configuring Gemini...${NC}"
|
||||
read -p "Enter Gemini API Key: " gemini_key
|
||||
read -p "Enter model name [press Enter for default: gemini-3.1-pro-preview]: " model_name
|
||||
model_name=${model_name:-gemini-3.1-pro-preview}
|
||||
read -p "Enter API Base URL [press Enter for default: https://generativelanguage.googleapis.com]: " api_base
|
||||
api_base=${api_base:-https://generativelanguage.googleapis.com}
|
||||
|
||||
MODEL_NAME="$model_name"
|
||||
GEMINI_KEY="$gemini_key"
|
||||
GEMINI_BASE="$api_base"
|
||||
read_model_config "Gemini" "gemini-3.1-pro-preview" "GEMINI_KEY"
|
||||
read_api_base "GEMINI_BASE" "https://generativelanguage.googleapis.com"
|
||||
;;
|
||||
8)
|
||||
# OpenAI
|
||||
echo -e "${GREEN}Configuring OpenAI GPT...${NC}"
|
||||
read -p "Enter OpenAI API Key: " openai_key
|
||||
read -p "Enter model name [press Enter for default: gpt-5.4]: " model_name
|
||||
model_name=${model_name:-gpt-5.4}
|
||||
read -p "Enter API Base URL [press Enter for default: https://api.openai.com/v1]: " api_base
|
||||
api_base=${api_base:-https://api.openai.com/v1}
|
||||
|
||||
MODEL_NAME="$model_name"
|
||||
OPENAI_KEY="$openai_key"
|
||||
OPENAI_BASE="$api_base"
|
||||
read_model_config "OpenAI GPT" "gpt-5.4" "OPENAI_KEY"
|
||||
read_api_base "OPENAI_BASE" "https://api.openai.com/v1"
|
||||
;;
|
||||
9)
|
||||
# LinkAI
|
||||
echo -e "${GREEN}Configuring LinkAI...${NC}"
|
||||
read -p "Enter LinkAI API Key: " linkai_key
|
||||
read -p "Enter model name [press Enter for default: MiniMax-M2.5]: " model_name
|
||||
model_name=${model_name:-MiniMax-M2.5}
|
||||
|
||||
MODEL_NAME="$model_name"
|
||||
read_model_config "LinkAI" "MiniMax-M2.7" "LINKAI_KEY"
|
||||
USE_LINKAI="true"
|
||||
LINKAI_KEY="$linkai_key"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
@@ -407,23 +327,24 @@ select_channel() {
|
||||
echo -e "${CYAN}${BOLD}=========================================${NC}"
|
||||
echo -e "${CYAN}${BOLD} Select Communication Channel${NC}"
|
||||
echo -e "${CYAN}${BOLD}=========================================${NC}"
|
||||
echo -e "${YELLOW}1) Feishu (飞书)${NC}"
|
||||
echo -e "${YELLOW}2) DingTalk (钉钉)${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 -e "${YELLOW}1) Weixin (微信)${NC}"
|
||||
echo -e "${YELLOW}2) Feishu (飞书)${NC}"
|
||||
echo -e "${YELLOW}3) DingTalk (钉钉)${NC}"
|
||||
echo -e "${YELLOW}4) WeCom Bot (企微智能机器人)${NC}"
|
||||
echo -e "${YELLOW}5) QQ (QQ 机器人)${NC}"
|
||||
echo -e "${YELLOW}6) WeCom App (企微自建应用)${NC}"
|
||||
echo -e "${YELLOW}7) Web (网页)${NC}"
|
||||
echo ""
|
||||
|
||||
while true; do
|
||||
read -p "Enter your choice [press Enter for default: 1 - Feishu]: " channel_choice
|
||||
read -p "Enter your choice [press Enter for default: 1 - Weixin]: " channel_choice
|
||||
channel_choice=${channel_choice:-1}
|
||||
case "$channel_choice" in
|
||||
1|2|3|4|5|6)
|
||||
1|2|3|4|5|6|7)
|
||||
break
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Invalid choice. Please enter 1-6.${NC}"
|
||||
echo -e "${RED}Invalid choice. Please enter 1-7.${NC}"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
@@ -433,20 +354,23 @@ select_channel() {
|
||||
configure_channel() {
|
||||
case "$channel_choice" in
|
||||
1)
|
||||
# Weixin
|
||||
CHANNEL_TYPE="weixin"
|
||||
ACCESS_INFO="Weixin channel configured. Scan QR code in terminal or web console to login."
|
||||
;;
|
||||
2)
|
||||
# Feishu (WebSocket mode)
|
||||
CHANNEL_TYPE="feishu"
|
||||
echo -e "${GREEN}Configure Feishu (WebSocket mode)...${NC}"
|
||||
read -p "Enter Feishu App ID: " fs_app_id
|
||||
read -p "Enter Feishu App Secret: " fs_app_secret
|
||||
read -p "Enter Feishu Bot Name: " fs_bot_name
|
||||
|
||||
FEISHU_APP_ID="$fs_app_id"
|
||||
FEISHU_APP_SECRET="$fs_app_secret"
|
||||
FEISHU_BOT_NAME="$fs_bot_name"
|
||||
FEISHU_EVENT_MODE="websocket"
|
||||
ACCESS_INFO="Feishu channel configured (WebSocket mode)"
|
||||
;;
|
||||
2)
|
||||
3)
|
||||
# DingTalk
|
||||
CHANNEL_TYPE="dingtalk"
|
||||
echo -e "${GREEN}Configure DingTalk...${NC}"
|
||||
@@ -457,7 +381,7 @@ configure_channel() {
|
||||
DT_CLIENT_SECRET="$dt_client_secret"
|
||||
ACCESS_INFO="DingTalk channel configured"
|
||||
;;
|
||||
3)
|
||||
4)
|
||||
# WeCom Bot
|
||||
CHANNEL_TYPE="wecom_bot"
|
||||
echo -e "${GREEN}Configure WeCom Bot...${NC}"
|
||||
@@ -468,7 +392,7 @@ configure_channel() {
|
||||
WECOM_BOT_SECRET="$wecom_bot_secret"
|
||||
ACCESS_INFO="WeCom Bot channel configured"
|
||||
;;
|
||||
4)
|
||||
5)
|
||||
# QQ
|
||||
CHANNEL_TYPE="qq"
|
||||
echo -e "${GREEN}Configure QQ Bot...${NC}"
|
||||
@@ -479,7 +403,7 @@ configure_channel() {
|
||||
QQ_APP_SECRET="$qq_app_secret"
|
||||
ACCESS_INFO="QQ Bot channel configured"
|
||||
;;
|
||||
5)
|
||||
6)
|
||||
# WeCom App
|
||||
CHANNEL_TYPE="wechatcom_app"
|
||||
echo -e "${GREEN}Configure WeCom App...${NC}"
|
||||
@@ -499,7 +423,7 @@ configure_channel() {
|
||||
WECHATCOM_PORT="$com_port"
|
||||
ACCESS_INFO="WeCom App channel configured on port ${com_port}"
|
||||
;;
|
||||
6)
|
||||
7)
|
||||
# Web
|
||||
CHANNEL_TYPE="web"
|
||||
read -p "Enter web port [press Enter for default: 9899]: " web_port
|
||||
@@ -514,227 +438,85 @@ configure_channel() {
|
||||
# Generate config file
|
||||
create_config_file() {
|
||||
echo -e "${GREEN}📝 Generating config.json...${NC}"
|
||||
|
||||
# Build JSON based on channel type
|
||||
case "$CHANNEL_TYPE" in
|
||||
feishu)
|
||||
cat > config.json <<EOF
|
||||
{
|
||||
"channel_type": "feishu",
|
||||
"model": "${MODEL_NAME}",
|
||||
"open_ai_api_key": "${OPENAI_KEY:-}",
|
||||
"open_ai_api_base": "${OPENAI_BASE:-https://api.openai.com/v1}",
|
||||
"claude_api_key": "${CLAUDE_KEY:-}",
|
||||
"claude_api_base": "${CLAUDE_BASE:-https://api.anthropic.com/v1}",
|
||||
"gemini_api_key": "${GEMINI_KEY:-}",
|
||||
"gemini_api_base": "${GEMINI_BASE:-https://generativelanguage.googleapis.com}",
|
||||
"zhipu_ai_api_key": "${ZHIPU_KEY:-}",
|
||||
"moonshot_api_key": "${MOONSHOT_KEY:-}",
|
||||
"ark_api_key": "${ARK_KEY:-}",
|
||||
"dashscope_api_key": "${DASHSCOPE_KEY:-}",
|
||||
"minimax_api_key": "${MINIMAX_KEY:-}",
|
||||
"voice_to_text": "openai",
|
||||
"text_to_voice": "openai",
|
||||
"voice_reply_voice": false,
|
||||
"speech_recognition": true,
|
||||
"group_speech_recognition": false,
|
||||
"use_linkai": ${USE_LINKAI:-false},
|
||||
"linkai_api_key": "${LINKAI_KEY:-}",
|
||||
"linkai_app_code": "",
|
||||
"feishu_bot_name": "${FEISHU_BOT_NAME}",
|
||||
"feishu_app_id": "${FEISHU_APP_ID}",
|
||||
"feishu_app_secret": "${FEISHU_APP_SECRET}",
|
||||
"dingtalk_client_id": "",
|
||||
"dingtalk_client_secret": "",
|
||||
"agent": true,
|
||||
"agent_max_context_tokens": 40000,
|
||||
"agent_max_context_turns": 30,
|
||||
"agent_max_steps": 15
|
||||
|
||||
CHANNEL_TYPE="$CHANNEL_TYPE" \
|
||||
MODEL_NAME="$MODEL_NAME" \
|
||||
OPENAI_KEY="${OPENAI_KEY:-}" \
|
||||
OPENAI_BASE="${OPENAI_BASE:-https://api.openai.com/v1}" \
|
||||
CLAUDE_KEY="${CLAUDE_KEY:-}" \
|
||||
CLAUDE_BASE="${CLAUDE_BASE:-https://api.anthropic.com/v1}" \
|
||||
GEMINI_KEY="${GEMINI_KEY:-}" \
|
||||
GEMINI_BASE="${GEMINI_BASE:-https://generativelanguage.googleapis.com}" \
|
||||
ZHIPU_KEY="${ZHIPU_KEY:-}" \
|
||||
MOONSHOT_KEY="${MOONSHOT_KEY:-}" \
|
||||
ARK_KEY="${ARK_KEY:-}" \
|
||||
DASHSCOPE_KEY="${DASHSCOPE_KEY:-}" \
|
||||
MINIMAX_KEY="${MINIMAX_KEY:-}" \
|
||||
USE_LINKAI="${USE_LINKAI:-false}" \
|
||||
LINKAI_KEY="${LINKAI_KEY:-}" \
|
||||
FEISHU_APP_ID="${FEISHU_APP_ID:-}" \
|
||||
FEISHU_APP_SECRET="${FEISHU_APP_SECRET:-}" \
|
||||
WEB_PORT="${WEB_PORT:-}" \
|
||||
DT_CLIENT_ID="${DT_CLIENT_ID:-}" \
|
||||
DT_CLIENT_SECRET="${DT_CLIENT_SECRET:-}" \
|
||||
WECOM_BOT_ID="${WECOM_BOT_ID:-}" \
|
||||
WECOM_BOT_SECRET="${WECOM_BOT_SECRET:-}" \
|
||||
QQ_APP_ID="${QQ_APP_ID:-}" \
|
||||
QQ_APP_SECRET="${QQ_APP_SECRET:-}" \
|
||||
WECHATCOM_CORP_ID="${WECHATCOM_CORP_ID:-}" \
|
||||
WECHATCOM_TOKEN="${WECHATCOM_TOKEN:-}" \
|
||||
WECHATCOM_SECRET="${WECHATCOM_SECRET:-}" \
|
||||
WECHATCOM_AGENT_ID="${WECHATCOM_AGENT_ID:-}" \
|
||||
WECHATCOM_AES_KEY="${WECHATCOM_AES_KEY:-}" \
|
||||
WECHATCOM_PORT="${WECHATCOM_PORT:-}" \
|
||||
$PYTHON_CMD -c "
|
||||
import json, os
|
||||
e = os.environ.get
|
||||
base = {
|
||||
'channel_type': e('CHANNEL_TYPE'),
|
||||
'model': e('MODEL_NAME'),
|
||||
'open_ai_api_key': e('OPENAI_KEY', ''),
|
||||
'open_ai_api_base': e('OPENAI_BASE'),
|
||||
'claude_api_key': e('CLAUDE_KEY', ''),
|
||||
'claude_api_base': e('CLAUDE_BASE'),
|
||||
'gemini_api_key': e('GEMINI_KEY', ''),
|
||||
'gemini_api_base': e('GEMINI_BASE'),
|
||||
'zhipu_ai_api_key': e('ZHIPU_KEY', ''),
|
||||
'moonshot_api_key': e('MOONSHOT_KEY', ''),
|
||||
'ark_api_key': e('ARK_KEY', ''),
|
||||
'dashscope_api_key': e('DASHSCOPE_KEY', ''),
|
||||
'minimax_api_key': e('MINIMAX_KEY', ''),
|
||||
'voice_to_text': 'openai',
|
||||
'text_to_voice': 'openai',
|
||||
'voice_reply_voice': False,
|
||||
'speech_recognition': True,
|
||||
'group_speech_recognition': False,
|
||||
'use_linkai': e('USE_LINKAI') == 'true',
|
||||
'linkai_api_key': e('LINKAI_KEY', ''),
|
||||
'linkai_app_code': '',
|
||||
'agent': True,
|
||||
'agent_max_context_tokens': 40000,
|
||||
'agent_max_context_turns': 30,
|
||||
'agent_max_steps': 15,
|
||||
}
|
||||
EOF
|
||||
;;
|
||||
web)
|
||||
cat > config.json <<EOF
|
||||
{
|
||||
"channel_type": "web",
|
||||
"web_port": ${WEB_PORT},
|
||||
"model": "${MODEL_NAME}",
|
||||
"open_ai_api_key": "${OPENAI_KEY:-}",
|
||||
"open_ai_api_base": "${OPENAI_BASE:-https://api.openai.com/v1}",
|
||||
"claude_api_key": "${CLAUDE_KEY:-}",
|
||||
"claude_api_base": "${CLAUDE_BASE:-https://api.anthropic.com/v1}",
|
||||
"gemini_api_key": "${GEMINI_KEY:-}",
|
||||
"gemini_api_base": "${GEMINI_BASE:-https://generativelanguage.googleapis.com}",
|
||||
"zhipu_ai_api_key": "${ZHIPU_KEY:-}",
|
||||
"moonshot_api_key": "${MOONSHOT_KEY:-}",
|
||||
"ark_api_key": "${ARK_KEY:-}",
|
||||
"dashscope_api_key": "${DASHSCOPE_KEY:-}",
|
||||
"minimax_api_key": "${MINIMAX_KEY:-}",
|
||||
"voice_to_text": "openai",
|
||||
"text_to_voice": "openai",
|
||||
"voice_reply_voice": false,
|
||||
"speech_recognition": true,
|
||||
"group_speech_recognition": false,
|
||||
"use_linkai": ${USE_LINKAI:-false},
|
||||
"linkai_api_key": "${LINKAI_KEY:-}",
|
||||
"linkai_app_code": "",
|
||||
"feishu_bot_name": "$feishu_bot_name",
|
||||
"feishu_app_id": "",
|
||||
"feishu_app_secret": "",
|
||||
"dingtalk_client_id": "",
|
||||
"dingtalk_client_secret": "",
|
||||
"agent": true,
|
||||
"agent_max_context_tokens": 40000,
|
||||
"agent_max_context_turns": 30,
|
||||
"agent_max_steps": 15
|
||||
channel_map = {
|
||||
'feishu': {'feishu_app_id': 'FEISHU_APP_ID', 'feishu_app_secret': 'FEISHU_APP_SECRET'},
|
||||
'web': {'web_port': ('WEB_PORT', int)},
|
||||
'dingtalk': {'dingtalk_client_id': 'DT_CLIENT_ID', 'dingtalk_client_secret': 'DT_CLIENT_SECRET'},
|
||||
'wecom_bot': {'wecom_bot_id': 'WECOM_BOT_ID', 'wecom_bot_secret': 'WECOM_BOT_SECRET'},
|
||||
'qq': {'qq_app_id': 'QQ_APP_ID', 'qq_app_secret': 'QQ_APP_SECRET'},
|
||||
'wechatcom_app': {'wechatcom_corp_id': 'WECHATCOM_CORP_ID', 'wechatcomapp_token': 'WECHATCOM_TOKEN', 'wechatcomapp_secret': 'WECHATCOM_SECRET', 'wechatcomapp_agent_id': 'WECHATCOM_AGENT_ID', 'wechatcomapp_aes_key': 'WECHATCOM_AES_KEY', 'wechatcomapp_port': ('WECHATCOM_PORT', int)},
|
||||
}
|
||||
EOF
|
||||
;;
|
||||
dingtalk)
|
||||
cat > config.json <<EOF
|
||||
{
|
||||
"channel_type": "dingtalk",
|
||||
"model": "${MODEL_NAME}",
|
||||
"open_ai_api_key": "${OPENAI_KEY:-}",
|
||||
"open_ai_api_base": "${OPENAI_BASE:-https://api.openai.com/v1}",
|
||||
"claude_api_key": "${CLAUDE_KEY:-}",
|
||||
"claude_api_base": "${CLAUDE_BASE:-https://api.anthropic.com/v1}",
|
||||
"gemini_api_key": "${GEMINI_KEY:-}",
|
||||
"gemini_api_base": "${GEMINI_BASE:-https://generativelanguage.googleapis.com}",
|
||||
"zhipu_ai_api_key": "${ZHIPU_KEY:-}",
|
||||
"moonshot_api_key": "${MOONSHOT_KEY:-}",
|
||||
"ark_api_key": "${ARK_KEY:-}",
|
||||
"dashscope_api_key": "${DASHSCOPE_KEY:-}",
|
||||
"minimax_api_key": "${MINIMAX_KEY:-}",
|
||||
"voice_to_text": "openai",
|
||||
"text_to_voice": "openai",
|
||||
"voice_reply_voice": false,
|
||||
"speech_recognition": true,
|
||||
"group_speech_recognition": false,
|
||||
"use_linkai": ${USE_LINKAI:-false},
|
||||
"linkai_api_key": "${LINKAI_KEY:-}",
|
||||
"linkai_app_code": "",
|
||||
"feishu_bot_name": "$feishu_bot_name",
|
||||
"feishu_app_id": "",
|
||||
"feishu_app_secret": "",
|
||||
"dingtalk_client_id": "${DT_CLIENT_ID}",
|
||||
"dingtalk_client_secret": "${DT_CLIENT_SECRET}",
|
||||
"agent": true,
|
||||
"agent_max_context_tokens": 40000,
|
||||
"agent_max_context_turns": 30,
|
||||
"agent_max_steps": 15
|
||||
}
|
||||
EOF
|
||||
;;
|
||||
wecom_bot)
|
||||
cat > config.json <<EOF
|
||||
{
|
||||
"channel_type": "wecom_bot",
|
||||
"wecom_bot_id": "${WECOM_BOT_ID}",
|
||||
"wecom_bot_secret": "${WECOM_BOT_SECRET}",
|
||||
"model": "${MODEL_NAME}",
|
||||
"open_ai_api_key": "${OPENAI_KEY:-}",
|
||||
"open_ai_api_base": "${OPENAI_BASE:-https://api.openai.com/v1}",
|
||||
"claude_api_key": "${CLAUDE_KEY:-}",
|
||||
"claude_api_base": "${CLAUDE_BASE:-https://api.anthropic.com/v1}",
|
||||
"gemini_api_key": "${GEMINI_KEY:-}",
|
||||
"gemini_api_base": "${GEMINI_BASE:-https://generativelanguage.googleapis.com}",
|
||||
"zhipu_ai_api_key": "${ZHIPU_KEY:-}",
|
||||
"moonshot_api_key": "${MOONSHOT_KEY:-}",
|
||||
"ark_api_key": "${ARK_KEY:-}",
|
||||
"dashscope_api_key": "${DASHSCOPE_KEY:-}",
|
||||
"minimax_api_key": "${MINIMAX_KEY:-}",
|
||||
"voice_to_text": "openai",
|
||||
"text_to_voice": "openai",
|
||||
"voice_reply_voice": false,
|
||||
"speech_recognition": true,
|
||||
"group_speech_recognition": false,
|
||||
"use_linkai": ${USE_LINKAI:-false},
|
||||
"linkai_api_key": "${LINKAI_KEY:-}",
|
||||
"linkai_app_code": "",
|
||||
"agent": true,
|
||||
"agent_max_context_tokens": 40000,
|
||||
"agent_max_context_turns": 30,
|
||||
"agent_max_steps": 15
|
||||
}
|
||||
EOF
|
||||
;;
|
||||
qq)
|
||||
cat > config.json <<EOF
|
||||
{
|
||||
"channel_type": "qq",
|
||||
"qq_app_id": "${QQ_APP_ID}",
|
||||
"qq_app_secret": "${QQ_APP_SECRET}",
|
||||
"model": "${MODEL_NAME}",
|
||||
"open_ai_api_key": "${OPENAI_KEY:-}",
|
||||
"open_ai_api_base": "${OPENAI_BASE:-https://api.openai.com/v1}",
|
||||
"claude_api_key": "${CLAUDE_KEY:-}",
|
||||
"claude_api_base": "${CLAUDE_BASE:-https://api.anthropic.com/v1}",
|
||||
"gemini_api_key": "${GEMINI_KEY:-}",
|
||||
"gemini_api_base": "${GEMINI_BASE:-https://generativelanguage.googleapis.com}",
|
||||
"zhipu_ai_api_key": "${ZHIPU_KEY:-}",
|
||||
"moonshot_api_key": "${MOONSHOT_KEY:-}",
|
||||
"ark_api_key": "${ARK_KEY:-}",
|
||||
"dashscope_api_key": "${DASHSCOPE_KEY:-}",
|
||||
"minimax_api_key": "${MINIMAX_KEY:-}",
|
||||
"voice_to_text": "openai",
|
||||
"text_to_voice": "openai",
|
||||
"voice_reply_voice": false,
|
||||
"speech_recognition": true,
|
||||
"group_speech_recognition": false,
|
||||
"use_linkai": ${USE_LINKAI:-false},
|
||||
"linkai_api_key": "${LINKAI_KEY:-}",
|
||||
"linkai_app_code": "",
|
||||
"agent": true,
|
||||
"agent_max_context_tokens": 40000,
|
||||
"agent_max_context_turns": 30,
|
||||
"agent_max_steps": 15
|
||||
}
|
||||
EOF
|
||||
;;
|
||||
wechatcom_app)
|
||||
cat > config.json <<EOF
|
||||
{
|
||||
"channel_type": "wechatcom_app",
|
||||
"wechatcom_corp_id": "${WECHATCOM_CORP_ID}",
|
||||
"wechatcomapp_token": "${WECHATCOM_TOKEN}",
|
||||
"wechatcomapp_secret": "${WECHATCOM_SECRET}",
|
||||
"wechatcomapp_agent_id": "${WECHATCOM_AGENT_ID}",
|
||||
"wechatcomapp_aes_key": "${WECHATCOM_AES_KEY}",
|
||||
"wechatcomapp_port": ${WECHATCOM_PORT},
|
||||
"model": "${MODEL_NAME}",
|
||||
"open_ai_api_key": "${OPENAI_KEY:-}",
|
||||
"open_ai_api_base": "${OPENAI_BASE:-https://api.openai.com/v1}",
|
||||
"claude_api_key": "${CLAUDE_KEY:-}",
|
||||
"claude_api_base": "${CLAUDE_BASE:-https://api.anthropic.com/v1}",
|
||||
"gemini_api_key": "${GEMINI_KEY:-}",
|
||||
"gemini_api_base": "${GEMINI_BASE:-https://generativelanguage.googleapis.com}",
|
||||
"zhipu_ai_api_key": "${ZHIPU_KEY:-}",
|
||||
"moonshot_api_key": "${MOONSHOT_KEY:-}",
|
||||
"ark_api_key": "${ARK_KEY:-}",
|
||||
"dashscope_api_key": "${DASHSCOPE_KEY:-}",
|
||||
"minimax_api_key": "${MINIMAX_KEY:-}",
|
||||
"voice_to_text": "openai",
|
||||
"text_to_voice": "openai",
|
||||
"voice_reply_voice": false,
|
||||
"speech_recognition": true,
|
||||
"group_speech_recognition": false,
|
||||
"use_linkai": ${USE_LINKAI:-false},
|
||||
"linkai_api_key": "${LINKAI_KEY:-}",
|
||||
"linkai_app_code": "",
|
||||
"feishu_bot_name": "$feishu_bot_name",
|
||||
"feishu_app_id": "",
|
||||
"feishu_app_secret": "",
|
||||
"dingtalk_client_id": "",
|
||||
"dingtalk_client_secret": "",
|
||||
"agent": true,
|
||||
"agent_max_context_tokens": 40000,
|
||||
"agent_max_context_turns": 30,
|
||||
"agent_max_steps": 15
|
||||
}
|
||||
EOF
|
||||
;;
|
||||
esac
|
||||
ch = e('CHANNEL_TYPE')
|
||||
for key, spec in channel_map.get(ch, {}).items():
|
||||
if isinstance(spec, tuple):
|
||||
env_name, conv = spec
|
||||
base[key] = conv(e(env_name))
|
||||
else:
|
||||
base[key] = e(spec, '')
|
||||
with open('config.json', 'w') as f:
|
||||
json.dump(base, f, indent=2, ensure_ascii=False)
|
||||
"
|
||||
|
||||
echo -e "${GREEN}✅ Configuration file created successfully.${NC}"
|
||||
}
|
||||
@@ -811,21 +593,22 @@ show_usage() {
|
||||
echo -e "${CYAN}${BOLD}=========================================${NC}"
|
||||
}
|
||||
|
||||
# Check if service is running
|
||||
is_running() {
|
||||
# Ensure PYTHON_CMD is set
|
||||
ensure_python_cmd() {
|
||||
if [ -z "$PYTHON_CMD" ]; then
|
||||
detect_python_command 2>/dev/null || PYTHON_CMD="python3"
|
||||
detect_python_command > /dev/null 2>&1 || PYTHON_CMD="python3"
|
||||
fi
|
||||
pid=$(ps ax | grep -i app.py | grep "${BASE_DIR}" | grep "$PYTHON_CMD" | grep -v grep | awk '{print $1}')
|
||||
[ -n "$pid" ]
|
||||
}
|
||||
|
||||
# Get service PID
|
||||
# Get service PID (empty string if not running)
|
||||
get_pid() {
|
||||
if [ -z "$PYTHON_CMD" ]; then
|
||||
detect_python_command 2>/dev/null || PYTHON_CMD="python3"
|
||||
fi
|
||||
ps ax | grep -i app.py | grep "${BASE_DIR}" | grep "$PYTHON_CMD" | grep -v grep | awk '{print $1}'
|
||||
ensure_python_cmd > /dev/null 2>&1
|
||||
ps ax | grep -i app.py | grep "${BASE_DIR}" | grep "$PYTHON_CMD" | grep -v grep | awk '{print $1}' | grep -E '^[0-9]+$'
|
||||
}
|
||||
|
||||
# Check if service is running
|
||||
is_running() {
|
||||
[ -n "$(get_pid)" ]
|
||||
}
|
||||
|
||||
# Start service
|
||||
@@ -850,23 +633,28 @@ cmd_start() {
|
||||
# Stop service
|
||||
cmd_stop() {
|
||||
echo -e "${GREEN}${EMOJI_STOP} Stopping CowAgent...${NC}"
|
||||
|
||||
|
||||
if ! is_running; then
|
||||
echo -e "${YELLOW}${EMOJI_WARN} CowAgent is not running${NC}"
|
||||
return
|
||||
fi
|
||||
|
||||
|
||||
pid=$(get_pid)
|
||||
if [ -z "$pid" ] || ! echo "$pid" | grep -qE '^[0-9]+$'; then
|
||||
echo -e "${RED}❌ Failed to get valid PID (got: ${pid})${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Found running process (PID: ${pid})${NC}"
|
||||
|
||||
|
||||
kill ${pid}
|
||||
sleep 3
|
||||
|
||||
|
||||
if ps -p ${pid} > /dev/null 2>&1; then
|
||||
echo -e "${YELLOW}⚠️ Process not stopped, forcing termination...${NC}"
|
||||
kill -9 ${pid}
|
||||
fi
|
||||
|
||||
|
||||
echo -e "${GREEN}${EMOJI_CHECK} CowAgent stopped${NC}"
|
||||
}
|
||||
|
||||
@@ -1023,67 +811,38 @@ install_mode() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Require running inside the project directory
|
||||
require_project_dir() {
|
||||
if [ "$IS_PROJECT_DIR" = false ]; then
|
||||
echo -e "${RED}${EMOJI_CROSS} Must run in project directory${NC}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Main function
|
||||
main() {
|
||||
case "$1" in
|
||||
start)
|
||||
if [ "$IS_PROJECT_DIR" = false ]; then
|
||||
echo -e "${RED}❌ Must run in project directory${NC}"
|
||||
exit 1
|
||||
fi
|
||||
cmd_start
|
||||
;;
|
||||
stop)
|
||||
if [ "$IS_PROJECT_DIR" = false ]; then
|
||||
echo -e "${RED}❌ Must run in project directory${NC}"
|
||||
exit 1
|
||||
fi
|
||||
cmd_stop
|
||||
;;
|
||||
restart)
|
||||
if [ "$IS_PROJECT_DIR" = false ]; then
|
||||
echo -e "${RED}❌ Must run in project directory${NC}"
|
||||
exit 1
|
||||
fi
|
||||
cmd_restart
|
||||
;;
|
||||
status)
|
||||
if [ "$IS_PROJECT_DIR" = false ]; then
|
||||
echo -e "${RED}❌ Must run in project directory${NC}"
|
||||
exit 1
|
||||
fi
|
||||
cmd_status
|
||||
;;
|
||||
logs)
|
||||
if [ "$IS_PROJECT_DIR" = false ]; then
|
||||
echo -e "${RED}❌ Must run in project directory${NC}"
|
||||
exit 1
|
||||
fi
|
||||
cmd_logs
|
||||
;;
|
||||
config)
|
||||
if [ "$IS_PROJECT_DIR" = false ]; then
|
||||
echo -e "${RED}❌ Must run in project directory${NC}"
|
||||
exit 1
|
||||
fi
|
||||
cmd_config
|
||||
;;
|
||||
update)
|
||||
if [ "$IS_PROJECT_DIR" = false ]; then
|
||||
echo -e "${RED}❌ Must run in project directory${NC}"
|
||||
exit 1
|
||||
fi
|
||||
cmd_update
|
||||
start|stop|restart|status|logs|config|update)
|
||||
require_project_dir
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$1" in
|
||||
start) cmd_start ;;
|
||||
stop) cmd_stop ;;
|
||||
restart) cmd_restart ;;
|
||||
status) cmd_status ;;
|
||||
logs) cmd_logs ;;
|
||||
config) cmd_config ;;
|
||||
update) cmd_update ;;
|
||||
help|--help|-h)
|
||||
show_usage
|
||||
;;
|
||||
"")
|
||||
# No command - install/configure mode
|
||||
install_mode
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}❌ Unknown command: $1${NC}"
|
||||
echo -e "${RED}${EMOJI_CROSS} Unknown command: $1${NC}"
|
||||
echo ""
|
||||
show_usage
|
||||
exit 1
|
||||
|
||||
Reference in New Issue
Block a user