ForcePilot/backend/server/routers/channels_router.py
Kris 915ae2f5cc refactor(channel): 重构渠道管理器使用方式,新增微信webhook和渠道注册注销接口
1. 将全局channel_manager改为通过get_channel_manager()获取单例
2. 新增微信webhook处理接口
3. 在dashboard反馈接口中新增channel字段返回
4. 新增渠道注册、注销API接口
5. 优化部分代码格式和异常处理逻辑
6. 注册Nostr profile api路由
2026-05-13 16:51:23 +08:00

832 lines
28 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from yuxi.channels.manager import get_channel_manager
from yuxi.storage.postgres.models_business import User
from yuxi.storage.postgres.models_channels import ChannelThreadMapping, ChannelUserMapping
from yuxi.utils.logging_config import logger
from server.utils.auth_middleware import get_admin_user, get_db
channels = APIRouter(prefix="/channels", tags=["channels"])
def _make_response(data: Any = None, code: int = 0, message: str = "ok") -> dict:
return {"code": code, "data": data, "message": message}
async def _check_rate_limit(user_id: str, key: str, max_req: int, window: int) -> None:
rate_key = f"{key}:{user_id}"
allowed = await get_channel_manager().check_rate_limit(rate_key, max_req, window)
if not allowed:
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail="请求频率超限,请稍后再试",
)
@channels.get("/status")
async def list_channels_status(current_user: User = Depends(get_admin_user)):
status_data = await get_channel_manager().get_channel_status()
return _make_response(data=status_data)
@channels.get("/actions")
async def list_channels_for_action(
action: str | None = Query(None, description="要查询的 MessageAction如 pin/edit/send"),
current_user: User = Depends(get_admin_user),
):
from yuxi.channels.message_actions import ActionRegistry, MessageAction
if action:
try:
msg_action = MessageAction(action)
except ValueError:
return _make_response(
code=-1,
message=f"Unknown action: {action}. Valid actions: {[a.value for a in MessageAction]}",
)
channels = ActionRegistry.list_channels_for_action(msg_action)
return _make_response(data={"action": action, "channels": channels})
all_channels = ActionRegistry.list_all()
result = {}
for cid, caps in all_channels.items():
result[cid] = {
"supported_actions": caps.supported_actions(),
"chat_types": caps.chat_types,
}
return _make_response(data=result)
@channels.post("/register")
async def register_channel(
body: dict[str, Any],
current_user: User = Depends(get_admin_user),
):
channel_type = body.get("channel_type")
if not channel_type:
raise HTTPException(status_code=400, detail="channel_type is required")
config = body.get("config", {})
manager = get_channel_manager()
try:
await manager.register_channel(channel_type, config)
return _make_response(
data={"channel_id": channel_type, "registered": True},
message="渠道注册成功",
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@channels.get("/{channel_id}/actions")
async def get_channel_actions(
channel_id: str,
current_user: User = Depends(get_admin_user),
):
from yuxi.channels.message_actions import ActionRegistry
declarations = ActionRegistry.list_all_declarations(channel_id)
if declarations:
actions_data = [
{
"action": decl.action.value,
"status": decl.status.value,
"reason": decl.reason,
"impl": decl.impl,
}
for decl in declarations.values()
]
return _make_response(data={"channel_id": channel_id, "actions": actions_data})
caps = ActionRegistry.get(channel_id)
if caps is None:
raise HTTPException(status_code=404, detail=f"Channel '{channel_id}' is not registered")
supported = caps.supported_actions()
return _make_response(
data={
"channel_id": channel_id,
"actions": [{"action": a, "status": "supported", "reason": "", "impl": ""} for a in supported],
}
)
@channels.get("/{channel_id}/status")
async def get_channel_status(
channel_id: str,
current_user: User = Depends(get_admin_user),
):
status_data = await get_channel_manager().get_channel_status(channel_id)
if status_data.get("status") == "not_found":
raise HTTPException(status_code=404, detail=f"Channel '{channel_id}' is not registered")
return _make_response(data=status_data)
@channels.post("/{channel_id}/start")
async def start_channel(
channel_id: str,
current_user: User = Depends(get_admin_user),
):
await _check_rate_limit(str(current_user.id), "channel_start", 3, 3)
manager = get_channel_manager()
if not manager.is_registered(channel_id):
raise HTTPException(status_code=404, detail=f"Channel '{channel_id}' is not registered")
if manager.is_running(channel_id):
return _make_response(code=409, message=f"Channel '{channel_id}' is already running")
try:
await manager.start_channel(channel_id)
return _make_response(
data={"channel_id": channel_id, "status": "connecting"},
message="Channel start initiated",
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Failed to start channel {channel_id}: {e}")
raise HTTPException(status_code=500, detail=f"Failed to start channel: {e}")
@channels.post("/{channel_id}/stop")
async def stop_channel(
channel_id: str,
current_user: User = Depends(get_admin_user),
):
await _check_rate_limit(str(current_user.id), "channel_stop", 3, 3)
manager = get_channel_manager()
if not manager.is_running(channel_id):
return _make_response(code=409, message=f"Channel '{channel_id}' is not running")
await manager.stop_channel(channel_id)
return _make_response(
data={"channel_id": channel_id, "status": "disconnected"},
message="Channel stopped",
)
@channels.post("/{channel_id}/restart")
async def restart_channel(
channel_id: str,
current_user: User = Depends(get_admin_user),
):
await _check_rate_limit(str(current_user.id), "channel_restart", 3, 3)
manager = get_channel_manager()
if not manager.is_registered(channel_id):
raise HTTPException(status_code=404, detail=f"Channel '{channel_id}' is not registered")
try:
await manager.restart_channel(channel_id)
return _make_response(
data={"channel_id": channel_id, "status": "connecting"},
message="Channel restart initiated",
)
except Exception as e:
logger.error(f"Failed to restart channel {channel_id}: {e}")
raise HTTPException(status_code=500, detail=f"Failed to restart channel: {e}")
@channels.put("/{channel_id}/config")
async def update_channel_config(
channel_id: str,
config_updates: dict[str, Any],
current_user: User = Depends(get_admin_user),
):
manager = get_channel_manager()
if not manager.is_registered(channel_id):
raise HTTPException(status_code=404, detail=f"Channel '{channel_id}' is not registered")
try:
result = await manager.update_channel_config(channel_id, config_updates)
return _make_response(
data=result,
message="Configuration updated. Restart channel to apply changes.",
)
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@channels.post("/{channel_id}/test")
async def test_channel(
channel_id: str,
current_user: User = Depends(get_admin_user),
):
manager = get_channel_manager()
if not manager.is_registered(channel_id):
raise HTTPException(status_code=404, detail=f"Channel '{channel_id}' is not registered")
result = await manager.test_channel(channel_id)
return _make_response(data=result)
@channels.get("/{channel_id}/stats")
async def get_channel_stats(
channel_id: str,
days: int = Query(7, ge=1, le=90),
period: str | None = Query(None, description="时间范围: 1d / 7d / 30d"),
granularity: str = Query("day"),
current_user: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
from datetime import timedelta
from sqlalchemy import func, select
from yuxi.storage.postgres.models_channels import ChannelMsgRecord
from yuxi.utils.datetime_utils import utc_now_naive
if period:
period_map = {"1d": 1, "7d": 7, "30d": 30}
days = period_map.get(period, 7)
manager = get_channel_manager()
if not manager.is_registered(channel_id):
raise HTTPException(status_code=404, detail=f"Channel '{channel_id}' is not registered")
now = utc_now_naive()
since = now - timedelta(days=days)
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
stats_base = select(
func.count().label("total"),
func.sum(
func.cast(
(ChannelMsgRecord.status == "success").cast(func.Integer),
func.Integer,
)
).label("success_count"),
func.sum(
func.cast(
(ChannelMsgRecord.status == "error").cast(func.Integer),
func.Integer,
)
).label("error_count"),
func.avg(ChannelMsgRecord.response_time_ms).label("avg_response_ms"),
func.percentile_cont(0.5).within_group(ChannelMsgRecord.response_time_ms).label("p50_ms"),
func.percentile_cont(0.95).within_group(ChannelMsgRecord.response_time_ms).label("p95_ms"),
func.percentile_cont(0.99).within_group(ChannelMsgRecord.response_time_ms).label("p99_ms"),
).where(
ChannelMsgRecord.channel_id == channel_id,
ChannelMsgRecord.created_at >= since,
)
result = await db.execute(stats_base)
row = result.one()
total = row.total or 0
success = row.success_count or 0
error = row.error_count or 0
today_result = await db.execute(
select(func.count()).where(
ChannelMsgRecord.channel_id == channel_id,
ChannelMsgRecord.created_at >= today_start,
)
)
today_count = today_result.scalar() or 0
active_users_result = await db.execute(
select(func.count(func.distinct(ChannelMsgRecord.sender_user_id))).where(
ChannelMsgRecord.channel_id == channel_id,
ChannelMsgRecord.created_at >= since,
)
)
active_users = active_users_result.scalar() or 0
summary = {
"today_count": today_count,
"total_count": total,
"active_users": active_users,
"success_count": int(success),
"error_count": int(error),
"success_rate": round(success / total, 3) if total > 0 else 0,
"avg_latency": round(row.avg_response_ms, 1) if row.avg_response_ms else 0,
"p50_latency": round(row.p50_ms, 1) if row.p50_ms else 0,
"p95_latency": round(row.p95_ms, 1) if row.p95_ms else 0,
"p99_latency": round(row.p99_ms, 1) if row.p99_ms else 0,
}
daily_trend: list[dict] = []
daily_success_rate: list[dict] = []
if granularity == "day":
daily_query = (
select(
func.date(ChannelMsgRecord.created_at).label("date"),
func.count().label("count"),
func.avg(ChannelMsgRecord.response_time_ms).label("avg_ms"),
)
.where(
ChannelMsgRecord.channel_id == channel_id,
ChannelMsgRecord.created_at >= since,
)
.group_by(func.date(ChannelMsgRecord.created_at))
.order_by(func.date(ChannelMsgRecord.created_at))
)
daily_result = await db.execute(daily_query)
daily_trend = [
{"date": str(r.date), "count": r.count, "avg_ms": round(r.avg_ms, 1) if r.avg_ms else 0}
for r in daily_result.all()
]
success_rate_query = (
select(
func.date(ChannelMsgRecord.created_at).label("date"),
func.count().label("total"),
func.sum(
func.cast(
(ChannelMsgRecord.status == "success").cast(func.Integer),
func.Integer,
)
).label("success"),
)
.where(
ChannelMsgRecord.channel_id == channel_id,
ChannelMsgRecord.created_at >= since,
)
.group_by(func.date(ChannelMsgRecord.created_at))
.order_by(func.date(ChannelMsgRecord.created_at))
)
success_rate_result = await db.execute(success_rate_query)
daily_success_rate = [
{
"date": str(r.date),
"success_rate": round(r.success / r.total * 100, 1) if r.total > 0 else 0,
"total": r.total,
}
for r in success_rate_result.all()
]
type_dist_result = await db.execute(
select(ChannelMsgRecord.chat_type, func.count().label("cnt"))
.where(ChannelMsgRecord.channel_id == channel_id, ChannelMsgRecord.created_at >= since)
.group_by(ChannelMsgRecord.chat_type)
)
chat_type_distribution = {r.chat_type: r.cnt for r in type_dist_result.all()}
msg_type_result = await db.execute(
select(ChannelMsgRecord.content_type, func.count().label("cnt"))
.where(ChannelMsgRecord.channel_id == channel_id, ChannelMsgRecord.created_at >= since)
.group_by(ChannelMsgRecord.content_type)
)
message_type_distribution = {r.content_type: r.cnt for r in msg_type_result.all()}
return _make_response(
data={
"channel_id": channel_id,
"period": {
"from": since.strftime("%Y-%m-%dT%H:%M:%SZ"),
"to": now.strftime("%Y-%m-%dT%H:%M:%SZ"),
},
"summary": summary,
"daily_trend": daily_trend,
"daily_success_rate": daily_success_rate,
"chat_type_distribution": chat_type_distribution,
"message_type_distribution": message_type_distribution,
}
)
@channels.get("/{channel_id}/users/{user_id}")
async def get_channel_user_mapping(
channel_id: str,
user_id: str,
current_user: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(ChannelUserMapping).where(
ChannelUserMapping.channel_id == channel_id,
ChannelUserMapping.channel_user_id == user_id,
)
)
mapping = result.scalar_one_or_none()
if not mapping:
raise HTTPException(status_code=404, detail="User mapping not found")
thread_result = await db.execute(
select(ChannelThreadMapping)
.where(
ChannelThreadMapping.channel_id == channel_id,
ChannelThreadMapping.internal_user_id == mapping.internal_user_id,
)
.order_by(ChannelThreadMapping.last_active_at.desc())
.limit(10)
)
recent_chats = [
{
"chat_id": t.channel_chat_id,
"thread_id": t.thread_id,
"last_active": t.last_active_at.strftime("%Y-%m-%dT%H:%M:%SZ") if t.last_active_at else None,
}
for t in thread_result.all()
]
return _make_response(
data={
"channel_id": channel_id,
"channel_user_id": user_id,
"internal_user_id": mapping.internal_user_id,
"created_at": mapping.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if mapping.created_at else None,
"recent_chats": recent_chats,
}
)
@channels.get("/{channel_id}/messages")
async def get_channel_messages(
channel_id: str,
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
status: str | None = Query(None),
content_type: str | None = Query(None),
date_from: str | None = Query(None),
date_to: str | None = Query(None),
search: str | None = Query(None),
current_user: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
from datetime import datetime
from sqlalchemy import func, select, desc
from yuxi.storage.postgres.models_channels import ChannelMsgRecord
conditions = [ChannelMsgRecord.channel_id == channel_id]
if status:
conditions.append(ChannelMsgRecord.status == status)
if content_type:
conditions.append(ChannelMsgRecord.content_type == content_type)
if date_from:
try:
dt_from = datetime.strptime(date_from, "%Y-%m-%d")
conditions.append(ChannelMsgRecord.created_at >= dt_from)
except ValueError:
pass
if date_to:
try:
dt_to = datetime.strptime(date_to, "%Y-%m-%d")
conditions.append(ChannelMsgRecord.created_at < dt_to)
except ValueError:
pass
if search:
conditions.append(ChannelMsgRecord.content_preview.ilike(f"%{search}%"))
count_query = select(func.count()).where(*conditions)
total_result = await db.execute(count_query)
total = total_result.scalar() or 0
query = (
select(ChannelMsgRecord)
.where(*conditions)
.order_by(desc(ChannelMsgRecord.created_at))
.offset((page - 1) * page_size)
.limit(page_size)
)
result = await db.execute(query)
rows = result.scalars().all()
messages = [
{
"id": r.id,
"created_at": r.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if r.created_at else None,
"sender_user_id": r.sender_user_id,
"content_preview": r.content_preview,
"content_type": r.content_type,
"status": r.status,
"response_time_ms": r.response_time_ms,
"reply_content_preview": r.reply_content_preview,
"error_message": r.error_message,
"chat_id": r.chat_id,
"chat_type": r.chat_type,
}
for r in rows
]
return _make_response(data={"messages": messages, "total": total})
@channels.get("/{channel_id}/messages/export")
async def export_channel_messages(
channel_id: str,
status: str | None = Query(None),
content_type: str | None = Query(None),
date_from: str | None = Query(None),
date_to: str | None = Query(None),
search: str | None = Query(None),
current_user: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
import csv
from datetime import datetime
from io import StringIO
from fastapi.responses import StreamingResponse
from yuxi.storage.postgres.models_channels import ChannelMsgRecord
conditions = [ChannelMsgRecord.channel_id == channel_id]
if status:
conditions.append(ChannelMsgRecord.status == status)
if content_type:
conditions.append(ChannelMsgRecord.content_type == content_type)
if date_from:
try:
dt_from = datetime.strptime(date_from, "%Y-%m-%d")
conditions.append(ChannelMsgRecord.created_at >= dt_from)
except ValueError:
pass
if date_to:
try:
dt_to = datetime.strptime(date_to, "%Y-%m-%d")
conditions.append(ChannelMsgRecord.created_at < dt_to)
except ValueError:
pass
if search:
conditions.append(ChannelMsgRecord.content_preview.ilike(f"%{search}%"))
query = select(ChannelMsgRecord).where(*conditions).order_by(ChannelMsgRecord.created_at.desc()).limit(10000)
result = await db.execute(query)
rows = result.scalars().all()
output = StringIO()
writer = csv.writer(output)
writer.writerow(
[
"id",
"created_at",
"sender_user_id",
"content_preview",
"content_type",
"status",
"response_time_ms",
"reply_content_preview",
"error_message",
"chat_id",
"chat_type",
]
)
for r in rows:
writer.writerow(
[
r.id,
r.created_at.strftime("%Y-%m-%d %H:%M:%S") if r.created_at else "",
r.sender_user_id,
r.content_preview,
r.content_type,
r.status,
r.response_time_ms or "",
r.reply_content_preview or "",
r.error_message or "",
r.chat_id,
r.chat_type,
]
)
output.seek(0)
return StreamingResponse(
iter([output.getvalue()]),
media_type="text/csv; charset=utf-8-sig",
headers={"Content-Disposition": f"attachment; filename=messages_{channel_id}.csv"},
)
@channels.get("/{channel_id}/policy")
async def get_channel_policy(
channel_id: str,
current_user: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
from yuxi.storage.postgres.models_channels import ChannelPolicyConfig
result = await db.execute(select(ChannelPolicyConfig).where(ChannelPolicyConfig.channel_id == channel_id))
policy = result.scalar_one_or_none()
if not policy:
return _make_response(
data={
"channel_id": channel_id,
"group_chat_mode": "all",
"whitelist_ids": [],
"welcome_message": "",
"work_hours_start": "09:00",
"work_hours_end": "18:00",
"timezone_offset": 8,
"off_hours_reply": "",
}
)
return _make_response(data=policy.to_dict())
@channels.put("/{channel_id}/policy")
async def update_channel_policy(
channel_id: str,
policy_updates: dict[str, Any],
current_user: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
from yuxi.storage.postgres.models_channels import ChannelPolicyConfig
from yuxi.utils.datetime_utils import utc_now_naive
result = await db.execute(select(ChannelPolicyConfig).where(ChannelPolicyConfig.channel_id == channel_id))
policy = result.scalar_one_or_none()
if policy:
for key in [
"group_chat_mode",
"whitelist_ids",
"welcome_message",
"work_hours_start",
"work_hours_end",
"timezone_offset",
"off_hours_reply",
]:
if key in policy_updates:
setattr(policy, key, policy_updates[key])
policy.updated_at = utc_now_naive()
else:
policy = ChannelPolicyConfig(
channel_id=channel_id,
**{
k: v
for k, v in policy_updates.items()
if k
in [
"group_chat_mode",
"whitelist_ids",
"welcome_message",
"work_hours_start",
"work_hours_end",
"timezone_offset",
"off_hours_reply",
]
},
)
db.add(policy)
await db.commit()
await db.refresh(policy)
return _make_response(data=policy.to_dict(), message="策略配置已保存")
@channels.get("/{channel_id}/routing")
async def get_channel_routing(
channel_id: str,
current_user: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
from yuxi.storage.postgres.models_channels import ChannelRoutingRule
result = await db.execute(
select(ChannelRoutingRule)
.where(ChannelRoutingRule.channel_id == channel_id)
.order_by(ChannelRoutingRule.priority.desc())
)
rules = result.scalars().all()
return _make_response(data={"channel_id": channel_id, "routes": [r.to_dict() for r in rules]})
@channels.put("/{channel_id}/routing")
async def update_channel_routing(
channel_id: str,
routing_updates: list[dict[str, Any]],
current_user: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
from yuxi.storage.postgres.models_channels import ChannelRoutingRule
existing = (
(await db.execute(select(ChannelRoutingRule).where(ChannelRoutingRule.channel_id == channel_id)))
.scalars()
.all()
)
for rule in existing:
await db.delete(rule)
for i, item in enumerate(routing_updates):
rule = ChannelRoutingRule(
channel_id=channel_id,
command=item.get("command", ""),
agent_config_id=str(item.get("agent_config_id", "0")),
priority=item.get("priority", len(routing_updates) - i),
)
db.add(rule)
await db.commit()
return _make_response(data={"channel_id": channel_id, "routes": routing_updates}, message="路由规则已保存")
@channels.get("/{channel_id}/config-schema")
async def get_channel_config_schema(
channel_id: str,
current_user: User = Depends(get_admin_user),
):
from yuxi.channels.registry import BUILTIN_ADAPTERS
all_adapters = {**BUILTIN_ADAPTERS}
for cid, adapter_cls in all_adapters.items():
if cid == channel_id:
try:
default_config = adapter_cls.default_config() if hasattr(adapter_cls, "default_config") else {}
except Exception:
default_config = {}
fields = []
for key, value in default_config.items():
field_type = "text"
if "token" in key or "secret" in key or "key" in key:
field_type = "password"
elif isinstance(value, bool):
field_type = "switch"
elif isinstance(value, (int, float)):
field_type = "number"
is_credential = any(k in key for k in ("token", "secret", "key", "app_id"))
fields.append(
{
"key": key,
"label": key.replace("_", " ").title(),
"type": field_type,
"default": value,
"required": "token" in key or "secret" in key,
"group": "credentials" if is_credential else "general",
}
)
fields.append(
{
"key": "agent_config_id",
"label": "默认 Agent",
"type": "agent_select",
"required": False,
"group": "routing",
}
)
return _make_response(
data={
"channel_type": channel_id,
"fields": fields,
"groups": [
{"key": "credentials", "label": "认证凭据"},
{"key": "general", "label": "通用设置"},
{"key": "routing", "label": "路由配置"},
],
}
)
return _make_response(data={"channel_type": channel_id, "fields": [], "groups": []})
@channels.get("/{channel_id}/chats/{chat_id}")
async def get_channel_chat_mapping(
channel_id: str,
chat_id: str,
current_user: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(ChannelThreadMapping)
.where(
ChannelThreadMapping.channel_id == channel_id,
ChannelThreadMapping.channel_chat_id == chat_id,
)
.order_by(ChannelThreadMapping.last_active_at.desc())
.limit(1)
)
mapping = result.scalar_one_or_none()
if not mapping:
raise HTTPException(status_code=404, detail="Chat mapping not found")
return _make_response(
data={
"channel_id": channel_id,
"channel_chat_id": chat_id,
"thread_id": mapping.thread_id,
"agent_id": mapping.agent_id,
"created_at": mapping.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if mapping.created_at else None,
"last_active_at": mapping.last_active_at.strftime("%Y-%m-%dT%H:%M:%SZ") if mapping.last_active_at else None,
}
)
@channels.delete("/{channel_id}")
async def unregister_channel(
channel_id: str,
current_user: User = Depends(get_admin_user),
):
manager = get_channel_manager()
if not manager.is_registered(channel_id):
raise HTTPException(status_code=404, detail=f"Channel '{channel_id}' not found")
await manager.unregister_channel(channel_id)
return _make_response(data={"channel_id": channel_id, "unregistered": True}, message="渠道已注销")