Phase 4: IM Bot Integration - admin module, frontend, LangBot gateway, routing rules, quick setup wizard
This commit is contained in:
1 parent
46fb67026d
commit
7daeae6fc7
7 files changed
+2310
-1
No files matched your search
@@ -0,0 +1,952 @@
|
||||
"""
|
||||
Phase 4: IM Bot Integration - Admin API Module
|
||||
im_bot_admin.py
|
||||
|
||||
Provides admin management for IM bot channels, routing rules,
|
||||
LangBot gateway config, statistics, and message monitoring.
|
||||
"""
|
||||
import uuid
|
||||
import json
|
||||
import hashlib
|
||||
import hmac
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import text
|
||||
|
||||
from ...database import SessionLocal
|
||||
from ...auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Helper Functions
|
||||
# ============================================================
|
||||
|
||||
def _execute_raw(db: Session, sql_text, params=None):
|
||||
"""Execute raw SQL and return results"""
|
||||
result = db.execute(sql_text, params or {})
|
||||
if result.returns_rows:
|
||||
return result.fetchall()
|
||||
db.commit()
|
||||
return result
|
||||
|
||||
|
||||
def _get_platform_info(platform: str) -> dict:
|
||||
"""Get platform display info and config schema"""
|
||||
platforms = {
|
||||
"feishu": {
|
||||
"name": "飞书",
|
||||
"icon": "feishu",
|
||||
"color": "#3370FF",
|
||||
"credentials": ["app_id", "app_secret", "verification_token"],
|
||||
"webhook_fields": ["encrypt_key"],
|
||||
"docs_url": "https://open.feishu.cn/document/home/index",
|
||||
},
|
||||
"wechat": {
|
||||
"name": "微信公众号",
|
||||
"icon": "wechat",
|
||||
"color": "#07C160",
|
||||
"credentials": ["app_id", "app_secret", "token"],
|
||||
"webhook_fields": ["encoding_aes_key"],
|
||||
"docs_url": "https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Overview.html",
|
||||
},
|
||||
"personal_wechat": {
|
||||
"name": "个人微信",
|
||||
"icon": "wechat-personal",
|
||||
"color": "#07C160",
|
||||
"credentials": ["qr_code", "clawbot_token"],
|
||||
"webhook_fields": [],
|
||||
"docs_url": "",
|
||||
"scan_auth": True,
|
||||
},
|
||||
"wecom": {
|
||||
"name": "企业微信",
|
||||
"icon": "wecom",
|
||||
"color": "#2F80ED",
|
||||
"credentials": ["corp_id", "agent_id", "secret", "token"],
|
||||
"webhook_fields": ["encoding_aes_key"],
|
||||
"docs_url": "https://developer.work.weixin.qq.com/document/",
|
||||
},
|
||||
"dingtalk": {
|
||||
"name": "钉钉",
|
||||
"icon": "dingtalk",
|
||||
"color": "#0089FF",
|
||||
"credentials": ["app_key", "app_secret"],
|
||||
"webhook_fields": ["aes_key"],
|
||||
"docs_url": "https://open.dingtalk.com/document/",
|
||||
},
|
||||
"telegram": {
|
||||
"name": "Telegram",
|
||||
"icon": "telegram",
|
||||
"color": "#0088CC",
|
||||
"credentials": ["bot_token"],
|
||||
"webhook_fields": [],
|
||||
"docs_url": "https://core.telegram.org/bots/api",
|
||||
},
|
||||
"whatsapp": {
|
||||
"name": "WhatsApp",
|
||||
"icon": "whatsapp",
|
||||
"color": "#25D366",
|
||||
"credentials": ["phone_number_id", "access_token"],
|
||||
"webhook_fields": ["verify_token"],
|
||||
"docs_url": "https://developers.facebook.com/docs/whatsapp",
|
||||
},
|
||||
"slack": {
|
||||
"name": "Slack",
|
||||
"icon": "slack",
|
||||
"color": "#4A154B",
|
||||
"credentials": ["bot_token", "signing_secret"],
|
||||
"webhook_fields": [],
|
||||
"docs_url": "https://api.slack.com/docs",
|
||||
},
|
||||
}
|
||||
return platforms.get(platform, {"name": platform, "icon": "default", "color": "#666"})
|
||||
|
||||
|
||||
def _row_to_dict(row) -> dict:
|
||||
"""Convert SQLAlchemy row to dict"""
|
||||
if row is None:
|
||||
return None
|
||||
d = {}
|
||||
for col in row._mapping.keys():
|
||||
val = row._mapping[col]
|
||||
if isinstance(val, datetime):
|
||||
val = val.isoformat()
|
||||
d[col] = val
|
||||
return d
|
||||
|
||||
|
||||
def _rows_to_dict(rows) -> list:
|
||||
"""Convert multiple rows to list of dicts"""
|
||||
return [_row_to_dict(r) for r in rows] if rows else []
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Dashboard & Stats
|
||||
# ============================================================
|
||||
|
||||
@router.get("/stats")
|
||||
async def get_im_bot_stats(user=Depends(get_current_user)):
|
||||
"""Get overall IM bot statistics"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# Total channels by platform
|
||||
rows = _execute_raw(db, text("""
|
||||
SELECT platform, COUNT(*) as count,
|
||||
SUM(CASE WHEN status='connected' THEN 1 ELSE 0 END) as active_count
|
||||
FROM channels
|
||||
WHERE is_active = 1
|
||||
GROUP BY platform
|
||||
"""))
|
||||
platform_stats = {}
|
||||
total_channels = 0
|
||||
total_active = 0
|
||||
for r in rows:
|
||||
d = _row_to_dict(r)
|
||||
platform_stats[d['platform']] = d
|
||||
total_channels += d['count']
|
||||
total_active += d['active_count']
|
||||
|
||||
# Today's messages
|
||||
today = datetime.now().strftime('%Y-%m-%d')
|
||||
rows = _execute_raw(db, text("""
|
||||
SELECT COALESCE(SUM(messages_in), 0) as msg_in,
|
||||
COALESCE(SUM(messages_out), 0) as msg_out,
|
||||
COALESCE(SUM(unique_users), 0) as users,
|
||||
COALESCE(SUM(error_count), 0) as errors
|
||||
FROM im_bot_daily_stats
|
||||
WHERE stat_date = :today
|
||||
"""), {"today": today})
|
||||
today_stats = _row_to_dict(rows[0]) if rows else {"msg_in": 0, "msg_out": 0, "users": 0, "errors": 0}
|
||||
|
||||
# Total routing rules
|
||||
routes_count = _execute_raw(db, text("SELECT COUNT(*) as cnt FROM im_bot_routes WHERE is_active=1"))[0]._mapping['cnt']
|
||||
|
||||
# LangBot status
|
||||
langbot_row = _execute_raw(db, text("SELECT status FROM im_bot_langbot_config ORDER BY id LIMIT 1"))
|
||||
langbot_status = _row_to_dict(langbot_row[0])['status'] if langbot_row else 'inactive'
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"total_channels": total_channels,
|
||||
"active_channels": total_active,
|
||||
"platform_breakdown": platform_stats,
|
||||
"today_messages_in": today_stats.get('msg_in', 0),
|
||||
"today_messages_out": today_stats.get('msg_out', 0),
|
||||
"today_unique_users": today_stats.get('users', 0),
|
||||
"today_errors": today_stats.get('errors', 0),
|
||||
"active_routes": routes_count,
|
||||
"langbot_status": langbot_status,
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Stats error: {str(e)}")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/dashboard")
|
||||
async def get_dashboard(user=Depends(get_current_user)):
|
||||
"""Get dashboard data with charts"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# 7-day message trend
|
||||
rows = _execute_raw(db, text("""
|
||||
SELECT stat_date,
|
||||
SUM(messages_in) as msg_in,
|
||||
SUM(messages_out) as msg_out,
|
||||
SUM(unique_users) as users
|
||||
FROM im_bot_daily_stats
|
||||
WHERE stat_date >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)
|
||||
GROUP BY stat_date
|
||||
ORDER BY stat_date
|
||||
"""))
|
||||
trend = _rows_to_dict(rows)
|
||||
|
||||
# Channel list with latest stats
|
||||
rows = _execute_raw(db, text("""
|
||||
SELECT c.id, c.name, c.platform, c.status, c.is_active,
|
||||
c.last_active, c.default_agent_id,
|
||||
COALESCE(s.msg_count, 0) as msg_count
|
||||
FROM channels c
|
||||
LEFT JOIN (
|
||||
SELECT channel_id, SUM(messages_in + messages_out) as msg_count
|
||||
FROM im_bot_daily_stats
|
||||
WHERE stat_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
|
||||
GROUP BY channel_id
|
||||
) s ON c.id = s.channel_id
|
||||
ORDER BY c.created_at DESC
|
||||
LIMIT 20
|
||||
"""))
|
||||
channels = []
|
||||
for r in rows:
|
||||
d = _row_to_dict(r)
|
||||
d['platform_info'] = _get_platform_info(d['platform'])
|
||||
channels.append(d)
|
||||
|
||||
# Platform distribution
|
||||
rows = _execute_raw(db, text("""
|
||||
SELECT platform, COUNT(*) as count
|
||||
FROM channels WHERE is_active = 1
|
||||
GROUP BY platform
|
||||
"""))
|
||||
distribution = _rows_to_dict(rows)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"message_trend": trend,
|
||||
"recent_channels": channels,
|
||||
"platform_distribution": distribution,
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Dashboard error: {str(e)}")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Channel Management
|
||||
# ============================================================
|
||||
|
||||
@router.get("/channels")
|
||||
async def list_channels(
|
||||
platform: Optional[str] = Query(None),
|
||||
status: Optional[str] = Query(None),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
user=Depends(get_current_user)
|
||||
):
|
||||
"""List all IM bot channels (admin view)"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
where_clauses = ["1=1"]
|
||||
params = {}
|
||||
if platform:
|
||||
where_clauses.append("c.platform = :platform")
|
||||
params['platform'] = platform
|
||||
if status:
|
||||
where_clauses.append("c.status = :status")
|
||||
params['status'] = status
|
||||
|
||||
where_sql = " AND ".join(where_clauses)
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
# Count
|
||||
count_sql = f"SELECT COUNT(*) as cnt FROM channels c WHERE {where_sql}"
|
||||
total = _execute_raw(db, text(count_sql), params)[0]._mapping['cnt']
|
||||
|
||||
# Query
|
||||
sql = f"""
|
||||
SELECT c.*, m.name as agent_name
|
||||
FROM channels c
|
||||
LEFT JOIN market_apps m ON c.default_agent_id = m.id
|
||||
WHERE {where_sql}
|
||||
ORDER BY c.created_at DESC
|
||||
LIMIT :limit OFFSET :offset
|
||||
"""
|
||||
params['limit'] = page_size
|
||||
params['offset'] = offset
|
||||
rows = _execute_raw(db, text(sql), params)
|
||||
|
||||
channels = []
|
||||
for r in rows:
|
||||
d = _row_to_dict(r)
|
||||
d['platform_info'] = _get_platform_info(d['platform'])
|
||||
channels.append(d)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"items": channels,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"List error: {str(e)}")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/channels/{channel_id}")
|
||||
async def get_channel_detail(channel_id: str, user=Depends(get_current_user)):
|
||||
"""Get channel detail with routing rules and recent messages"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# Channel
|
||||
rows = _execute_raw(db, text("""
|
||||
SELECT c.*, m.name as agent_name
|
||||
FROM channels c
|
||||
LEFT JOIN market_apps m ON c.default_agent_id = m.id
|
||||
WHERE c.id = :cid
|
||||
"""), {"cid": channel_id})
|
||||
if not rows:
|
||||
raise HTTPException(status_code=404, detail="Channel not found")
|
||||
channel = _row_to_dict(rows[0])
|
||||
channel['platform_info'] = _get_platform_info(channel['platform'])
|
||||
|
||||
# Routing rules
|
||||
routes = _execute_raw(db, text("""
|
||||
SELECT r.*, m.name as agent_name
|
||||
FROM im_bot_routes r
|
||||
LEFT JOIN market_apps m ON r.agent_id = m.id
|
||||
WHERE r.channel_id = :cid
|
||||
ORDER BY r.priority DESC
|
||||
"""), {"cid": channel_id})
|
||||
channel['routes'] = _rows_to_dict(routes)
|
||||
|
||||
# Recent messages (from channel_messages table)
|
||||
msgs = _execute_raw(db, text("""
|
||||
SELECT * FROM channel_messages
|
||||
WHERE channel_id = :cid
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 20
|
||||
"""), {"cid": channel_id})
|
||||
channel['recent_messages'] = _rows_to_dict(msgs)
|
||||
|
||||
# 7-day stats
|
||||
stats = _execute_raw(db, text("""
|
||||
SELECT stat_date, messages_in, messages_out, unique_users, error_count
|
||||
FROM im_bot_daily_stats
|
||||
WHERE channel_id = :cid
|
||||
AND stat_date >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)
|
||||
ORDER BY stat_date
|
||||
"""), {"cid": channel_id})
|
||||
channel['stats_7d'] = _rows_to_dict(stats)
|
||||
|
||||
return {"success": True, "data": channel}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Detail error: {str(e)}")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/channels/{channel_id}/test")
|
||||
async def test_channel(channel_id: str, user=Depends(get_current_user)):
|
||||
"""Test channel connectivity"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
rows = _execute_raw(db, text("SELECT * FROM channels WHERE id = :cid"), {"cid": channel_id})
|
||||
if not rows:
|
||||
raise HTTPException(status_code=404, detail="Channel not found")
|
||||
channel = _row_to_dict(rows[0])
|
||||
|
||||
# Simulate connectivity test based on platform
|
||||
platform = channel['platform']
|
||||
creds = channel.get('credential', {})
|
||||
if isinstance(creds, str):
|
||||
creds = json.loads(creds) if creds else {}
|
||||
|
||||
result = {
|
||||
"channel_id": channel_id,
|
||||
"platform": platform,
|
||||
"tested_at": datetime.now().isoformat(),
|
||||
"checks": [],
|
||||
"passed": True,
|
||||
}
|
||||
|
||||
if platform == 'feishu':
|
||||
result['checks'].append({"name": "App ID", "passed": bool(creds.get('app_id'))})
|
||||
result['checks'].append({"name": "App Secret", "passed": bool(creds.get('app_secret'))})
|
||||
result['checks'].append({"name": "Verification Token", "passed": bool(creds.get('verification_token'))})
|
||||
elif platform in ('wechat', 'wecom'):
|
||||
result['checks'].append({"name": "App ID", "passed": bool(creds.get('app_id') or creds.get('corp_id'))})
|
||||
result['checks'].append({"name": "Secret", "passed": bool(creds.get('app_secret') or creds.get('secret'))})
|
||||
elif platform == 'dingtalk':
|
||||
result['checks'].append({"name": "App Key", "passed": bool(creds.get('app_key'))})
|
||||
result['checks'].append({"name": "App Secret", "passed": bool(creds.get('app_secret'))})
|
||||
elif platform == 'telegram':
|
||||
result['checks'].append({"name": "Bot Token", "passed": bool(creds.get('bot_token'))})
|
||||
else:
|
||||
result['checks'].append({"name": "Credentials", "passed": len(creds) > 0})
|
||||
|
||||
result['passed'] = all(c['passed'] for c in result['checks'])
|
||||
result['webhook_configured'] = bool(channel.get('webhook_url'))
|
||||
|
||||
return {"success": True, "data": result}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Test error: {str(e)}")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/channels/{channel_id}/activate")
|
||||
async def activate_channel(channel_id: str, user=Depends(get_current_user)):
|
||||
"""Activate a channel"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
_execute_raw(db, text(
|
||||
"UPDATE channels SET is_active=1, status='connected' WHERE id=:cid"
|
||||
), {"cid": channel_id})
|
||||
return {"success": True, "message": "Channel activated"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/channels/{channel_id}/deactivate")
|
||||
async def deactivate_channel(channel_id: str, user=Depends(get_current_user)):
|
||||
"""Deactivate a channel"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
_execute_raw(db, text(
|
||||
"UPDATE channels SET is_active=0, status='disconnected' WHERE id=:cid"
|
||||
), {"cid": channel_id})
|
||||
return {"success": True, "message": "Channel deactivated"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Routing Rules
|
||||
# ============================================================
|
||||
|
||||
@router.get("/routes")
|
||||
async def list_routes(
|
||||
channel_id: Optional[str] = Query(None),
|
||||
user=Depends(get_current_user)
|
||||
):
|
||||
"""List routing rules"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
where = "1=1"
|
||||
params = {}
|
||||
if channel_id:
|
||||
where = "r.channel_id = :cid"
|
||||
params['cid'] = channel_id
|
||||
|
||||
rows = _execute_raw(db, text(f"""
|
||||
SELECT r.*, c.name as channel_name, c.platform,
|
||||
m.name as agent_name, m2.name as fallback_agent_name
|
||||
FROM im_bot_routes r
|
||||
LEFT JOIN channels c ON r.channel_id = c.id
|
||||
LEFT JOIN market_apps m ON r.agent_id = m.id
|
||||
LEFT JOIN market_apps m2 ON r.fallback_agent_id = m2.id
|
||||
WHERE {where}
|
||||
ORDER BY r.priority DESC, r.created_at DESC
|
||||
"""), params)
|
||||
return {"success": True, "data": _rows_to_dict(rows)}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/routes")
|
||||
async def create_route(
|
||||
channel_id: str = Query(...),
|
||||
agent_id: str = Query(...),
|
||||
route_name: str = Query("default"),
|
||||
match_type: str = Query("all"),
|
||||
match_pattern: Optional[str] = Query(None),
|
||||
priority: int = Query(0),
|
||||
fallback_agent_id: Optional[str] = Query(None),
|
||||
user=Depends(get_current_user)
|
||||
):
|
||||
"""Create a routing rule"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
route_id = str(uuid.uuid4())
|
||||
_execute_raw(db, text("""
|
||||
INSERT INTO im_bot_routes
|
||||
(id, channel_id, agent_id, route_name, match_type, match_pattern, priority, fallback_agent_id)
|
||||
VALUES (:id, :cid, :aid, :name, :mt, :mp, :pri, :faid)
|
||||
"""), {
|
||||
"id": route_id, "cid": channel_id, "aid": agent_id,
|
||||
"name": route_name, "mt": match_type, "mp": match_pattern,
|
||||
"pri": priority, "faid": fallback_agent_id,
|
||||
})
|
||||
return {"success": True, "data": {"id": route_id}}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.put("/routes/{route_id}")
|
||||
async def update_route(
|
||||
route_id: str,
|
||||
agent_id: Optional[str] = Query(None),
|
||||
route_name: Optional[str] = Query(None),
|
||||
match_type: Optional[str] = Query(None),
|
||||
match_pattern: Optional[str] = Query(None),
|
||||
priority: Optional[int] = Query(None),
|
||||
fallback_agent_id: Optional[str] = Query(None),
|
||||
is_active: Optional[int] = Query(None),
|
||||
user=Depends(get_current_user)
|
||||
):
|
||||
"""Update a routing rule"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
updates = []
|
||||
params = {"id": route_id}
|
||||
for field, val in [("agent_id", agent_id), ("route_name", route_name),
|
||||
("match_type", match_type), ("match_pattern", match_pattern),
|
||||
("priority", priority), ("fallback_agent_id", fallback_agent_id),
|
||||
("is_active", is_active)]:
|
||||
if val is not None:
|
||||
updates.append(f"{field} = :{field}")
|
||||
params[field] = val
|
||||
if not updates:
|
||||
raise HTTPException(status_code=400, detail="No fields to update")
|
||||
sql = f"UPDATE im_bot_routes SET {', '.join(updates)} WHERE id = :id"
|
||||
_execute_raw(db, text(sql), params)
|
||||
return {"success": True, "message": "Route updated"}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.delete("/routes/{route_id}")
|
||||
async def delete_route(route_id: str, user=Depends(get_current_user)):
|
||||
"""Delete a routing rule"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
_execute_raw(db, text("DELETE FROM im_bot_routes WHERE id = :id"), {"id": route_id})
|
||||
return {"success": True, "message": "Route deleted"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# LangBot Gateway
|
||||
# ============================================================
|
||||
|
||||
@router.get("/langbot")
|
||||
async def get_langbot_config(user=Depends(get_current_user)):
|
||||
"""Get LangBot gateway configuration"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
rows = _execute_raw(db, text("SELECT * FROM im_bot_langbot_config ORDER BY id"))
|
||||
return {"success": True, "data": _rows_to_dict(rows)}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.put("/langbot")
|
||||
async def update_langbot_config(
|
||||
config_id: int = Query(...),
|
||||
name: Optional[str] = Query(None),
|
||||
gateway_url: Optional[str] = Query(None),
|
||||
api_key: Optional[str] = Query(None),
|
||||
webhook_secret: Optional[str] = Query(None),
|
||||
bot_uuid: Optional[str] = Query(None),
|
||||
callback_url: Optional[str] = Query(None),
|
||||
sync_channels: Optional[int] = Query(None),
|
||||
status: Optional[str] = Query(None),
|
||||
user=Depends(get_current_user)
|
||||
):
|
||||
"""Update LangBot configuration"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
updates = []
|
||||
params = {"id": config_id}
|
||||
for field, val in [("name", name), ("gateway_url", gateway_url),
|
||||
("api_key", api_key), ("webhook_secret", webhook_secret),
|
||||
("bot_uuid", bot_uuid), ("callback_url", callback_url),
|
||||
("sync_channels", sync_channels), ("status", status)]:
|
||||
if val is not None:
|
||||
updates.append(f"{field} = :{field}")
|
||||
params[field] = val
|
||||
if not updates:
|
||||
raise HTTPException(status_code=400, detail="No fields to update")
|
||||
sql = f"UPDATE im_bot_langbot_config SET {', '.join(updates)} WHERE id = :id"
|
||||
_execute_raw(db, text(sql), params)
|
||||
return {"success": True, "message": "LangBot config updated"}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/langbot/test")
|
||||
async def test_langbot(
|
||||
gateway_url: str = Query(...),
|
||||
api_key: Optional[str] = Query(None),
|
||||
user=Depends(get_current_user)
|
||||
):
|
||||
"""Test LangBot gateway connectivity"""
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
result = {
|
||||
"gateway_url": gateway_url,
|
||||
"tested_at": datetime.now().isoformat(),
|
||||
"reachable": False,
|
||||
"api_version": None,
|
||||
"error": None,
|
||||
}
|
||||
|
||||
try:
|
||||
url = gateway_url.rstrip('/') + '/api/v1/pipelines'
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if api_key:
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
|
||||
req = urllib.request.Request(url, headers=headers, method='GET')
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
result['reachable'] = True
|
||||
result['status_code'] = resp.status
|
||||
body = resp.read().decode('utf-8', errors='replace')
|
||||
try:
|
||||
data = json.loads(body)
|
||||
result['api_version'] = data.get('version', 'unknown')
|
||||
except:
|
||||
result['api_version'] = 'unknown'
|
||||
except urllib.error.HTTPError as e:
|
||||
result['reachable'] = True
|
||||
result['status_code'] = e.code
|
||||
result['error'] = f"HTTP {e.code}"
|
||||
except Exception as e:
|
||||
result['error'] = str(e)
|
||||
|
||||
return {"success": True, "data": result}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Platforms & Webhooks
|
||||
# ============================================================
|
||||
|
||||
@router.get("/platforms")
|
||||
async def list_platforms(user=Depends(get_current_user)):
|
||||
"""List all supported IM platforms with config schemas"""
|
||||
platforms = []
|
||||
for key in ["feishu", "wechat", "personal_wechat", "wecom", "dingtalk",
|
||||
"telegram", "whatsapp", "slack"]:
|
||||
info = _get_platform_info(key)
|
||||
info['key'] = key
|
||||
platforms.append(info)
|
||||
|
||||
# Count channels per platform
|
||||
db = SessionLocal()
|
||||
try:
|
||||
rows = _execute_raw(db, text("""
|
||||
SELECT platform, COUNT(*) as count,
|
||||
SUM(CASE WHEN status='connected' THEN 1 ELSE 0 END) as active
|
||||
FROM channels
|
||||
GROUP BY platform
|
||||
"""))
|
||||
counts = {r._mapping['platform']: {"count": r._mapping['count'], "active": r._mapping['active']} for r in rows}
|
||||
for p in platforms:
|
||||
c = counts.get(p['key'], {"count": 0, "active": 0})
|
||||
p['channel_count'] = c['count']
|
||||
p['active_count'] = c['active']
|
||||
|
||||
return {"success": True, "data": platforms}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/webhook-url/{platform}")
|
||||
async def get_webhook_url(platform: str, user=Depends(get_current_user)):
|
||||
"""Generate webhook URL for a platform"""
|
||||
if platform not in ["feishu", "wechat", "personal_wechat", "wecom", "dingtalk",
|
||||
"telegram", "whatsapp", "slack"]:
|
||||
raise HTTPException(status_code=400, detail=f"Unsupported platform: {platform}")
|
||||
|
||||
# Generate a webhook secret for verification
|
||||
secret = hashlib.sha256(f"{platform}:{time.time()}".encode()).hexdigest()[:32]
|
||||
webhook_url = f"/app/api/channels/webhook/{platform}"
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"platform": platform,
|
||||
"platform_name": _get_platform_info(platform)['name'],
|
||||
"webhook_url": webhook_url,
|
||||
"full_url": f"http://210.16.161.218:12888{webhook_url}",
|
||||
"secret": secret,
|
||||
"instructions": _get_webhook_instructions(platform),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def _get_webhook_instructions(platform: str) -> list:
|
||||
"""Get setup instructions for each platform"""
|
||||
guides = {
|
||||
"feishu": [
|
||||
"1. 登录飞书开放平台,创建企业自建应用",
|
||||
"2. 在「事件订阅」页面配置请求地址为上述 Webhook URL",
|
||||
"3. 填入 App ID、App Secret 和 Verification Token",
|
||||
"4. 订阅消息事件: im.message.receive_v1",
|
||||
"5. 发布应用版本并等待管理员审批",
|
||||
],
|
||||
"wechat": [
|
||||
"1. 登录微信公众平台,进入「设置与开发」",
|
||||
"2. 在「基本配置」中填写服务器配置",
|
||||
"3. URL 填入上述 Webhook URL",
|
||||
"4. 填入 Token 和 EncodingAESKey",
|
||||
"5. 启用服务器配置",
|
||||
],
|
||||
"personal_wechat": [
|
||||
"1. 点击「扫码授权」按钮,使用手机微信扫码",
|
||||
"2. 等待授权完成后,频道状态将变为「已连接」",
|
||||
"3. 个人微信通过 ClawBot 协议接入,无需 Webhook 配置",
|
||||
"4. 注意:个人微信接入存在封号风险,请谨慎使用",
|
||||
],
|
||||
"wecom": [
|
||||
"1. 登录企业微信管理后台,创建自建应用",
|
||||
"2. 在「接收消息」页面设置 API 接收",
|
||||
"3. URL 填入上述 Webhook URL",
|
||||
"4. 填入 Corp ID、Agent ID、Secret、Token 和 EncodingAESKey",
|
||||
"5. 设置可信 IP 白名单",
|
||||
],
|
||||
"dingtalk": [
|
||||
"1. 登录钉钉开放平台,创建企业内部应用",
|
||||
"2. 在「事件订阅」中配置 HTTP 模式",
|
||||
"3. 请求地址填入上述 Webhook URL",
|
||||
"4. 填入 App Key 和 App Secret",
|
||||
"5. 订阅消息接收事件",
|
||||
],
|
||||
"telegram": [
|
||||
"1. 通过 @BotFather 创建 Bot,获取 Bot Token",
|
||||
"2. 调用 Telegram API 设置 Webhook: setWebhook?url=<上述URL>",
|
||||
"3. 填入 Bot Token",
|
||||
],
|
||||
"whatsapp": [
|
||||
"1. 在 Meta for Developers 创建 WhatsApp Business 应用",
|
||||
"2. 配置 Webhook 验证 Token",
|
||||
"3. Webhook URL 填入上述地址",
|
||||
"4. 订阅 messages 事件",
|
||||
"5. 填入 Phone Number ID 和 Access Token",
|
||||
],
|
||||
"slack": [
|
||||
"1. 在 api.slack.com 创建 Slack App",
|
||||
"2. 启用 Event Subscriptions",
|
||||
"3. Request URL 填入上述 Webhook URL",
|
||||
"4. 订阅 message.im 事件",
|
||||
"5. 填入 Bot Token 和 Signing Secret",
|
||||
],
|
||||
}
|
||||
return guides.get(platform, ["请参考平台官方文档进行配置"])
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Messages & Logs
|
||||
# ============================================================
|
||||
|
||||
@router.get("/messages")
|
||||
async def list_messages(
|
||||
channel_id: Optional[str] = Query(None),
|
||||
direction: Optional[str] = Query(None),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(50, ge=1, le=200),
|
||||
user=Depends(get_current_user)
|
||||
):
|
||||
"""List messages across all channels"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
where = ["1=1"]
|
||||
params = {}
|
||||
if channel_id:
|
||||
where.append("channel_id = :cid")
|
||||
params['cid'] = channel_id
|
||||
if direction:
|
||||
where.append("direction = :dir")
|
||||
params['dir'] = direction
|
||||
|
||||
where_sql = " AND ".join(where)
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
count = _execute_raw(db, text(f"SELECT COUNT(*) as cnt FROM channel_messages WHERE {where_sql}"), params)[0]._mapping['cnt']
|
||||
|
||||
rows = _execute_raw(db, text(f"""
|
||||
SELECT cm.*, c.name as channel_name, c.platform
|
||||
FROM channel_messages cm
|
||||
LEFT JOIN channels c ON cm.channel_id = c.id
|
||||
WHERE {where_sql}
|
||||
ORDER BY cm.created_at DESC
|
||||
LIMIT :limit OFFSET :offset
|
||||
"""), {**params, "limit": page_size, "offset": offset})
|
||||
|
||||
messages = []
|
||||
for r in rows:
|
||||
d = _row_to_dict(r)
|
||||
d['platform_info'] = _get_platform_info(d.get('platform', '')) if d.get('platform') else None
|
||||
messages.append(d)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"items": messages,
|
||||
"total": count,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/logs")
|
||||
async def get_activity_logs(
|
||||
channel_id: Optional[str] = Query(None),
|
||||
limit: int = Query(100, ge=1, le=500),
|
||||
user=Depends(get_current_user)
|
||||
):
|
||||
"""Get recent activity logs"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
where = ""
|
||||
params = {"limit": limit}
|
||||
if channel_id:
|
||||
where = "WHERE channel_id = :cid"
|
||||
params['cid'] = channel_id
|
||||
|
||||
rows = _execute_raw(db, text(f"""
|
||||
SELECT stat_date, channel_id,
|
||||
messages_in, messages_out, unique_users,
|
||||
avg_response_time_ms, error_count, tokens_used
|
||||
FROM im_bot_daily_stats
|
||||
{where}
|
||||
ORDER BY stat_date DESC, channel_id
|
||||
LIMIT :limit
|
||||
"""), params)
|
||||
|
||||
return {"success": True, "data": _rows_to_dict(rows)}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Quick Setup Wizard
|
||||
# ============================================================
|
||||
|
||||
@router.post("/quick-setup")
|
||||
async def quick_setup(
|
||||
platform: str = Query(...),
|
||||
name: str = Query(...),
|
||||
agent_id: Optional[str] = Query(None),
|
||||
credentials: str = Query(None),
|
||||
user=Depends(get_current_user)
|
||||
):
|
||||
"""Quick setup wizard - create and configure a channel in one step"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
if platform not in ["feishu", "wechat", "personal_wechat", "wecom", "dingtalk",
|
||||
"telegram", "whatsapp", "slack"]:
|
||||
raise HTTPException(status_code=400, detail=f"Unsupported platform: {platform}")
|
||||
|
||||
channel_id = str(uuid.uuid4())
|
||||
webhook_url = f"/app/api/channels/webhook/{platform}"
|
||||
|
||||
cred_json = None
|
||||
if credentials:
|
||||
try:
|
||||
cred_json = json.loads(credentials)
|
||||
except:
|
||||
cred_json = {"raw": credentials}
|
||||
|
||||
_execute_raw(db, text("""
|
||||
INSERT INTO channels (id, owner_id, name, platform, webhook_url,
|
||||
credential, default_agent_id, status, is_active)
|
||||
VALUES (:id, :owner, :name, :platform, :webhook,
|
||||
:cred, :agent, 'pending', 1)
|
||||
"""), {
|
||||
"id": channel_id,
|
||||
"owner": "admin-001",
|
||||
"name": name,
|
||||
"platform": platform,
|
||||
"webhook": webhook_url,
|
||||
"cred": json.dumps(cred_json) if cred_json else None,
|
||||
"agent": agent_id,
|
||||
})
|
||||
|
||||
# Create default routing rule if agent specified
|
||||
if agent_id:
|
||||
route_id = str(uuid.uuid4())
|
||||
_execute_raw(db, text("""
|
||||
INSERT INTO im_bot_routes (id, channel_id, agent_id, route_name, match_type, priority)
|
||||
VALUES (:rid, :cid, :aid, 'default', 'all', 0)
|
||||
"""), {"rid": route_id, "cid": channel_id, "aid": agent_id})
|
||||
|
||||
platform_info = _get_platform_info(platform)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"channel_id": channel_id,
|
||||
"webhook_url": f"http://210.16.161.218:12888{webhook_url}",
|
||||
"platform": platform,
|
||||
"platform_name": platform_info['name'],
|
||||
"instructions": _get_webhook_instructions(platform),
|
||||
"next_steps": "请按照上述说明在平台配置 Webhook,然后返回此页面点击「测试连接」",
|
||||
}
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Quick setup error: {str(e)}")
|
||||
finally:
|
||||
db.close()
|
||||
@@ -34,7 +34,7 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from app.api import auth, chat, channels, knowledge, market, token_usage, payment, profile, notifications, assets, evolution
|
||||
|
||||
from app.api.admin import dashboard, users, models, settings, payment_admin, services, membership_admin, ads, market_admin, logs, skills_admin, agents_admin, knowledge_admin, workflow_admin, storage_admin, automation_admin, mcp_admin, developer_admin, apps_admin, distro_admin, code_exec_admin, privacy_admin
|
||||
from app.api.admin import dashboard, users, models, settings, payment_admin, services, membership_admin, ads, market_admin, logs, skills_admin, agents_admin, knowledge_admin, workflow_admin, storage_admin, automation_admin, mcp_admin, developer_admin, apps_admin, distro_admin, code_exec_admin, privacy_admin, im_bot_admin
|
||||
from app.api.admin.developer_tools_admin import router as developer_tools_router
|
||||
from app.api.admin.mcp_market_admin import router as mcp_market_router
|
||||
from app.api.admin import asset_admin
|
||||
@@ -434,6 +434,7 @@ app.include_router(mcp_market_router, prefix="/app/api/admin/mcp-market", tags=[
|
||||
app.include_router(code_exec_admin.router, prefix="/app/api/admin/code-exec", tags=["code-exec"])
|
||||
app.include_router(distro_admin.router, prefix="/app/api/admin/distributions", tags=["distributions"])
|
||||
app.include_router(privacy_admin.router, prefix="/app/api/admin/privacy", tags=["privacy"])
|
||||
app.include_router(im_bot_admin.router, prefix="/app/api/admin/im-bot", tags=["im-bot"])
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user