mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
feat: add wecom and wecomApp test
This commit is contained in:
+83
-1
@@ -262,7 +262,7 @@ Et voilà ! Vous avez un assistant IA fonctionnel en 2 minutes.
|
||||
|
||||
## 💬 Applications de Chat
|
||||
|
||||
Discutez avec votre PicoClaw via Telegram, Discord, DingTalk ou LINE
|
||||
Discutez avec votre PicoClaw via Telegram, Discord, DingTalk, LINE ou WeCom
|
||||
|
||||
| Canal | Configuration |
|
||||
| ------------ | -------------------------------------- |
|
||||
@@ -271,6 +271,7 @@ Discutez avec votre PicoClaw via Telegram, Discord, DingTalk ou LINE
|
||||
| **QQ** | Facile (AppID + AppSecret) |
|
||||
| **DingTalk** | Moyen (identifiants de l'application) |
|
||||
| **LINE** | Moyen (identifiants + URL de webhook) |
|
||||
| **WeCom** | Moyen (CorpID + configuration webhook) |
|
||||
|
||||
<details>
|
||||
<summary><b>Telegram</b> (Recommandé)</summary>
|
||||
@@ -470,6 +471,87 @@ picoclaw gateway
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>WeCom (WeChat Work)</b></summary>
|
||||
|
||||
PicoClaw prend en charge deux types d'intégration WeCom :
|
||||
|
||||
**Option 1 : WeCom Bot (Robot Intelligent)** - Configuration plus facile, prend en charge les discussions de groupe
|
||||
**Option 2 : WeCom App (Application Personnalisée)** - Plus de fonctionnalités, messagerie proactive
|
||||
|
||||
Voir le [Guide de Configuration WeCom App](docs/wecom-app-configuration.md) pour des instructions détaillées.
|
||||
|
||||
**Configuration Rapide - WeCom Bot :**
|
||||
|
||||
**1. Créer un bot**
|
||||
|
||||
* Accédez à la Console d'Administration WeCom → Discussion de Groupe → Ajouter un Bot de Groupe
|
||||
* Copiez l'URL du webhook (format : `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx`)
|
||||
|
||||
**2. Configurer**
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"wecom": {
|
||||
"enabled": true,
|
||||
"token": "YOUR_TOKEN",
|
||||
"encoding_aes_key": "YOUR_ENCODING_AES_KEY",
|
||||
"webhook_url": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY",
|
||||
"webhook_host": "0.0.0.0",
|
||||
"webhook_port": 18793,
|
||||
"webhook_path": "/webhook/wecom",
|
||||
"allow_from": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Configuration Rapide - WeCom App :**
|
||||
|
||||
**1. Créer une application**
|
||||
|
||||
* Accédez à la Console d'Administration WeCom → Gestion des Applications → Créer une Application
|
||||
* Copiez l'**AgentId** et le **Secret**
|
||||
* Accédez à la page "Mon Entreprise", copiez le **CorpID**
|
||||
|
||||
**2. Configurer la réception des messages**
|
||||
|
||||
* Dans les détails de l'application, cliquez sur "Recevoir les Messages" → "Configurer l'API"
|
||||
* Définissez l'URL sur `http://your-server:18792/webhook/wecom-app`
|
||||
* Générez le **Token** et l'**EncodingAESKey**
|
||||
|
||||
**3. Configurer**
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"wecom_app": {
|
||||
"enabled": true,
|
||||
"corp_id": "wwxxxxxxxxxxxxxxxx",
|
||||
"corp_secret": "YOUR_CORP_SECRET",
|
||||
"agent_id": 1000002,
|
||||
"token": "YOUR_TOKEN",
|
||||
"encoding_aes_key": "YOUR_ENCODING_AES_KEY",
|
||||
"webhook_host": "0.0.0.0",
|
||||
"webhook_port": 18792,
|
||||
"webhook_path": "/webhook/wecom-app",
|
||||
"allow_from": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**4. Lancer**
|
||||
|
||||
```bash
|
||||
picoclaw gateway
|
||||
```
|
||||
|
||||
> **Note** : WeCom App nécessite l'ouverture du port 18792 pour les callbacks webhook. Utilisez un proxy inverse pour HTTPS en production.
|
||||
|
||||
</details>
|
||||
|
||||
## <img src="assets/clawdchat-icon.png" width="24" height="24" alt="ClawdChat"> Rejoignez le Réseau Social d'Agents
|
||||
|
||||
Connectez PicoClaw au Réseau Social d'Agents simplement en envoyant un seul message via le CLI ou n'importe quelle application de chat intégrée.
|
||||
|
||||
+83
-1
@@ -226,7 +226,7 @@ picoclaw agent -m "What is 2+2?"
|
||||
|
||||
## 💬 チャットアプリ
|
||||
|
||||
Telegram、Discord、QQ、DingTalk、LINE で PicoClaw と会話できます
|
||||
Telegram、Discord、QQ、DingTalk、LINE、WeCom で PicoClaw と会話できます
|
||||
|
||||
| チャネル | セットアップ |
|
||||
|---------|------------|
|
||||
@@ -235,6 +235,7 @@ Telegram、Discord、QQ、DingTalk、LINE で PicoClaw と会話できます
|
||||
| **QQ** | 簡単(AppID + AppSecret) |
|
||||
| **DingTalk** | 普通(アプリ認証情報) |
|
||||
| **LINE** | 普通(認証情報 + Webhook URL) |
|
||||
| **WeCom** | 普通(CorpID + Webhook設定) |
|
||||
|
||||
<details>
|
||||
<summary><b>Telegram</b>(推奨)</summary>
|
||||
@@ -430,6 +431,87 @@ picoclaw gateway
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>WeCom (企業微信)</b></summary>
|
||||
|
||||
PicoClaw は2種類の WeCom 統合をサポートしています:
|
||||
|
||||
**オプション1: WeCom Bot (智能ロボット)** - 簡単な設定、グループチャット対応
|
||||
**オプション2: WeCom App (自作アプリ)** - より多機能、アクティブメッセージング対応
|
||||
|
||||
詳細な設定手順は [WeCom App Configuration Guide](docs/wecom-app-configuration.md) を参照してください。
|
||||
|
||||
**クイックセットアップ - WeCom Bot:**
|
||||
|
||||
**1. ボットを作成**
|
||||
|
||||
* WeCom 管理コンソール → グループチャット → グループボットを追加
|
||||
* Webhook URL をコピー(形式: `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx`)
|
||||
|
||||
**2. 設定**
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"wecom": {
|
||||
"enabled": true,
|
||||
"token": "YOUR_TOKEN",
|
||||
"encoding_aes_key": "YOUR_ENCODING_AES_KEY",
|
||||
"webhook_url": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY",
|
||||
"webhook_host": "0.0.0.0",
|
||||
"webhook_port": 18793,
|
||||
"webhook_path": "/webhook/wecom",
|
||||
"allow_from": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**クイックセットアップ - WeCom App:**
|
||||
|
||||
**1. アプリを作成**
|
||||
|
||||
* WeCom 管理コンソール → アプリ管理 → アプリを作成
|
||||
* **AgentId** と **Secret** をコピー
|
||||
* "マイ会社" ページで **CorpID** をコピー
|
||||
|
||||
**2. メッセージ受信を設定**
|
||||
|
||||
* アプリ詳細で "メッセージを受信" → "APIを設定" をクリック
|
||||
* URL を `http://your-server:18792/webhook/wecom-app` に設定
|
||||
* **Token** と **EncodingAESKey** を生成
|
||||
|
||||
**3. 設定**
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"wecom_app": {
|
||||
"enabled": true,
|
||||
"corp_id": "wwxxxxxxxxxxxxxxxx",
|
||||
"corp_secret": "YOUR_CORP_SECRET",
|
||||
"agent_id": 1000002,
|
||||
"token": "YOUR_TOKEN",
|
||||
"encoding_aes_key": "YOUR_ENCODING_AES_KEY",
|
||||
"webhook_host": "0.0.0.0",
|
||||
"webhook_port": 18792,
|
||||
"webhook_path": "/webhook/wecom-app",
|
||||
"allow_from": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**4. 起動**
|
||||
|
||||
```bash
|
||||
picoclaw gateway
|
||||
```
|
||||
|
||||
> **注意**: WeCom App は Webhook コールバック用にポート 18792 を開放する必要があります。本番環境では HTTPS 用のリバースプロキシを使用してください。
|
||||
|
||||
</details>
|
||||
|
||||
## ⚙️ 設定
|
||||
|
||||
設定ファイル: `~/.picoclaw/config.json`
|
||||
|
||||
@@ -264,7 +264,7 @@ That's it! You have a working AI assistant in 2 minutes.
|
||||
|
||||
## 💬 Chat Apps
|
||||
|
||||
Talk to your picoclaw through Telegram, Discord, DingTalk, or LINE
|
||||
Talk to your picoclaw through Telegram, Discord, DingTalk, LINE, or WeCom
|
||||
|
||||
| Channel | Setup |
|
||||
| ------------ | ---------------------------------- |
|
||||
@@ -273,6 +273,7 @@ Talk to your picoclaw through Telegram, Discord, DingTalk, or LINE
|
||||
| **QQ** | Easy (AppID + AppSecret) |
|
||||
| **DingTalk** | Medium (app credentials) |
|
||||
| **LINE** | Medium (credentials + webhook URL) |
|
||||
| **WeCom** | Medium (CorpID + webhook setup) |
|
||||
|
||||
<details>
|
||||
<summary><b>Telegram</b> (Recommended)</summary>
|
||||
@@ -472,6 +473,87 @@ picoclaw gateway
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>WeCom (企业微信)</b></summary>
|
||||
|
||||
PicoClaw supports two types of WeCom integration:
|
||||
|
||||
**Option 1: WeCom Bot (智能机器人)** - Easier setup, supports group chats
|
||||
**Option 2: WeCom App (自建应用)** - More features, proactive messaging
|
||||
|
||||
See [WeCom App Configuration Guide](docs/wecom-app-configuration.md) for detailed setup instructions.
|
||||
|
||||
**Quick Setup - WeCom Bot:**
|
||||
|
||||
**1. Create a bot**
|
||||
|
||||
* Go to WeCom Admin Console → Group Chat → Add Group Bot
|
||||
* Copy the webhook URL (format: `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx`)
|
||||
|
||||
**2. Configure**
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"wecom": {
|
||||
"enabled": true,
|
||||
"token": "YOUR_TOKEN",
|
||||
"encoding_aes_key": "YOUR_ENCODING_AES_KEY",
|
||||
"webhook_url": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY",
|
||||
"webhook_host": "0.0.0.0",
|
||||
"webhook_port": 18793,
|
||||
"webhook_path": "/webhook/wecom",
|
||||
"allow_from": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Quick Setup - WeCom App:**
|
||||
|
||||
**1. Create an app**
|
||||
|
||||
* Go to WeCom Admin Console → App Management → Create App
|
||||
* Copy **AgentId** and **Secret**
|
||||
* Go to "My Company" page, copy **CorpID**
|
||||
|
||||
**2. Configure receive message**
|
||||
|
||||
* In App details, click "Receive Message" → "Set API"
|
||||
* Set URL to `http://your-server:18792/webhook/wecom-app`
|
||||
* Generate **Token** and **EncodingAESKey**
|
||||
|
||||
**3. Configure**
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"wecom_app": {
|
||||
"enabled": true,
|
||||
"corp_id": "wwxxxxxxxxxxxxxxxx",
|
||||
"corp_secret": "YOUR_CORP_SECRET",
|
||||
"agent_id": 1000002,
|
||||
"token": "YOUR_TOKEN",
|
||||
"encoding_aes_key": "YOUR_ENCODING_AES_KEY",
|
||||
"webhook_host": "0.0.0.0",
|
||||
"webhook_port": 18792,
|
||||
"webhook_path": "/webhook/wecom-app",
|
||||
"allow_from": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**4. Run**
|
||||
|
||||
```bash
|
||||
picoclaw gateway
|
||||
```
|
||||
|
||||
> **Note**: WeCom App requires opening port 18792 for webhook callbacks. Use a reverse proxy for HTTPS.
|
||||
|
||||
</details>
|
||||
|
||||
## <img src="assets/clawdchat-icon.png" width="24" height="24" alt="ClawdChat"> Join the Agent Social Network
|
||||
|
||||
Connect Picoclaw to the Agent Social Network simply by sending a single message via the CLI or any integrated Chat App.
|
||||
|
||||
+83
-1
@@ -263,7 +263,7 @@ Pronto! Você tem um assistente de IA funcionando em 2 minutos.
|
||||
|
||||
## 💬 Integração com Apps de Chat
|
||||
|
||||
Converse com seu PicoClaw via Telegram, Discord, DingTalk ou LINE.
|
||||
Converse com seu PicoClaw via Telegram, Discord, DingTalk, LINE ou WeCom.
|
||||
|
||||
| Canal | Nível de Configuração |
|
||||
| --- | --- |
|
||||
@@ -272,6 +272,7 @@ Converse com seu PicoClaw via Telegram, Discord, DingTalk ou LINE.
|
||||
| **QQ** | Fácil (AppID + AppSecret) |
|
||||
| **DingTalk** | Médio (credenciais do app) |
|
||||
| **LINE** | Médio (credenciais + webhook URL) |
|
||||
| **WeCom** | Médio (CorpID + configuração webhook) |
|
||||
|
||||
<details>
|
||||
<summary><b>Telegram</b> (Recomendado)</summary>
|
||||
@@ -471,6 +472,87 @@ picoclaw gateway
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>WeCom (WeChat Work)</b></summary>
|
||||
|
||||
O PicoClaw suporta dois tipos de integração WeCom:
|
||||
|
||||
**Opção 1: WeCom Bot (Robô Inteligente)** - Configuração mais fácil, suporta chats em grupo
|
||||
**Opção 2: WeCom App (Aplicativo Personalizado)** - Mais recursos, mensagens proativas
|
||||
|
||||
Veja o [Guia de Configuração WeCom App](docs/wecom-app-configuration.md) para instruções detalhadas.
|
||||
|
||||
**Configuração Rápida - WeCom Bot:**
|
||||
|
||||
**1. Criar um bot**
|
||||
|
||||
* Acesse o Console de Administração WeCom → Chat em Grupo → Adicionar Bot de Grupo
|
||||
* Copie a URL do webhook (formato: `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx`)
|
||||
|
||||
**2. Configurar**
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"wecom": {
|
||||
"enabled": true,
|
||||
"token": "YOUR_TOKEN",
|
||||
"encoding_aes_key": "YOUR_ENCODING_AES_KEY",
|
||||
"webhook_url": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY",
|
||||
"webhook_host": "0.0.0.0",
|
||||
"webhook_port": 18793,
|
||||
"webhook_path": "/webhook/wecom",
|
||||
"allow_from": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Configuração Rápida - WeCom App:**
|
||||
|
||||
**1. Criar um aplicativo**
|
||||
|
||||
* Acesse o Console de Administração WeCom → Gerenciamento de Aplicativos → Criar Aplicativo
|
||||
* Copie o **AgentId** e o **Secret**
|
||||
* Acesse a página "Minha Empresa", copie o **CorpID**
|
||||
|
||||
**2. Configurar recebimento de mensagens**
|
||||
|
||||
* Nos detalhes do aplicativo, clique em "Receber Mensagens" → "Configurar API"
|
||||
* Defina a URL como `http://your-server:18792/webhook/wecom-app`
|
||||
* Gere o **Token** e o **EncodingAESKey**
|
||||
|
||||
**3. Configurar**
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"wecom_app": {
|
||||
"enabled": true,
|
||||
"corp_id": "wwxxxxxxxxxxxxxxxx",
|
||||
"corp_secret": "YOUR_CORP_SECRET",
|
||||
"agent_id": 1000002,
|
||||
"token": "YOUR_TOKEN",
|
||||
"encoding_aes_key": "YOUR_ENCODING_AES_KEY",
|
||||
"webhook_host": "0.0.0.0",
|
||||
"webhook_port": 18792,
|
||||
"webhook_path": "/webhook/wecom-app",
|
||||
"allow_from": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**4. Executar**
|
||||
|
||||
```bash
|
||||
picoclaw gateway
|
||||
```
|
||||
|
||||
> **Nota**: O WeCom App requer a abertura da porta 18792 para callbacks de webhook. Use um proxy reverso para HTTPS em produção.
|
||||
|
||||
</details>
|
||||
|
||||
## <img src="assets/clawdchat-icon.png" width="24" height="24" alt="ClawdChat"> Junte-se a Rede Social de Agentes
|
||||
|
||||
Conecte o PicoClaw a Rede Social de Agentes simplesmente enviando uma única mensagem via CLI ou qualquer App de Chat integrado.
|
||||
|
||||
+83
-1
@@ -243,7 +243,7 @@ Vậy là xong! Bạn đã có một trợ lý AI hoạt động chỉ trong 2 p
|
||||
|
||||
## 💬 Tích hợp ứng dụng Chat
|
||||
|
||||
Trò chuyện với PicoClaw qua Telegram, Discord, DingTalk hoặc LINE.
|
||||
Trò chuyện với PicoClaw qua Telegram, Discord, DingTalk, LINE hoặc WeCom.
|
||||
|
||||
| Kênh | Mức độ thiết lập |
|
||||
| --- | --- |
|
||||
@@ -252,6 +252,7 @@ Trò chuyện với PicoClaw qua Telegram, Discord, DingTalk hoặc LINE.
|
||||
| **QQ** | Dễ (AppID + AppSecret) |
|
||||
| **DingTalk** | Trung bình (app credentials) |
|
||||
| **LINE** | Trung bình (credentials + webhook URL) |
|
||||
| **WeCom** | Trung bình (CorpID + cấu hình webhook) |
|
||||
|
||||
<details>
|
||||
<summary><b>Telegram</b> (Khuyên dùng)</summary>
|
||||
@@ -451,6 +452,87 @@ picoclaw gateway
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>WeCom (WeChat Work)</b></summary>
|
||||
|
||||
PicoClaw hỗ trợ hai loại tích hợp WeCom:
|
||||
|
||||
**Tùy chọn 1: WeCom Bot (Robot Thông minh)** - Thiết lập dễ dàng hơn, hỗ trợ chat nhóm
|
||||
**Tùy chọn 2: WeCom App (Ứng dụng Tự xây dựng)** - Nhiều tính năng hơn, nhắn tin chủ động
|
||||
|
||||
Xem [Hướng dẫn Cấu hình WeCom App](docs/wecom-app-configuration.md) để biết hướng dẫn chi tiết.
|
||||
|
||||
**Thiết lập Nhanh - WeCom Bot:**
|
||||
|
||||
**1. Tạo bot**
|
||||
|
||||
* Truy cập Bảng điều khiển Quản trị WeCom → Chat Nhóm → Thêm Bot Nhóm
|
||||
* Sao chép URL webhook (định dạng: `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx`)
|
||||
|
||||
**2. Cấu hình**
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"wecom": {
|
||||
"enabled": true,
|
||||
"token": "YOUR_TOKEN",
|
||||
"encoding_aes_key": "YOUR_ENCODING_AES_KEY",
|
||||
"webhook_url": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY",
|
||||
"webhook_host": "0.0.0.0",
|
||||
"webhook_port": 18793,
|
||||
"webhook_path": "/webhook/wecom",
|
||||
"allow_from": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Thiết lập Nhanh - WeCom App:**
|
||||
|
||||
**1. Tạo ứng dụng**
|
||||
|
||||
* Truy cập Bảng điều khiển Quản trị WeCom → Quản lý Ứng dụng → Tạo Ứng dụng
|
||||
* Sao chép **AgentId** và **Secret**
|
||||
* Truy cập trang "Công ty của tôi", sao chép **CorpID**
|
||||
|
||||
**2. Cấu hình nhận tin nhắn**
|
||||
|
||||
* Trong chi tiết ứng dụng, nhấp vào "Nhận Tin nhắn" → "Thiết lập API"
|
||||
* Đặt URL thành `http://your-server:18792/webhook/wecom-app`
|
||||
* Tạo **Token** và **EncodingAESKey**
|
||||
|
||||
**3. Cấu hình**
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"wecom_app": {
|
||||
"enabled": true,
|
||||
"corp_id": "wwxxxxxxxxxxxxxxxx",
|
||||
"corp_secret": "YOUR_CORP_SECRET",
|
||||
"agent_id": 1000002,
|
||||
"token": "YOUR_TOKEN",
|
||||
"encoding_aes_key": "YOUR_ENCODING_AES_KEY",
|
||||
"webhook_host": "0.0.0.0",
|
||||
"webhook_port": 18792,
|
||||
"webhook_path": "/webhook/wecom-app",
|
||||
"allow_from": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**4. Chạy**
|
||||
|
||||
```bash
|
||||
picoclaw gateway
|
||||
```
|
||||
|
||||
> **Lưu ý**: WeCom App yêu cầu mở cổng 18792 cho callback webhook. Sử dụng proxy ngược cho HTTPS trong môi trường sản xuất.
|
||||
|
||||
</details>
|
||||
|
||||
## <img src="assets/clawdchat-icon.png" width="24" height="24" alt="ClawdChat"> Tham gia Mạng xã hội Agent
|
||||
|
||||
Kết nối PicoClaw với Mạng xã hội Agent chỉ bằng cách gửi một tin nhắn qua CLI hoặc bất kỳ ứng dụng Chat nào đã tích hợp.
|
||||
|
||||
+85
-2
@@ -273,14 +273,15 @@ picoclaw agent -m "2+2 等于几?"
|
||||
|
||||
## 💬 聊天应用集成 (Chat Apps)
|
||||
|
||||
通过 Telegram, Discord 或钉钉与您的 PicoClaw 对话。
|
||||
通过 Telegram, Discord, 钉钉或企业微信与您的 PicoClaw 对话。
|
||||
|
||||
| 渠道 | 设置难度 |
|
||||
| --- | --- |
|
||||
| **Telegram** | 简单 (仅需 token) |
|
||||
| **Discord** | 简单 (bot token + intents) |
|
||||
| **QQ** | 简单 (AppID + AppSecret) |
|
||||
| **钉钉 (DingTalk)** | 中等 (app credentials) |
|
||||
| **钉钉 (DingTalk)** | 中等 (应用凭证) |
|
||||
| **企业微信 (WeCom)** | 中等 (企业ID + Webhook配置) |
|
||||
|
||||
<details>
|
||||
<summary><b>Telegram</b> (推荐)</summary>
|
||||
@@ -438,6 +439,88 @@ picoclaw gateway
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>企业微信 (WeCom)</b></summary>
|
||||
|
||||
PicoClaw 支持两种企业微信集成方式:
|
||||
|
||||
**选项1: 智能机器人 (WeCom Bot)** - 设置更简单,支持群聊
|
||||
**选项2: 自建应用 (WeCom App)** - 功能更丰富,支持主动推送消息
|
||||
|
||||
详见 [企业微信自建应用配置指南](docs/wecom-app-configuration.md)。
|
||||
|
||||
**快速设置 - 智能机器人:**
|
||||
|
||||
**1. 创建机器人**
|
||||
|
||||
* 前往企业微信管理后台 → 群聊 → 添加群机器人
|
||||
* 复制 Webhook URL (格式: `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx`)
|
||||
|
||||
**2. 配置**
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"wecom": {
|
||||
"enabled": true,
|
||||
"token": "YOUR_TOKEN",
|
||||
"encoding_aes_key": "YOUR_ENCODING_AES_KEY",
|
||||
"webhook_url": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY",
|
||||
"webhook_host": "0.0.0.0",
|
||||
"webhook_port": 18793,
|
||||
"webhook_path": "/webhook/wecom",
|
||||
"allow_from": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**快速设置 - 自建应用:**
|
||||
|
||||
**1. 创建应用**
|
||||
|
||||
* 前往企业微信管理后台 → 应用管理 → 创建应用
|
||||
* 复制 **AgentId** 和 **Secret**
|
||||
* 前往"我的企业"页面,复制 **CorpID**
|
||||
|
||||
**2. 配置接收消息**
|
||||
|
||||
* 在应用详情页,点击"接收消息" → "设置API"
|
||||
* 设置 URL 为 `http://your-server:18792/webhook/wecom-app`
|
||||
* 生成 **Token** 和 **EncodingAESKey**
|
||||
|
||||
**3. 配置**
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"wecom_app": {
|
||||
"enabled": true,
|
||||
"corp_id": "wwxxxxxxxxxxxxxxxx",
|
||||
"corp_secret": "YOUR_CORP_SECRET",
|
||||
"agent_id": 1000002,
|
||||
"token": "YOUR_TOKEN",
|
||||
"encoding_aes_key": "YOUR_ENCODING_AES_KEY",
|
||||
"webhook_host": "0.0.0.0",
|
||||
"webhook_port": 18792,
|
||||
"webhook_path": "/webhook/wecom-app",
|
||||
"allow_from": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**4. 运行**
|
||||
|
||||
```bash
|
||||
picoclaw gateway
|
||||
|
||||
```
|
||||
|
||||
> **注意**: 自建应用需要开放 18792 端口用于接收 Webhook 回调。生产环境建议使用反向代理配置 HTTPS。
|
||||
|
||||
</details>
|
||||
|
||||
## <img src="assets/clawdchat-icon.png" width="24" height="24" alt="ClawdChat"> 加入 Agent 社交网络
|
||||
|
||||
只需通过 CLI 或任何集成的聊天应用发送一条消息,即可将 PicoClaw 连接到 Agent 社交网络。
|
||||
|
||||
@@ -108,6 +108,7 @@
|
||||
"allow_from": []
|
||||
},
|
||||
"wecom": {
|
||||
"_comment": "WeCom Bot (智能机器人) - Easier setup, supports group chats",
|
||||
"enabled": false,
|
||||
"token": "YOUR_TOKEN",
|
||||
"encoding_aes_key": "YOUR_43_CHAR_ENCODING_AES_KEY",
|
||||
@@ -119,6 +120,7 @@
|
||||
"reply_timeout": 5
|
||||
},
|
||||
"wecom_app": {
|
||||
"_comment": "WeCom App (自建应用) - More features, proactive messaging, private chat only. See docs/wecom-app-configuration.md",
|
||||
"enabled": false,
|
||||
"corp_id": "YOUR_CORP_ID",
|
||||
"corp_secret": "YOUR_CORP_SECRET",
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
# 企业微信自建应用 (WeCom App) 配置指南
|
||||
|
||||
本文档介绍如何在 PicoClaw 中配置企业微信自建应用 (wecom-app) 通道。
|
||||
|
||||
## 功能特性
|
||||
|
||||
| 功能 | 支持状态 |
|
||||
|------|---------|
|
||||
| 被动接收消息 | ✅ |
|
||||
| 主动发送消息 | ✅ |
|
||||
| 私聊 | ✅ |
|
||||
| 群聊 | ❌ |
|
||||
|
||||
## 配置步骤
|
||||
|
||||
### 1. 企业微信后台配置
|
||||
|
||||
1. 登录 [企业微信管理后台](https://work.weixin.qq.com/wework_admin)
|
||||
2. 进入"应用管理" → 选择自建应用
|
||||
3. 记录以下信息:
|
||||
- **AgentId**: 应用详情页显示
|
||||
- **Secret**: 点击"查看"获取
|
||||
4. 进入"我的企业"页面,记录 **企业ID** (CorpID)
|
||||
|
||||
### 2. 接收消息配置
|
||||
|
||||
1. 在应用详情页,点击"接收消息"的"设置API接收"
|
||||
2. 填写以下信息:
|
||||
- **URL**: `http://your-server:18792/webhook/wecom-app`
|
||||
- **Token**: 随机生成或自定义(用于签名验证)
|
||||
- **EncodingAESKey**: 点击"随机生成"生成43字符的密钥
|
||||
3. 点击"保存"时,企业微信会发送验证请求
|
||||
|
||||
### 3. PicoClaw 配置
|
||||
|
||||
在 `config.json` 中添加以下配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"wecom_app": {
|
||||
"enabled": true,
|
||||
"corp_id": "wwxxxxxxxxxxxxxxxx", // 企业ID
|
||||
"corp_secret": "xxxxxxxxxxxxxxxxxxxxxxxx", // 应用Secret
|
||||
"agent_id": 1000002, // 应用AgentId
|
||||
"token": "your_token", // 接收消息配置的Token
|
||||
"encoding_aes_key": "your_encoding_aes_key", // 接收消息配置的EncodingAESKey
|
||||
"webhook_host": "0.0.0.0",
|
||||
"webhook_port": 18792,
|
||||
"webhook_path": "/webhook/wecom-app",
|
||||
"allow_from": [],
|
||||
"reply_timeout": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. 回调URL验证失败
|
||||
|
||||
**症状**: 企业微信保存API接收消息时提示验证失败
|
||||
|
||||
**检查项**:
|
||||
- 确认服务器防火墙已开放 18792 端口
|
||||
- 确认 `corp_id`、`token`、`encoding_aes_key` 配置正确
|
||||
- 查看 PicoClaw 日志是否有请求到达
|
||||
|
||||
### 2. 中文消息解密失败
|
||||
|
||||
**症状**: 发送中文消息时出现 `invalid padding size` 错误
|
||||
|
||||
**原因**: 企业微信使用非标准的 PKCS7 填充(32字节块大小)
|
||||
|
||||
**解决**: 确保使用最新版本的 PicoClaw,已修复此问题。
|
||||
|
||||
### 3. 端口冲突
|
||||
|
||||
**症状**: 启动时提示端口已被占用
|
||||
|
||||
**解决**: 修改 `webhook_port` 为其他端口,如 18794
|
||||
|
||||
## 技术细节
|
||||
|
||||
### 加密算法
|
||||
|
||||
- **算法**: AES-256-CBC
|
||||
- **密钥**: EncodingAESKey Base64解码后的32字节
|
||||
- **IV**: AESKey的前16字节
|
||||
- **填充**: PKCS7(块大小为32字节,非标准16字节)
|
||||
- **消息格式**: XML
|
||||
|
||||
### 消息结构
|
||||
|
||||
解密后的消息格式:
|
||||
```
|
||||
random(16B) + msg_len(4B) + msg + receiveid
|
||||
```
|
||||
|
||||
其中 `receiveid` 对于自建应用是 `corp_id`。
|
||||
|
||||
## 调试
|
||||
|
||||
启用调试模式查看详细日志:
|
||||
|
||||
```bash
|
||||
picoclaw gateway --debug
|
||||
```
|
||||
|
||||
关键日志标识:
|
||||
- `wecom_app`: WeCom App 通道相关日志
|
||||
- `wecom_common`: 加密解密相关日志
|
||||
|
||||
## 参考文档
|
||||
|
||||
- [企业微信官方文档 - 接收消息](https://developer.work.weixin.qq.com/document/path/96211)
|
||||
- [企业微信官方加解密库](https://github.com/sbzhu/weworkapi_golang)
|
||||
@@ -7,11 +7,17 @@ package channels
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -470,3 +476,129 @@ func (c *WeComBotChannel) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(status)
|
||||
}
|
||||
|
||||
// WeCom common utilities for both WeCom Bot and WeCom App
|
||||
// The following functions were moved from wecom_common.go
|
||||
|
||||
// WeComVerifySignature verifies the message signature for WeCom
|
||||
// This is a common function used by both WeCom Bot and WeCom App
|
||||
func WeComVerifySignature(token, msgSignature, timestamp, nonce, msgEncrypt string) bool {
|
||||
if token == "" {
|
||||
return true // Skip verification if token is not set
|
||||
}
|
||||
|
||||
// Sort parameters
|
||||
params := []string{token, timestamp, nonce, msgEncrypt}
|
||||
sort.Strings(params)
|
||||
|
||||
// Concatenate
|
||||
str := strings.Join(params, "")
|
||||
|
||||
// SHA1 hash
|
||||
hash := sha1.Sum([]byte(str))
|
||||
expectedSignature := fmt.Sprintf("%x", hash)
|
||||
|
||||
return expectedSignature == msgSignature
|
||||
}
|
||||
|
||||
// WeComDecryptMessage decrypts the encrypted message using AES
|
||||
// This is a common function used by both WeCom Bot and WeCom App
|
||||
// For AIBOT, receiveid should be the aibotid; for other apps, it should be corp_id
|
||||
func WeComDecryptMessage(encryptedMsg, encodingAESKey string) (string, error) {
|
||||
return WeComDecryptMessageWithVerify(encryptedMsg, encodingAESKey, "")
|
||||
}
|
||||
|
||||
// WeComDecryptMessageWithVerify decrypts the encrypted message and optionally verifies receiveid
|
||||
// receiveid: for AIBOT use aibotid, for WeCom App use corp_id. If empty, skip verification.
|
||||
func WeComDecryptMessageWithVerify(encryptedMsg, encodingAESKey, receiveid string) (string, error) {
|
||||
if encodingAESKey == "" {
|
||||
// No encryption, return as is (base64 decode)
|
||||
decoded, err := base64.StdEncoding.DecodeString(encryptedMsg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(decoded), nil
|
||||
}
|
||||
|
||||
// Decode AES key (base64)
|
||||
aesKey, err := base64.StdEncoding.DecodeString(encodingAESKey + "=")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decode AES key: %w", err)
|
||||
}
|
||||
|
||||
// Decode encrypted message
|
||||
cipherText, err := base64.StdEncoding.DecodeString(encryptedMsg)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decode message: %w", err)
|
||||
}
|
||||
|
||||
// AES decrypt
|
||||
block, err := aes.NewCipher(aesKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create cipher: %w", err)
|
||||
}
|
||||
|
||||
if len(cipherText) < aes.BlockSize {
|
||||
return "", fmt.Errorf("ciphertext too short")
|
||||
}
|
||||
|
||||
// IV is the first 16 bytes of AESKey
|
||||
iv := aesKey[:aes.BlockSize]
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
plainText := make([]byte, len(cipherText))
|
||||
mode.CryptBlocks(plainText, cipherText)
|
||||
|
||||
// Remove PKCS7 padding
|
||||
plainText, err = pkcs7UnpadWeCom(plainText)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to unpad: %w", err)
|
||||
}
|
||||
|
||||
// Parse message structure
|
||||
// Format: random(16) + msg_len(4) + msg + receiveid
|
||||
if len(plainText) < 20 {
|
||||
return "", fmt.Errorf("decrypted message too short")
|
||||
}
|
||||
|
||||
msgLen := binary.BigEndian.Uint32(plainText[16:20])
|
||||
if int(msgLen) > len(plainText)-20 {
|
||||
return "", fmt.Errorf("invalid message length")
|
||||
}
|
||||
|
||||
msg := plainText[20 : 20+msgLen]
|
||||
|
||||
// Verify receiveid if provided
|
||||
if receiveid != "" && len(plainText) > 20+int(msgLen) {
|
||||
actualReceiveID := string(plainText[20+msgLen:])
|
||||
if actualReceiveID != receiveid {
|
||||
return "", fmt.Errorf("receiveid mismatch: expected %s, got %s", receiveid, actualReceiveID)
|
||||
}
|
||||
}
|
||||
|
||||
return string(msg), nil
|
||||
}
|
||||
|
||||
// pkcs7UnpadWeCom removes PKCS7 padding with validation
|
||||
// WeCom uses block size of 32 (not standard AES block size of 16)
|
||||
const wecomBlockSize = 32
|
||||
|
||||
func pkcs7UnpadWeCom(data []byte) ([]byte, error) {
|
||||
if len(data) == 0 {
|
||||
return data, nil
|
||||
}
|
||||
padding := int(data[len(data)-1])
|
||||
// WeCom uses 32-byte block size for PKCS7 padding
|
||||
if padding == 0 || padding > wecomBlockSize {
|
||||
return nil, fmt.Errorf("invalid padding size: %d", padding)
|
||||
}
|
||||
if padding > len(data) {
|
||||
return nil, fmt.Errorf("padding size larger than data")
|
||||
}
|
||||
// Verify all padding bytes
|
||||
for i := 0; i < padding; i++ {
|
||||
if data[len(data)-1-i] != byte(padding) {
|
||||
return nil, fmt.Errorf("invalid padding byte at position %d", i)
|
||||
}
|
||||
}
|
||||
return data[:len(data)-padding], nil
|
||||
}
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
// PicoClaw - Ultra-lightweight personal AI agent
|
||||
// WeCom common utilities for both WeCom Bot and WeCom App
|
||||
|
||||
package channels
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/logger"
|
||||
)
|
||||
|
||||
// WeComVerifySignature verifies the message signature for WeCom
|
||||
// This is a common function used by both WeCom Bot and WeCom App
|
||||
func WeComVerifySignature(token, msgSignature, timestamp, nonce, msgEncrypt string) bool {
|
||||
if token == "" {
|
||||
return true // Skip verification if token is not set
|
||||
}
|
||||
|
||||
// Sort parameters
|
||||
params := []string{token, timestamp, nonce, msgEncrypt}
|
||||
sort.Strings(params)
|
||||
|
||||
// Concatenate
|
||||
str := strings.Join(params, "")
|
||||
|
||||
// SHA1 hash
|
||||
hash := sha1.Sum([]byte(str))
|
||||
expectedSignature := fmt.Sprintf("%x", hash)
|
||||
|
||||
return expectedSignature == msgSignature
|
||||
}
|
||||
|
||||
// WeComDecryptMessage decrypts the encrypted message using AES
|
||||
// This is a common function used by both WeCom Bot and WeCom App
|
||||
// For AIBOT, receiveid should be the aibotid; for other apps, it should be corp_id
|
||||
func WeComDecryptMessage(encryptedMsg, encodingAESKey string) (string, error) {
|
||||
return WeComDecryptMessageWithVerify(encryptedMsg, encodingAESKey, "")
|
||||
}
|
||||
|
||||
// WeComDecryptMessageWithVerify decrypts the encrypted message and optionally verifies receiveid
|
||||
// receiveid: for AIBOT use aibotid, for WeCom App use corp_id. If empty, skip verification.
|
||||
func WeComDecryptMessageWithVerify(encryptedMsg, encodingAESKey, receiveid string) (string, error) {
|
||||
logger.DebugCF("wecom_common", "Starting decryption", map[string]interface{}{
|
||||
"encodingAESKey_len": len(encodingAESKey),
|
||||
"receiveid": receiveid,
|
||||
"encryptedMsg_len": len(encryptedMsg),
|
||||
})
|
||||
|
||||
if encodingAESKey == "" {
|
||||
// No encryption, return as is (base64 decode)
|
||||
decoded, err := base64.StdEncoding.DecodeString(encryptedMsg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(decoded), nil
|
||||
}
|
||||
|
||||
// Decode AES key (base64)
|
||||
aesKey, err := base64.StdEncoding.DecodeString(encodingAESKey + "=")
|
||||
if err != nil {
|
||||
logger.ErrorCF("wecom_common", "Failed to decode AES key", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"key": encodingAESKey,
|
||||
})
|
||||
return "", fmt.Errorf("failed to decode AES key: %w", err)
|
||||
}
|
||||
logger.DebugCF("wecom_common", "AES key decoded", map[string]interface{}{
|
||||
"key_len": len(aesKey),
|
||||
})
|
||||
|
||||
// Decode encrypted message
|
||||
cipherText, err := base64.StdEncoding.DecodeString(encryptedMsg)
|
||||
if err != nil {
|
||||
logger.ErrorCF("wecom_common", "Failed to decode message", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return "", fmt.Errorf("failed to decode message: %w", err)
|
||||
}
|
||||
logger.DebugCF("wecom_common", "Message decoded", map[string]interface{}{
|
||||
"cipher_len": len(cipherText),
|
||||
})
|
||||
|
||||
// AES decrypt
|
||||
block, err := aes.NewCipher(aesKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create cipher: %w", err)
|
||||
}
|
||||
|
||||
if len(cipherText) < aes.BlockSize {
|
||||
return "", fmt.Errorf("ciphertext too short: %d < %d", len(cipherText), aes.BlockSize)
|
||||
}
|
||||
|
||||
// IV is the first 16 bytes of AESKey
|
||||
iv := aesKey[:aes.BlockSize]
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
plainText := make([]byte, len(cipherText))
|
||||
mode.CryptBlocks(plainText, cipherText)
|
||||
|
||||
// Remove PKCS7 padding
|
||||
unpaddedText, err := pkcs7UnpadWeCom(plainText)
|
||||
if err != nil {
|
||||
lastByte := -1
|
||||
if len(plainText) > 0 {
|
||||
lastByte = int(plainText[len(plainText)-1])
|
||||
}
|
||||
logger.ErrorCF("wecom_common", "PKCS7 unpad failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"plain_len": len(plainText),
|
||||
"last_byte": lastByte,
|
||||
})
|
||||
return "", fmt.Errorf("failed to unpad: %w", err)
|
||||
}
|
||||
plainText = unpaddedText
|
||||
|
||||
// Parse message structure
|
||||
// Format: random(16) + msg_len(4) + msg + receiveid
|
||||
if len(plainText) < 20 {
|
||||
return "", fmt.Errorf("decrypted message too short")
|
||||
}
|
||||
|
||||
msgLen := binary.BigEndian.Uint32(plainText[16:20])
|
||||
logger.DebugCF("wecom_common", "Message structure parsed", map[string]interface{}{
|
||||
"msg_len": msgLen,
|
||||
"plain_len": len(plainText),
|
||||
"total_expected": 20 + int(msgLen),
|
||||
})
|
||||
|
||||
if int(msgLen) > len(plainText)-20 {
|
||||
return "", fmt.Errorf("invalid message length: %d > %d", msgLen, len(plainText)-20)
|
||||
}
|
||||
|
||||
msg := plainText[20 : 20+msgLen]
|
||||
|
||||
// Verify receiveid if provided
|
||||
if receiveid != "" && len(plainText) > 20+int(msgLen) {
|
||||
actualReceiveID := string(plainText[20+msgLen:])
|
||||
logger.DebugCF("wecom_common", "ReceiveID verification", map[string]interface{}{
|
||||
"expected": receiveid,
|
||||
"actual": actualReceiveID,
|
||||
})
|
||||
if actualReceiveID != receiveid {
|
||||
return "", fmt.Errorf("receiveid mismatch: expected %s, got %s", receiveid, actualReceiveID)
|
||||
}
|
||||
}
|
||||
|
||||
logger.DebugCF("wecom_common", "Decryption successful", map[string]interface{}{
|
||||
"msg_len": len(msg),
|
||||
})
|
||||
return string(msg), nil
|
||||
}
|
||||
|
||||
// pkcs7UnpadWeCom removes PKCS7 padding with validation
|
||||
// WeCom uses block size of 32 (not standard AES block size of 16)
|
||||
const wecomBlockSize = 32
|
||||
|
||||
func pkcs7UnpadWeCom(data []byte) ([]byte, error) {
|
||||
if len(data) == 0 {
|
||||
return data, nil
|
||||
}
|
||||
padding := int(data[len(data)-1])
|
||||
// WeCom uses 32-byte block size for PKCS7 padding
|
||||
if padding == 0 || padding > wecomBlockSize {
|
||||
return nil, fmt.Errorf("invalid padding size: %d", padding)
|
||||
}
|
||||
if padding > len(data) {
|
||||
return nil, fmt.Errorf("padding size larger than data")
|
||||
}
|
||||
// Verify all padding bytes
|
||||
for i := 0; i < padding; i++ {
|
||||
if data[len(data)-1-i] != byte(padding) {
|
||||
return nil, fmt.Errorf("invalid padding byte at position %d", i)
|
||||
}
|
||||
}
|
||||
return data[:len(data)-padding], nil
|
||||
}
|
||||
Reference in New Issue
Block a user