🔄 卡若AI 同步 2026-03-13 23:06 | 更新:火炬、总索引与入口、运营中枢、运营中枢工作台 | 排除 >20MB: 11 个

This commit is contained in:
2026-03-13 23:06:19 +08:00
parent 7885fa27c5
commit d52f030646
20 changed files with 1582 additions and 3 deletions

View File

@@ -0,0 +1,172 @@
---
name: 艾叶 IM Bridge
description: 多平台 IM 消息网关。将个人微信、企业微信、飞书、WhatsApp、网页聊天等平台的消息桥接到卡若AI实现跨平台 AI 对话。使用本技能当需要:(1) 配置/启动 IM 桥接 (2) 新增聊天通道 (3) 排查 IM 通道问题 (4) 对接新的消息平台
triggers: 艾叶、IM、聊天对接、消息网关、微信对接、企业微信对接、飞书对接、WhatsApp对接、网页聊天、IM桥接、通道配置、艾叶IM
owner: 火炬
group: 火
version: "1.0"
updated: "2026-03-13"
---
# 艾叶 IM Bridge
## 概述
艾叶是卡若AI的多平台 IM 消息网关,参考 OpenClaw 三层架构设计Gateway → Channel → LLM让任何聊天平台的消息都能路由到卡若AI进行对话然后把 AI 回复推回对应平台。
**核心理念**:一个网关,任何平台,同一个 AI。
## 架构
```
个人微信 / 企业微信 / 飞书 / WhatsApp / 网页
┌─────────▼─────────┐
│ 艾叶 IM Bridge │ ← Channel Layer通道适配
│ (FastAPI:18900) │
└─────────┬─────────┘
│ HTTP POST /v1/chat
┌─────────▼─────────┐
│ 卡若AI 网关 │ ← LLM LayerAI 推理)
│ (FastAPI:18080) │
└───────────────────┘
```
## 源代码位置
```
/Users/karuo/Documents/个人/卡若AI/运营中枢/scripts/aiye_im_bridge/
```
## 支持的通道
| 通道 | 对接方式 | Webhook 路径 | 状态 |
|:---|:---|:---|:---|
| 个人微信 | 中间件 Webhook兼容存客宝/WeChatFerry/ComWeChatBot | `/webhook/wechat_personal` | ✅ 就绪 |
| 企业微信 | 官方 API 回调 | `/webhook/wechat_work` | ✅ 就绪 |
| 飞书 | 事件订阅回调 | `/webhook/feishu` | ✅ 就绪 |
| WhatsApp | Cloud API Webhook | `/webhook/whatsapp` | ✅ 就绪 |
| 网页聊天 | REST + WebSocket | `/api/web/chat` `/ws/web/chat` `/chat` | ✅ 就绪 |
## 快速开始
### 1. 启动
```bash
cd 运营中枢/scripts/aiye_im_bridge
bash start.sh # 默认端口 18900
bash start.sh 19000 # 指定端口
```
### 2. 配置通道
编辑 `config/channels.yaml`,按需启用通道并填入对应平台的凭证。首次运行会自动从 `channels.example.yaml` 复制一份。
### 3. 验证
- 访问 `http://localhost:18900` 查看欢迎页
- 访问 `http://localhost:18900/chat` 打开网页聊天
- 访问 `http://localhost:18900/status` 查看通道状态
## 各通道配置说明
### 个人微信
需要一个微信协议中间件存客宝、WeChatFerry、ComWeChatBot 等),中间件负责微信登录和消息抓取,艾叶只做 HTTP 桥接:
1. 中间件将消息 POST 到 `http://艾叶地址/webhook/wechat_personal`
2. 艾叶处理后通过 `callback_url` 回调中间件发送回复
### 企业微信
1. 在企业微信管理后台创建自建应用
2. 设置回调 URL 为 `http(s)://你的域名/webhook/wechat_work`
3.`channels.yaml` 填入 `corp_id``agent_id``secret``token``encoding_aes_key`
### 飞书
1. 在飞书开放平台创建应用
2. 开启「机器人」能力
3. 事件订阅地址设为 `http(s)://你的域名/webhook/feishu`
4. 订阅事件 `im.message.receive_v1`
5.`channels.yaml` 填入 `app_id``app_secret``verification_token`
### WhatsApp
1. 在 Meta 开发者后台配置 WhatsApp Business API
2. Webhook URL 设为 `http(s)://你的域名/webhook/whatsapp`
3.`channels.yaml` 填入 `phone_number_id``access_token``verify_token`
### 网页聊天
默认启用。访问 `/chat` 即可使用内置聊天界面,也可通过 REST API 或 WebSocket 集成到自己的系统。
## 扩展新通道
继承 `core/channel_base.py``ChannelBase`,实现以下方法:
```python
class MyChannel(ChannelBase):
@property
def platform(self) -> str:
return "my_platform"
async def start(self) -> None: ...
async def stop(self) -> None: ...
async def send(self, msg: OutboundMessage) -> bool: ...
def register_routes(self, app) -> None: ...
```
然后在 `main.py``_register_channels()` 中注册即可。
## 聊天命令
在任何通道中发送:
- `/reset` — 重置对话上下文
- `/status` — 查看当前会话状态
- `/help` — 查看可用命令
## 管理接口
| 路径 | 方法 | 说明 |
|:---|:---|:---|
| `/` | GET | 欢迎页 |
| `/status` | GET | 通道状态 |
| `/health` | GET | 健康检查 |
| `/chat` | GET | 网页聊天界面 |
| `/docs` | GET | API 文档Swagger |
## 目录结构
```
aiye_im_bridge/
├── main.py # 主入口
├── start.sh # 启动脚本
├── requirements.txt # 依赖
├── config/
│ ├── channels.yaml # 通道配置(不入库)
│ └── channels.example.yaml # 配置示例
├── core/
│ ├── channel_base.py # Channel 基类
│ ├── router.py # 消息路由
│ ├── session.py # 会话管理
│ └── bridge.py # 网关桥接
└── channels/
├── wechat_personal.py # 个人微信
├── wechat_work.py # 企业微信
├── feishu.py # 飞书
├── whatsapp.py # WhatsApp
└── web.py # 网页聊天
```
## 依赖
- Python 3.10+
- fastapi、uvicorn、httpx、pyyaml、websockets
- 卡若AI 网关运行中(默认 `http://127.0.0.1:18080`
## 与消息中枢的关系
- **消息中枢**Clawdbot/MoltbotTypeScriptOpenClaw 框架,重型多通道 AI 助手
- **艾叶**Python轻量 Webhook 桥接专注于把消息接到卡若AI网关
两者可共存,艾叶更适合快速对接新平台、轻量部署。

View File

@@ -1,7 +1,7 @@
# 卡若AI 技能注册表Skill Registry
> **一张表查所有技能**。任何 AI 拿到这张表,就能按关键词找到对应技能的 SKILL.md 路径并执行。
> 70 技能 | 14 成员 | 5 负责人
> 71 技能 | 14 成员 | 5 负责人
> 版本5.5 | 更新2026-03-13
>
> **技能配置、安装、删除、掌管人登记** → 见 **`运营中枢/工作台/01_技能控制台.md`**。
@@ -125,6 +125,7 @@
| F01a | 前端开发 | 火炬 | **前端开发、毛玻璃、神射手风格、毛狐狸风格、前端标准、苹果毛玻璃** | `04_卡火/火炬_全栈消息/前端开发/SKILL.md` | 苹果毛玻璃风格 + 神射手/毛狐狸前端标准;官网/全站前端走本 Skill |
| F01b | 全栈测试 | 火炬 | **全栈测试、功能测试、回归测试、深度测试、E2E测试、API测试、发布测试、测试验收** | `04_卡火/火炬_全栈消息/全栈开发/全栈测试/SKILL.md` | 功能开发后系统化验收:前端/后端/数据库/脚本/发布引擎五维测试;**每完成一个功能必须调用** |
| F02 | 消息中枢 | 火炬 | WhatsApp、Telegram | `04_卡火/火炬_全栈消息/消息中枢/SKILL.md` | 多平台消息聚合 |
| F02a | **艾叶 IM Bridge** | 火炬 | **艾叶、IM、聊天对接、消息网关、微信对接、企业微信对接、飞书对接、WhatsApp对接、网页聊天、IM桥接、通道配置、艾叶IM** | `04_卡火/火炬_全栈消息/艾叶/SKILL.md` | 多平台 IM 网关:个人微信/企业微信/飞书/WhatsApp/网页→卡若AI 对话 |
| F03 | 读书笔记 | 火炬 | 拆解这本书、五行拆书 | `04_卡火/火炬_全栈消息/读书笔记/SKILL.md` | 五行框架拆书 |
| F04 | 文档清洗 | 火炬 | 文档清洗、PDF转MD | `04_卡火/火炬_全栈消息/文档清洗/SKILL.md` | 批量文档格式转换 |
| F05 | 代码修复 | 火锤 | 代码修复、Bug | `04_卡火/火锤_代码修复/代码修复/SKILL.md` | 定位 Bug 并修复 |
@@ -175,6 +176,6 @@
| 金 | 卡资 | 2 | 21 |
| 水 | 卡人 | 3 | 13 |
| 木 | 卡木 | 3 | 13 |
| 火 | 卡火 | 4 | 15 |
| 火 | 卡火 | 4 | 16 |
| 土 | 卡土 | 4 | 8 |
| **合计** | **5** | **14** | **70** |
| **合计** | **5** | **14** | **71** |

View File

@@ -0,0 +1,162 @@
"""
艾叶 IM Bridge — 飞书通道
对接方式:飞书应用事件订阅(接收 im.message.receive_v1 事件)
配置项:
app_id: 飞书应用 App ID
app_secret: 飞书应用 App Secret
verification_token: 事件订阅验证 Token
encrypt_key: 事件加密密钥(可选)
Webhook: POST /webhook/feishu
"""
from __future__ import annotations
import json
import logging
import time
from typing import Any
import httpx
from fastapi import Request
from core.channel_base import ChannelBase, InboundMessage, MessageType, OutboundMessage
logger = logging.getLogger("aiye.channel.feishu")
class FeishuChannel(ChannelBase):
"""飞书通道"""
_tenant_access_token: str = ""
_token_expires: float = 0
@property
def platform(self) -> str:
return "feishu"
async def start(self) -> None:
logger.info("飞书通道已就绪Webhook: /webhook/feishu")
async def stop(self) -> None:
logger.info("飞书通道已停止")
async def _get_tenant_token(self) -> str:
if self._tenant_access_token and time.time() < self._token_expires:
return self._tenant_access_token
app_id = self._config.get("app_id", "")
app_secret = self._config.get("app_secret", "")
if not app_id or not app_secret:
return ""
try:
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.post(
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
json={"app_id": app_id, "app_secret": app_secret},
)
data = resp.json()
if data.get("code") == 0:
self._tenant_access_token = data["tenant_access_token"]
self._token_expires = time.time() + data.get("expire", 7200) - 300
return self._tenant_access_token
logger.warning("飞书 token 获取失败: %s", data)
except Exception as e:
logger.error("飞书 token 异常: %s", e)
return ""
async def send(self, msg: OutboundMessage) -> bool:
token = await self._get_tenant_token()
if not token:
logger.warning("飞书无 tenant_access_token无法发送")
return False
receive_id_type = msg.extra.get("receive_id_type", "open_id")
payload = {
"receive_id": msg.chat_id,
"msg_type": "text",
"content": json.dumps({"text": msg.content}, ensure_ascii=False),
}
try:
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.post(
f"https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type={receive_id_type}",
json=payload,
headers={"Authorization": f"Bearer {token}"},
)
data = resp.json()
if data.get("code") == 0:
return True
logger.warning("飞书发送失败: %s", data)
except Exception as e:
logger.error("飞书发送异常: %s", e)
return False
def register_routes(self, app: Any) -> None:
channel = self
_processed_ids: set = set()
@app.post("/webhook/feishu")
async def feishu_webhook(request: Request):
try:
data = await request.json()
except Exception:
return {"code": 400}
# URL 验证(飞书后台配置回调时调用)
if data.get("type") == "url_verification":
return {"challenge": data.get("challenge", "")}
# 事件回调
header = data.get("header", {})
event = data.get("event", {})
event_type = header.get("event_type", "")
if event_type != "im.message.receive_v1":
return {"code": 0}
message = event.get("message", {})
msg_id = message.get("message_id", "")
if msg_id in _processed_ids:
return {"code": 0}
_processed_ids.add(msg_id)
if len(_processed_ids) > 1000:
_processed_ids.clear()
msg_type = message.get("message_type", "")
sender = event.get("sender", {}).get("sender_id", {})
open_id = sender.get("open_id", "")
chat_id = message.get("chat_id", "") or open_id
content_str = message.get("content", "{}")
try:
content_obj = json.loads(content_str)
text = content_obj.get("text", "")
except Exception:
text = content_str
if msg_type != "text" or not text.strip():
return {"code": 0}
# 去掉 @bot 的 mention
mentions = message.get("mentions", [])
for m in mentions:
key = m.get("key", "")
if key:
text = text.replace(key, "").strip()
inbound = InboundMessage(
channel_id=channel.channel_id,
platform=channel.platform,
sender_id=open_id,
chat_id=chat_id,
content=text,
raw=data,
)
reply = await channel.dispatch(inbound)
if reply:
reply.extra["receive_id_type"] = (
"chat_id" if message.get("chat_type") == "group" else "open_id"
)
await channel.send(reply)
return {"code": 0}

View File

@@ -0,0 +1,214 @@
"""
艾叶 IM Bridge — 个人网页通道
对接方式:
1. REST API — POST /api/web/chat
2. WebSocket — ws://host:port/ws/web/chat
提供即开即用的网页聊天入口,也可作为第三方系统接入的通用 API。
配置项:
allowed_origins: ["*"] # CORS 白名单
"""
from __future__ import annotations
import json
import logging
import uuid
from typing import Any, Dict, Set
from fastapi import Request, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse
from core.channel_base import ChannelBase, InboundMessage, MessageType, OutboundMessage
logger = logging.getLogger("aiye.channel.web")
class WebChannel(ChannelBase):
"""个人网页通道REST + WebSocket"""
_ws_connections: Dict[str, WebSocket] = {}
@property
def platform(self) -> str:
return "web"
async def start(self) -> None:
logger.info("网页通道已就绪API: /api/web/chat | WS: /ws/web/chat")
async def stop(self) -> None:
for ws in self._ws_connections.values():
try:
await ws.close()
except Exception:
pass
self._ws_connections.clear()
logger.info("网页通道已停止")
async def send(self, msg: OutboundMessage) -> bool:
ws = self._ws_connections.get(msg.chat_id)
if ws:
try:
await ws.send_json({"type": "reply", "content": msg.content})
return True
except Exception as e:
logger.warning("WebSocket 发送失败: %s", e)
return True
def register_routes(self, app: Any) -> None:
channel = self
@app.post("/api/web/chat")
async def web_chat_api(request: Request):
"""REST 聊天接口:{"message": "你好", "session_id": "可选"}"""
try:
data = await request.json()
except Exception:
return {"code": 400, "msg": "invalid json"}
message = data.get("message", "").strip()
session_id = data.get("session_id", "") or str(uuid.uuid4())[:8]
if not message:
return {"code": 400, "msg": "empty message"}
inbound = InboundMessage(
channel_id=channel.channel_id,
platform=channel.platform,
sender_id=session_id,
sender_name=data.get("name", "网页用户"),
chat_id=session_id,
content=message,
)
reply = await channel.dispatch(inbound)
return {
"code": 0,
"reply": reply.content if reply else "",
"session_id": session_id,
}
@app.websocket("/ws/web/chat")
async def web_chat_ws(websocket: WebSocket):
"""WebSocket 聊天接口"""
await websocket.accept()
ws_id = str(uuid.uuid4())[:8]
channel._ws_connections[ws_id] = websocket
await websocket.send_json({"type": "connected", "session_id": ws_id})
logger.info("WebSocket 连接: %s", ws_id)
try:
while True:
raw = await websocket.receive_text()
try:
data = json.loads(raw)
message = data.get("message", "").strip()
except Exception:
message = raw.strip()
if not message:
continue
inbound = InboundMessage(
channel_id=channel.channel_id,
platform=channel.platform,
sender_id=ws_id,
sender_name="网页用户",
chat_id=ws_id,
content=message,
)
reply = await channel.dispatch(inbound)
if reply:
await websocket.send_json({
"type": "reply",
"content": reply.content,
})
except WebSocketDisconnect:
logger.info("WebSocket 断开: %s", ws_id)
except Exception as e:
logger.warning("WebSocket 异常: %s", e)
finally:
channel._ws_connections.pop(ws_id, None)
@app.get("/chat", response_class=HTMLResponse)
async def web_chat_page():
"""内嵌网页聊天界面"""
html_path = channel._config.get("html_path", "")
if html_path:
try:
from pathlib import Path
return Path(html_path).read_text(encoding="utf-8")
except Exception:
pass
return _DEFAULT_CHAT_HTML
_DEFAULT_CHAT_HTML = """<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>艾叶 · 卡若AI 聊天</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#f0f2f5;height:100vh;display:flex;flex-direction:column}
.header{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:#fff;padding:16px 20px;font-size:18px;font-weight:600;text-align:center}
.chat-box{flex:1;overflow-y:auto;padding:16px 20px;display:flex;flex-direction:column;gap:12px}
.msg{max-width:80%;padding:10px 14px;border-radius:16px;font-size:15px;line-height:1.5;word-break:break-word;animation:fadeIn .3s}
.msg.user{align-self:flex-end;background:#667eea;color:#fff;border-bottom-right-radius:4px}
.msg.ai{align-self:flex-start;background:#fff;color:#333;border-bottom-left-radius:4px;box-shadow:0 1px 3px rgba(0,0,0,.1)}
.msg.ai pre{background:#f5f5f5;padding:8px;border-radius:6px;overflow-x:auto;font-size:13px;margin:6px 0}
.input-area{display:flex;gap:8px;padding:12px 20px;background:#fff;border-top:1px solid #e0e0e0}
.input-area input{flex:1;border:1px solid #ddd;border-radius:20px;padding:10px 16px;font-size:15px;outline:none}
.input-area input:focus{border-color:#667eea}
.input-area button{background:#667eea;color:#fff;border:none;border-radius:20px;padding:10px 20px;font-size:15px;cursor:pointer}
.input-area button:hover{background:#5a6fd6}
.typing{align-self:flex-start;color:#999;font-size:13px;padding:4px 14px}
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
</style>
</head>
<body>
<div class="header">艾叶 · 卡若AI</div>
<div class="chat-box" id="chatBox">
<div class="msg ai">你好我是卡若AI通过艾叶 IM 为你服务。有什么可以帮你的?</div>
</div>
<div class="input-area">
<input id="msgInput" placeholder="输入消息..." autocomplete="off">
<button onclick="sendMsg()">发送</button>
</div>
<script>
const chatBox=document.getElementById('chatBox'),input=document.getElementById('msgInput');
let ws,sessionId='';
function connect(){
const proto=location.protocol==='https:'?'wss:':'ws:';
ws=new WebSocket(`${proto}//${location.host}/ws/web/chat`);
ws.onopen=()=>console.log('connected');
ws.onmessage=e=>{
const d=JSON.parse(e.data);
if(d.type==='connected'){sessionId=d.session_id;return}
if(d.type==='reply'){removeTyping();addMsg(d.content,'ai')}
};
ws.onclose=()=>setTimeout(connect,3000);
}
function addMsg(text,who){
const d=document.createElement('div');d.className='msg '+who;
d.innerHTML=who==='ai'?text.replace(/\\n/g,'<br>'):escHtml(text);
chatBox.appendChild(d);chatBox.scrollTop=chatBox.scrollHeight;
}
function showTyping(){
if(document.getElementById('typing'))return;
const d=document.createElement('div');d.id='typing';d.className='typing';d.textContent='卡若AI 正在思考...';
chatBox.appendChild(d);chatBox.scrollTop=chatBox.scrollHeight;
}
function removeTyping(){const t=document.getElementById('typing');if(t)t.remove()}
function escHtml(s){return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}
function sendMsg(){
const m=input.value.trim();if(!m||!ws||ws.readyState!==1)return;
addMsg(m,'user');ws.send(JSON.stringify({message:m}));input.value='';showTyping();
}
input.addEventListener('keydown',e=>{if(e.key==='Enter')sendMsg()});
connect();
</script>
</body>
</html>"""

View File

@@ -0,0 +1,100 @@
"""
艾叶 IM Bridge — 个人微信通道
对接方式:通过 Webhook 回调接收消息兼容存客宝、WeChatFerry、ComWeChatBot 等中间件)。
中间件负责微信协议层,艾叶只做消息收发的 HTTP 桥接。
接口约定:
- POST /webhook/wechat_personal 接收消息推送
- 中间件需将消息 POST 到此地址,格式见下方
- 回复通过中间件的回调 URL 发送
消息推送格式JSON
{
"msg_id": "xxx",
"from_user": "wxid_xxx",
"from_name": "昵称",
"to_user": "wxid_yyy",
"room_id": "", // 群聊为群 ID私聊为空
"room_name": "",
"content": "你好",
"msg_type": 1, // 1=文本, 3=图片, 34=语音, 43=视频, 49=链接
"timestamp": 1710000000
}
"""
from __future__ import annotations
import logging
from typing import Any
import httpx
from fastapi import Request
from core.channel_base import ChannelBase, InboundMessage, MessageType, OutboundMessage
logger = logging.getLogger("aiye.channel.wechat_personal")
class WeChatPersonalChannel(ChannelBase):
"""个人微信通道Webhook 模式)"""
@property
def platform(self) -> str:
return "wechat_personal"
async def start(self) -> None:
logger.info("个人微信通道已就绪,等待中间件推送消息到 /webhook/wechat_personal")
async def stop(self) -> None:
logger.info("个人微信通道已停止")
async def send(self, msg: OutboundMessage) -> bool:
callback_url = self._config.get("callback_url", "")
if not callback_url:
logger.warning("个人微信通道未配置 callback_url无法发送回复")
return False
payload = {
"to_user": msg.chat_id,
"content": msg.content,
"msg_type": "text",
}
try:
async with httpx.AsyncClient(timeout=15) as client:
resp = await client.post(callback_url, json=payload)
if resp.status_code == 200:
return True
logger.warning("微信回复发送失败: %d %s", resp.status_code, resp.text[:100])
except Exception as e:
logger.error("微信回复发送异常: %s", e)
return False
def register_routes(self, app: Any) -> None:
channel = self
@app.post("/webhook/wechat_personal")
async def wechat_personal_webhook(request: Request):
try:
data = await request.json()
except Exception:
return {"code": 400, "msg": "invalid json"}
msg_type_map = {1: MessageType.TEXT, 3: MessageType.IMAGE, 34: MessageType.VOICE}
wx_msg_type = data.get("msg_type", 1)
inbound = InboundMessage(
channel_id=channel.channel_id,
platform=channel.platform,
sender_id=data.get("from_user", ""),
sender_name=data.get("from_name", ""),
chat_id=data.get("room_id") or data.get("from_user", ""),
chat_name=data.get("room_name", ""),
content=data.get("content", ""),
msg_type=msg_type_map.get(wx_msg_type, MessageType.TEXT),
raw=data,
)
reply = await channel.dispatch(inbound)
if reply:
await channel.send(reply)
return {"code": 0, "msg": "ok", "reply": reply.content}
return {"code": 0, "msg": "no reply"}

View File

@@ -0,0 +1,154 @@
"""
艾叶 IM Bridge — 企业微信通道
对接方式:企业微信应用消息回调(接收消息事件 + 被动回复 / 主动发消息 API
配置项:
corp_id: 企业 ID
agent_id: 应用 AgentId
secret: 应用 Secret
token: 回调 Token用于验签
encoding_aes_key: 回调 EncodingAESKey用于解密
Webhook: POST /webhook/wechat_work
验证: GET /webhook/wechat_work?echostr=xxx&msg_signature=xxx&timestamp=xxx&nonce=xxx
"""
from __future__ import annotations
import hashlib
import logging
import time
from typing import Any, Optional
from xml.etree import ElementTree
import httpx
from fastapi import Request, Response
from core.channel_base import ChannelBase, InboundMessage, MessageType, OutboundMessage
logger = logging.getLogger("aiye.channel.wechat_work")
class WeChatWorkChannel(ChannelBase):
"""企业微信通道"""
_access_token: str = ""
_token_expires: float = 0
@property
def platform(self) -> str:
return "wechat_work"
async def start(self) -> None:
logger.info("企业微信通道已就绪Webhook: /webhook/wechat_work")
async def stop(self) -> None:
logger.info("企业微信通道已停止")
async def _get_access_token(self) -> str:
if self._access_token and time.time() < self._token_expires:
return self._access_token
corp_id = self._config.get("corp_id", "")
secret = self._config.get("secret", "")
if not corp_id or not secret:
return ""
try:
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.get(
"https://qyapi.weixin.qq.com/cgi-bin/gettoken",
params={"corpid": corp_id, "corpsecret": secret},
)
data = resp.json()
if data.get("errcode", 0) == 0:
self._access_token = data["access_token"]
self._token_expires = time.time() + data.get("expires_in", 7200) - 300
return self._access_token
logger.warning("获取企业微信 access_token 失败: %s", data)
except Exception as e:
logger.error("获取企业微信 access_token 异常: %s", e)
return ""
async def send(self, msg: OutboundMessage) -> bool:
token = await self._get_access_token()
if not token:
logger.warning("企业微信无 access_token无法发送")
return False
agent_id = self._config.get("agent_id", "")
payload = {
"touser": msg.chat_id,
"msgtype": "text",
"agentid": agent_id,
"text": {"content": msg.content},
}
try:
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.post(
f"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={token}",
json=payload,
)
data = resp.json()
if data.get("errcode", 0) == 0:
return True
logger.warning("企业微信发送失败: %s", data)
except Exception as e:
logger.error("企业微信发送异常: %s", e)
return False
def _verify_signature(self, token: str, timestamp: str, nonce: str, signature: str) -> bool:
cfg_token = self._config.get("token", "")
if not cfg_token:
return True
items = sorted([cfg_token, timestamp, nonce])
sha1 = hashlib.sha1("".join(items).encode("utf-8")).hexdigest()
return sha1 == signature
def register_routes(self, app: Any) -> None:
channel = self
@app.get("/webhook/wechat_work")
async def wechat_work_verify(
msg_signature: str = "",
timestamp: str = "",
nonce: str = "",
echostr: str = "",
):
"""URL 验证(企业微信后台配置回调时调用)"""
if channel._verify_signature(
channel._config.get("token", ""), timestamp, nonce, msg_signature
):
return Response(content=echostr, media_type="text/plain")
return Response(content="forbidden", status_code=403)
@app.post("/webhook/wechat_work")
async def wechat_work_webhook(request: Request):
"""接收企业微信消息事件"""
body = await request.body()
try:
root = ElementTree.fromstring(body)
msg_type = (root.findtext("MsgType") or "").strip()
from_user = (root.findtext("FromUserName") or "").strip()
content = (root.findtext("Content") or "").strip()
except Exception:
try:
data = await request.json()
msg_type = data.get("MsgType", "text")
from_user = data.get("FromUserName", "")
content = data.get("Content", "")
except Exception:
return Response(content="", media_type="text/xml")
if msg_type != "text" or not content:
return Response(content="", media_type="text/xml")
inbound = InboundMessage(
channel_id=channel.channel_id,
platform=channel.platform,
sender_id=from_user,
chat_id=from_user,
content=content,
raw={"body": body.decode("utf-8", errors="replace")},
)
reply = await channel.dispatch(inbound)
if reply:
await channel.send(reply)
return Response(content="", media_type="text/xml")

View File

@@ -0,0 +1,132 @@
"""
艾叶 IM Bridge — WhatsApp 通道
对接方式:
1. WhatsApp Business API (Cloud API) — Webhook 回调
2. 可扩展对接 OpenClaw/Moltbot Gateway 的 WebSocket
当前实现WhatsApp Cloud API Webhook 模式
配置项:
phone_number_id: WhatsApp Business 电话号码 ID
access_token: Meta Graph API 长期令牌
verify_token: Webhook 验证令牌(自定义字符串)
api_version: Graph API 版本(默认 v21.0
Webhook: POST /webhook/whatsapp
验证: GET /webhook/whatsapp
"""
from __future__ import annotations
import logging
from typing import Any
import httpx
from fastapi import Request, Response
from core.channel_base import ChannelBase, InboundMessage, MessageType, OutboundMessage
logger = logging.getLogger("aiye.channel.whatsapp")
class WhatsAppChannel(ChannelBase):
"""WhatsApp Cloud API 通道"""
@property
def platform(self) -> str:
return "whatsapp"
async def start(self) -> None:
logger.info("WhatsApp 通道已就绪Webhook: /webhook/whatsapp")
async def stop(self) -> None:
logger.info("WhatsApp 通道已停止")
async def send(self, msg: OutboundMessage) -> bool:
phone_id = self._config.get("phone_number_id", "")
token = self._config.get("access_token", "")
api_ver = self._config.get("api_version", "v21.0")
if not phone_id or not token:
logger.warning("WhatsApp 未配置 phone_number_id / access_token")
return False
url = f"https://graph.facebook.com/{api_ver}/{phone_id}/messages"
payload = {
"messaging_product": "whatsapp",
"to": msg.chat_id,
"type": "text",
"text": {"body": msg.content},
}
try:
async with httpx.AsyncClient(timeout=15) as client:
resp = await client.post(
url,
json=payload,
headers={"Authorization": f"Bearer {token}"},
)
if resp.status_code == 200:
return True
logger.warning("WhatsApp 发送失败: %d %s", resp.status_code, resp.text[:200])
except Exception as e:
logger.error("WhatsApp 发送异常: %s", e)
return False
def register_routes(self, app: Any) -> None:
channel = self
@app.get("/webhook/whatsapp")
async def whatsapp_verify(
request: Request,
):
"""Meta Webhook 验证"""
params = request.query_params
mode = params.get("hub.mode", "")
token = params.get("hub.verify_token", "")
challenge = params.get("hub.challenge", "")
if mode == "subscribe" and token == channel._config.get("verify_token", ""):
return Response(content=challenge, media_type="text/plain")
return Response(content="forbidden", status_code=403)
@app.post("/webhook/whatsapp")
async def whatsapp_webhook(request: Request):
try:
data = await request.json()
except Exception:
return {"status": "error"}
entries = data.get("entry", [])
for entry in entries:
changes = entry.get("changes", [])
for change in changes:
value = change.get("value", {})
messages = value.get("messages", [])
contacts = value.get("contacts", [])
contact_map = {
c.get("wa_id", ""): c.get("profile", {}).get("name", "")
for c in contacts
}
for wa_msg in messages:
msg_type = wa_msg.get("type", "")
from_id = wa_msg.get("from", "")
text = ""
if msg_type == "text":
text = wa_msg.get("text", {}).get("body", "")
if not text.strip():
continue
inbound = InboundMessage(
channel_id=channel.channel_id,
platform=channel.platform,
sender_id=from_id,
sender_name=contact_map.get(from_id, ""),
chat_id=from_id,
content=text,
raw=wa_msg,
)
reply = await channel.dispatch(inbound)
if reply:
await channel.send(reply)
return {"status": "ok"}

View File

@@ -0,0 +1,70 @@
# ──────────────────────────────────────────────
# 艾叶 IM Bridge 配置文件
# 复制为 channels.yaml 后按需修改
# ──────────────────────────────────────────────
# 卡若AI 网关连接
gateway:
url: "http://127.0.0.1:18080" # 卡若AI 网关地址
api_key: "" # 网关 API Key若网关启用鉴权
timeout: 60 # 请求超时秒数
# ── 通道配置 ──────────────────────────────────
channels:
# ╔═══════════════════════════════════╗
# ║ 个人微信Webhook 模式) ║
# ╚═══════════════════════════════════╝
# 需要微信中间件(存客宝/WeChatFerry/ComWeChatBot配合
# 中间件将消息 POST 到 /webhook/wechat_personal
wechat_personal:
enabled: false
# 中间件的回复接口(艾叶把 AI 回复 POST 到这里)
callback_url: "http://127.0.0.1:9000/api/send"
# ╔═══════════════════════════════════╗
# ║ 企业微信 ║
# ╚═══════════════════════════════════╝
# 在企业微信管理后台创建自建应用,配置回调 URL 为:
# http(s)://your-domain/webhook/wechat_work
wechat_work:
enabled: false
corp_id: "" # 企业 ID
agent_id: "" # 应用 AgentId
secret: "" # 应用 Secret
token: "" # 回调 Token
encoding_aes_key: "" # 回调 EncodingAESKey
# ╔═══════════════════════════════════╗
# ║ 飞书 ║
# ╚═══════════════════════════════════╝
# 在飞书开放平台创建应用,事件订阅地址:
# http(s)://your-domain/webhook/feishu
# 订阅事件: im.message.receive_v1
feishu:
enabled: false
app_id: "" # 飞书 App ID
app_secret: "" # 飞书 App Secret
verification_token: "" # 事件订阅验证 Token
encrypt_key: "" # 事件加密密钥(可选)
# ╔═══════════════════════════════════╗
# ║ WhatsApp (Cloud API) ║
# ╚═══════════════════════════════════╝
# 在 Meta 开发者后台配置 Webhook:
# http(s)://your-domain/webhook/whatsapp
whatsapp:
enabled: false
phone_number_id: "" # WhatsApp Business 电话号码 ID
access_token: "" # Graph API 长期令牌
verify_token: "aiye_whatsapp_verify" # Webhook 验证令牌
api_version: "v21.0"
# ╔═══════════════════════════════════╗
# ║ 个人网页聊天 ║
# ╚═══════════════════════════════════╝
# 默认启用,提供 /chat 网页 + REST API + WebSocket
web:
enabled: true
allowed_origins: ["*"]
# html_path: "" # 自定义聊天页面路径(可选)

View File

@@ -0,0 +1,67 @@
"""
艾叶 IM Bridge — 网关桥接
将用户消息通过 HTTP 转发到卡若AI 网关 (/v1/chat),拿到 AI 回复。
支持带会话上下文的多轮对话。
"""
from __future__ import annotations
import logging
import os
from typing import Optional
import httpx
from .session import Session
logger = logging.getLogger("aiye.bridge")
DEFAULT_GATEWAY_URL = "http://127.0.0.1:18080"
DEFAULT_TIMEOUT = 60
class KaruoGatewayBridge:
"""调用卡若AI网关获取 AI 回复。"""
def __init__(self, gateway_url: str = "", api_key: str = "", timeout: int = 0):
self.gateway_url = (
gateway_url
or os.environ.get("AIYE_GATEWAY_URL", "").strip()
or DEFAULT_GATEWAY_URL
).rstrip("/")
self.api_key = api_key or os.environ.get("AIYE_GATEWAY_KEY", "").strip()
self.timeout = timeout or int(os.environ.get("AIYE_GATEWAY_TIMEOUT", str(DEFAULT_TIMEOUT)))
async def ask(self, prompt: str, session: Optional[Session] = None) -> str:
full_prompt = self._build_prompt(prompt, session)
headers = {"Content-Type": "application/json"}
if self.api_key:
headers["X-Karuo-Api-Key"] = self.api_key
try:
async with httpx.AsyncClient(timeout=self.timeout) as client:
resp = await client.post(
f"{self.gateway_url}/v1/chat",
json={"prompt": full_prompt},
headers=headers,
)
if resp.status_code == 200:
data = resp.json()
reply = data.get("reply", "")
if reply and session:
session.add_turn(prompt, reply)
return reply or "抱歉,我暂时无法处理你的问题,请稍后再试。"
logger.warning("Gateway returned %d: %s", resp.status_code, resp.text[:200])
return f"网关返回异常({resp.status_code}),请稍后重试。"
except httpx.TimeoutException:
logger.error("Gateway timeout after %ds", self.timeout)
return "AI 处理超时,请稍后再试或缩短你的问题。"
except Exception as e:
logger.error("Gateway error: %s", e)
return "连接 AI 网关失败,请检查网关是否运行中。"
@staticmethod
def _build_prompt(user_msg: str, session: Optional[Session]) -> str:
if not session or not session.history:
return user_msg
ctx = session.context_summary()
return f"[对话上下文]\n{ctx}\n\n[当前问题]\n{user_msg}"

View File

@@ -0,0 +1,117 @@
"""
艾叶 IM Bridge — Channel 基类
每个通道(微信/飞书/WhatsApp等继承此类实现统一的消息收发接口。
参考 OpenClaw Channel Layer 设计:平台差异在适配器内部消化,对外暴露统一结构。
"""
from __future__ import annotations
import abc
import time
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Callable, Coroutine, Optional
class MessageType(str, Enum):
TEXT = "text"
IMAGE = "image"
VOICE = "voice"
VIDEO = "video"
FILE = "file"
LOCATION = "location"
LINK = "link"
SYSTEM = "system"
@dataclass
class InboundMessage:
"""从平台收到的标准化消息Channel → Router"""
channel_id: str
platform: str
sender_id: str
sender_name: str = ""
chat_id: str = ""
chat_name: str = ""
content: str = ""
msg_type: MessageType = MessageType.TEXT
media_url: str = ""
raw: dict = field(default_factory=dict)
timestamp: float = field(default_factory=time.time)
@dataclass
class OutboundMessage:
"""发往平台的标准化回复Router → Channel"""
channel_id: str
platform: str
chat_id: str
content: str = ""
msg_type: MessageType = MessageType.TEXT
media_url: str = ""
extra: dict = field(default_factory=dict)
MessageHandler = Callable[[InboundMessage], Coroutine[Any, Any, Optional[OutboundMessage]]]
class ChannelBase(abc.ABC):
"""
通道抽象基类。
生命周期:
configure(cfg) → start() → [运行中: on_message 回调] → stop()
子类必须实现:
- platform (属性) 平台标识
- start() 启动连接 / 注册 webhook
- stop() 断开
- send() 发送消息
- register_routes(app) 注册 FastAPI 路由webhook 回调)
"""
def __init__(self, channel_id: str):
self.channel_id = channel_id
self._handler: Optional[MessageHandler] = None
self._config: dict = {}
@property
@abc.abstractmethod
def platform(self) -> str:
...
def configure(self, cfg: dict) -> None:
self._config = cfg
def on_message(self, handler: MessageHandler) -> None:
self._handler = handler
async def dispatch(self, msg: InboundMessage) -> Optional[OutboundMessage]:
if self._handler:
return await self._handler(msg)
return None
@abc.abstractmethod
async def start(self) -> None:
...
@abc.abstractmethod
async def stop(self) -> None:
...
@abc.abstractmethod
async def send(self, msg: OutboundMessage) -> bool:
...
def register_routes(self, app: Any) -> None:
"""子类可覆盖,向 FastAPI app 注册 webhook 路由。"""
pass
@property
def status(self) -> dict:
return {
"channel_id": self.channel_id,
"platform": self.platform,
"configured": bool(self._config),
}

View File

@@ -0,0 +1,80 @@
"""
艾叶 IM Bridge — 消息路由
接收所有通道的标准化消息 → 查/建会话 → 调用网关 → 构造回复。
"""
from __future__ import annotations
import logging
from typing import Optional
from .bridge import KaruoGatewayBridge
from .channel_base import InboundMessage, MessageType, OutboundMessage
from .session import SessionManager
logger = logging.getLogger("aiye.router")
COMMANDS = {
"/reset": "重置当前对话上下文",
"/status": "查看当前会话状态",
"/help": "查看可用命令",
}
class MessageRouter:
"""统一消息路由器Channel → Session → Gateway → Reply"""
def __init__(self, bridge: KaruoGatewayBridge):
self.sessions = SessionManager()
self.bridge = bridge
async def handle(self, msg: InboundMessage) -> Optional[OutboundMessage]:
if msg.msg_type != MessageType.TEXT or not msg.content.strip():
return None
text = msg.content.strip()
session = self.sessions.get_or_create(
channel_id=msg.channel_id,
platform=msg.platform,
chat_id=msg.chat_id or msg.sender_id,
user_id=msg.sender_id,
user_name=msg.sender_name,
)
if text.startswith("/"):
reply = self._handle_command(text, session)
else:
logger.info(
"[%s:%s] %s: %s",
msg.platform,
msg.chat_id or msg.sender_id,
msg.sender_name or msg.sender_id,
text[:80],
)
reply = await self.bridge.ask(text, session)
return OutboundMessage(
channel_id=msg.channel_id,
platform=msg.platform,
chat_id=msg.chat_id or msg.sender_id,
content=reply,
)
@staticmethod
def _handle_command(text: str, session) -> str:
cmd = text.split()[0].lower()
if cmd == "/reset":
session.reset()
return "对话已重置,可以开始新话题了。"
if cmd == "/status":
return (
f"平台: {session.platform}\n"
f"会话: {session.session_id}\n"
f"历史轮数: {len(session.history)}\n"
f"用户: {session.user_name or session.user_id}"
)
if cmd == "/help":
lines = ["艾叶 IM 可用命令:"]
for c, desc in COMMANDS.items():
lines.append(f" {c}{desc}")
return "\n".join(lines)
return f"未知命令: {cmd}。输入 /help 查看可用命令。"

View File

@@ -0,0 +1,93 @@
"""
艾叶 IM Bridge — 会话管理
维护每个用户/群组的对话历史,支持上下文续接与过期清理。
"""
from __future__ import annotations
import time
from dataclasses import dataclass, field
from typing import Dict, List, Tuple
# 会话默认 30 分钟过期(无消息则重置上下文)
SESSION_TTL_SECONDS = 1800
MAX_HISTORY_TURNS = 20
@dataclass
class Session:
session_id: str
channel_id: str
platform: str
chat_id: str
user_id: str
user_name: str = ""
history: List[Tuple[str, str]] = field(default_factory=list)
created_at: float = field(default_factory=time.time)
last_active: float = field(default_factory=time.time)
@property
def is_expired(self) -> bool:
return (time.time() - self.last_active) > SESSION_TTL_SECONDS
def add_turn(self, user_msg: str, ai_reply: str) -> None:
self.history.append((user_msg, ai_reply))
if len(self.history) > MAX_HISTORY_TURNS:
self.history = self.history[-MAX_HISTORY_TURNS:]
self.last_active = time.time()
def reset(self) -> None:
self.history.clear()
self.last_active = time.time()
def context_summary(self) -> str:
if not self.history:
return ""
lines = []
for user_msg, ai_reply in self.history[-5:]:
lines.append(f"用户: {user_msg}")
lines.append(f"AI: {ai_reply}")
return "\n".join(lines)
class SessionManager:
"""内存级会话池,按 session_id 索引。"""
def __init__(self) -> None:
self._sessions: Dict[str, Session] = {}
def _make_id(self, platform: str, chat_id: str, user_id: str) -> str:
return f"{platform}:{chat_id}:{user_id}"
def get_or_create(
self,
channel_id: str,
platform: str,
chat_id: str,
user_id: str,
user_name: str = "",
) -> Session:
sid = self._make_id(platform, chat_id, user_id)
session = self._sessions.get(sid)
if session and session.is_expired:
session.reset()
if not session:
session = Session(
session_id=sid,
channel_id=channel_id,
platform=platform,
chat_id=chat_id,
user_id=user_id,
user_name=user_name,
)
self._sessions[sid] = session
return session
def cleanup_expired(self) -> int:
expired = [k for k, v in self._sessions.items() if v.is_expired]
for k in expired:
del self._sessions[k]
return len(expired)
@property
def active_count(self) -> int:
return sum(1 for v in self._sessions.values() if not v.is_expired)

View File

@@ -0,0 +1,182 @@
"""
艾叶 IM Bridge — 主入口
多平台 IM 消息网关通过卡若AI网关为所有接入平台提供 AI 对话能力。
架构(参考 OpenClaw 三层设计):
个人微信 / 企业微信 / 飞书 / WhatsApp / 网页
┌─────────▼─────────┐
│ 艾叶 IM Bridge │ ← Channel Layer
│ (FastAPI:18900) │
└─────────┬─────────┘
│ HTTP
┌─────────▼─────────┐
│ 卡若AI 网关 │ ← LLM Layer
│ (FastAPI:18080) │
└───────────────────┘
启动:
python main.py
# 或
uvicorn main:app --host 0.0.0.0 --port 18900
"""
from __future__ import annotations
import logging
import os
import sys
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Any, Dict, List
import yaml
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse
from core.bridge import KaruoGatewayBridge
from core.channel_base import ChannelBase
from core.router import MessageRouter
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(name)s] %(levelname)s %(message)s",
datefmt="%H:%M:%S",
)
logger = logging.getLogger("aiye")
CONFIG_PATH = Path(__file__).parent / "config" / "channels.yaml"
DEFAULT_PORT = 18900
# ── Channel 注册表 ──────────────────────────────────────────
CHANNEL_REGISTRY: Dict[str, type] = {}
def _register_channels() -> None:
from channels.wechat_personal import WeChatPersonalChannel
from channels.wechat_work import WeChatWorkChannel
from channels.feishu import FeishuChannel
from channels.whatsapp import WhatsAppChannel
from channels.web import WebChannel
CHANNEL_REGISTRY["wechat_personal"] = WeChatPersonalChannel
CHANNEL_REGISTRY["wechat_work"] = WeChatWorkChannel
CHANNEL_REGISTRY["feishu"] = FeishuChannel
CHANNEL_REGISTRY["whatsapp"] = WhatsAppChannel
CHANNEL_REGISTRY["web"] = WebChannel
def _load_config() -> Dict[str, Any]:
env_path = os.environ.get("AIYE_CONFIG", "").strip()
p = Path(env_path) if env_path else CONFIG_PATH
if not p.exists():
logger.warning("配置文件不存在: %s,使用默认配置(仅网页通道)", p)
return {"channels": {"web": {"enabled": True}}}
return yaml.safe_load(p.read_text(encoding="utf-8")) or {}
# ── 全局状态 ────────────────────────────────────────────────
active_channels: List[ChannelBase] = []
router: MessageRouter | None = None
@asynccontextmanager
async def lifespan(app: FastAPI):
global router
_register_channels()
cfg = _load_config()
gateway_cfg = cfg.get("gateway", {})
bridge = KaruoGatewayBridge(
gateway_url=gateway_cfg.get("url", ""),
api_key=gateway_cfg.get("api_key", ""),
timeout=gateway_cfg.get("timeout", 0),
)
router = MessageRouter(bridge)
channels_cfg = cfg.get("channels", {})
for ch_key, ch_cfg in channels_cfg.items():
if not isinstance(ch_cfg, dict):
continue
if not ch_cfg.get("enabled", True):
continue
cls = CHANNEL_REGISTRY.get(ch_key)
if not cls:
logger.warning("未知通道: %s,跳过", ch_key)
continue
channel = cls(channel_id=ch_key)
channel.configure(ch_cfg)
channel.on_message(router.handle)
channel.register_routes(app)
await channel.start()
active_channels.append(channel)
logger.info("✓ 通道已启动: %s (%s)", ch_key, channel.platform)
logger.info("艾叶 IM Bridge 启动完成,%d 个通道就绪", len(active_channels))
yield
for ch in active_channels:
try:
await ch.stop()
except Exception as e:
logger.warning("通道停止异常 %s: %s", ch.channel_id, e)
active_channels.clear()
logger.info("艾叶 IM Bridge 已停止")
app = FastAPI(
title="艾叶 IM Bridge",
description="卡若AI 多平台 IM 消息网关 — 让任何聊天平台都能与卡若AI对话",
version="1.0",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ── 管理接口 ────────────────────────────────────────────────
@app.get("/", response_class=HTMLResponse)
def index():
return """<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>艾叶 IM Bridge</title>
<style>
body{font-family:-apple-system,sans-serif;max-width:720px;margin:40px auto;padding:0 20px;color:#333}
h1{background:linear-gradient(135deg,#667eea,#764ba2);-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:8px}
.badge{display:inline-block;padding:3px 10px;border-radius:12px;font-size:13px;margin:2px}
.on{background:#e6f7e6;color:#2d8c2d} .off{background:#f5f5f5;color:#999}
a{color:#667eea;text-decoration:none}
</style></head><body>
<h1>艾叶 IM Bridge</h1>
<p>卡若AI 多平台 IM 消息网关 — 让任何聊天平台都能与卡若AI对话</p>
<p><a href="/status">→ 查看通道状态</a> | <a href="/chat">→ 网页聊天</a> | <a href="/docs">→ API 文档</a></p>
</body></html>"""
@app.get("/status")
def status():
return {
"service": "aiye_im_bridge",
"version": "1.0",
"active_channels": [ch.status for ch in active_channels],
"sessions_active": router.sessions.active_count if router else 0,
}
@app.get("/health")
def health():
return {"ok": True, "channels": len(active_channels)}
if __name__ == "__main__":
import uvicorn
port = int(os.environ.get("AIYE_PORT", str(DEFAULT_PORT)))
uvicorn.run(app, host="0.0.0.0", port=port)

View File

@@ -0,0 +1,5 @@
fastapi>=0.115.0
uvicorn>=0.32.0
httpx>=0.27.0
pyyaml>=6.0
websockets>=13.0

View File

@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# ──────────────────────────────────────────────
# 艾叶 IM Bridge 启动脚本
# 用法: bash start.sh [端口]
# ──────────────────────────────────────────────
set -e
cd "$(dirname "$0")"
PORT="${1:-18900}"
if [ ! -d ".venv" ]; then
echo "→ 创建虚拟环境 .venv"
python3 -m venv .venv
fi
echo "→ 安装依赖"
.venv/bin/pip install -q -r requirements.txt
if [ ! -f "config/channels.yaml" ]; then
echo "→ 初始化配置文件 config/channels.yaml"
cp config/channels.example.yaml config/channels.yaml
fi
echo "→ 启动艾叶 IM Bridge (端口 $PORT)"
AIYE_PORT="$PORT" .venv/bin/python main.py

View File

@@ -348,3 +348,4 @@
| 2026-03-13 22:32:29 | 🔄 卡若AI 同步 2026-03-13 22:32 | 更新:运营中枢工作台 | 排除 >20MB: 11 个 |
| 2026-03-13 22:35:57 | 🔄 卡若AI 同步 2026-03-13 22:35 | 更新:运营中枢工作台 | 排除 >20MB: 11 个 |
| 2026-03-13 22:39:49 | 🔄 卡若AI 同步 2026-03-13 22:39 | 更新:运营中枢工作台 | 排除 >20MB: 11 个 |
| 2026-03-13 22:58:28 | 🔄 卡若AI 同步 2026-03-13 22:58 | 更新:运营中枢工作台 | 排除 >20MB: 11 个 |

View File

@@ -351,3 +351,4 @@
| 2026-03-13 22:32:29 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-13 22:32 | 更新:运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
| 2026-03-13 22:35:57 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-13 22:35 | 更新:运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
| 2026-03-13 22:39:49 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-13 22:39 | 更新:运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
| 2026-03-13 22:58:28 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-13 22:58 | 更新:运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |

View File

@@ -38,6 +38,8 @@
| Soul 创业实验 | `开发/3、自营项目/一场soul的创业实验-react` | **3000** | **3002** | 开发:`pnpm dev -- -p 3002`(避免与其它占 3000 的冲突);部署:项目目录 `docker compose up -d` | 部署用 3000 时注意仅此项目;开发建议 3002 |
| OpenClaw 网关 | 已并入 **website** 编排 | **18789** / **18790** | — | 神射手目录 `docker compose up -d`(镜像需在 OpenClaw 项目内先 build`openclaw:local` | 容器名 `website-openclaw-gateway`;配置见 `开发/8、小工具/Docker项目/OpenClaw/openclaw/.env` |
| n8n | 已并入 **website** 编排 | **5678** | — | 神射手目录 `docker compose up -d` | 容器名 `website-n8n`;工作流自动化,访问 http://localhost:5678 |
| 艾叶 IM Bridge | `卡若AI/运营中枢/scripts/aiye_im_bridge` | **18900** | **18900** | `bash start.sh``python main.py` | 多平台 IM 网关(微信/企业微信/飞书/WhatsApp/网页→卡若AI |
| 卡若AI 网关 | `卡若AI/运营中枢/scripts/karuo_ai_gateway` | **18080** | **18080** | `bash start_local_gateway.sh` | 卡若AI HTTP API 网关 |
---
@@ -66,3 +68,4 @@
| 2026-03-01 | 卡若ai网站数据一律用唯一 MongoDB 27017、库名 karuo_site不再单独起 27018全量库设计见项目内 开发文档/7、数据库/全量MongoDB设计_官网与控制台.md |
| 2026-03-01 | OpenClaw 网关迁入 website 编排,容器名 website-openclaw-gateway端口 18789/18790启动同神射手目录 |
| 2026-03-04 | n8n 归入 website 编排,容器名 website-n8n端口 5678神射手目录启动 |
| 2026-03-13 | 新增 艾叶 IM Bridge端口 18900多平台 IM 网关卡若AI网关端口 18080补登记 |