From ab54d7344c029328a968c510a0066dc75f283fcc Mon Sep 17 00:00:00 2001 From: QihuAI Design Date: Tue, 14 Jul 2026 08:21:56 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=E9=80=9A?= =?UTF-8?q?=E7=94=A8=E9=A2=91=E9=81=93=E7=AE=A1=E7=90=86(LangBot)=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=EF=BC=8CIM=20Bot=20=E6=94=B9=E4=B8=BA=E7=9B=B4?= =?UTF-8?q?=E6=8E=A5=E5=AF=B9=E6=8E=A5=EF=BC=9B=E8=BF=81=E7=A7=BB=20=5Fcal?= =?UTF-8?q?l=5Freal=5Fllm=20=E8=87=B3=20im=5Fllm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/python/app/api/__init__.py | 1 - backend/python/app/api/admin/im_bot_admin.py | 2 +- backend/python/app/api/channels.py | 1567 ------- backend/python/app/api/im_bot_user.py | 1202 ++--- backend/python/app/api/im_llm.py | 45 + backend/python/main.py | 489 +- frontend/src/App.tsx | 344 +- frontend/src/api/index.ts | 3975 ++++++++--------- .../src/components/layout/FrontLayout.tsx | 365 +- frontend/src/mock/data.ts | 10 +- frontend/src/pages/admin/ImBots.tsx | 180 +- frontend/src/pages/front/ChannelPage.css | 1780 -------- frontend/src/pages/front/ChannelPage.tsx | 2012 --------- frontend/src/pages/front/LandingPage.tsx | 2 +- frontend/src/types/index.ts | 18 - 15 files changed, 3208 insertions(+), 8784 deletions(-) delete mode 100644 backend/python/app/api/channels.py create mode 100644 backend/python/app/api/im_llm.py delete mode 100644 frontend/src/pages/front/ChannelPage.css delete mode 100644 frontend/src/pages/front/ChannelPage.tsx diff --git a/backend/python/app/api/__init__.py b/backend/python/app/api/__init__.py index 0302f31..f77f19a 100644 --- a/backend/python/app/api/__init__.py +++ b/backend/python/app/api/__init__.py @@ -1,6 +1,5 @@ from . import auth from . import chat -from . import channels from . import knowledge from . import market from . import token_usage diff --git a/backend/python/app/api/admin/im_bot_admin.py b/backend/python/app/api/admin/im_bot_admin.py index 4e0bd21..eff21bf 100644 --- a/backend/python/app/api/admin/im_bot_admin.py +++ b/backend/python/app/api/admin/im_bot_admin.py @@ -608,7 +608,7 @@ def _qihua_reply(text: str) -> str: 自行创建 DB 会话,避免上层线程无可用 session 时崩溃。""" db = None try: - from app.api.channels import _call_real_llm + from app.api.im_llm import _call_real_llm db = SessionLocal() return _call_real_llm(db, None, text, "个人微信") except Exception as e: diff --git a/backend/python/app/api/channels.py b/backend/python/app/api/channels.py deleted file mode 100644 index f4c7aa6..0000000 --- a/backend/python/app/api/channels.py +++ /dev/null @@ -1,1567 +0,0 @@ -""" -频道/IM绑定 API -参考 QwenPaw / 悟空IM 标准设计,支持飞书、微信、钉钉、WhatsApp、Telegram、Slack -""" -from fastapi import APIRouter, Body, Depends, HTTPException, Query, Header -from sqlalchemy.orm import Session -from sqlalchemy import and_, desc -from pydantic import BaseModel, Field -from typing import Optional, List -import json -import os -import secrets -import uuid -import logging -from datetime import datetime - -from ..config import APP_ACCESS_URL -from ..database import get_db -from ..models import ( - User, Channel, ChannelMember, ChannelMessage, - gen_uuid -) -from ..auth import get_current_user -from ..platform_adapter import get_platform_adapter, list_platforms, platform_cls_map, platform_registry -from ..message_session import MessageSession, SessionType - -logger = logging.getLogger("aiflow") -router = APIRouter() - -# ============================================================ -# 平台接入配置 - 按 QwenPaw 标准定义 -# 每个平台包含:凭证字段、AI行为开关、IM交互策略 -# ============================================================ - -# 公共AI行为配置字段 -COMMON_AI_SETTINGS = { - "bot_prefix": { - "key": "bot_prefix", "label": "Bot Prefix", "type": "text", - "default": "@bot", "placeholder": "@bot", - "tooltip": "唤醒机器人的前缀命令,用户消息以此为前缀时触发AI处理" - }, - "show_tool_msg": { - "key": "show_tool_msg", "label": "显示工具消息", "type": "switch", - "default": True, - "tooltip": "是否在对话中显示AI调用工具(Function Call/MCP)时的中间消息" - }, - "show_thinking": { - "key": "show_thinking", "label": "显示思考过程", "type": "switch", - "default": False, - "tooltip": "是否显示AI推理过程(如Chain-of-Thought),开启后会展示思考链" - }, - "stream_output": { - "key": "stream_output", "label": "流式输出", "type": "switch", - "default": True, - "tooltip": "AI回复是否以流式(逐字)方式推送到IM平台" - }, - "private_chat_strategy": { - "key": "private_chat_strategy", "label": "私聊策略", "type": "select", - "options": ["开放", "白名单", "关闭"], - "default": "开放", - "tooltip": "私聊场景下的AI响应策略:开放=所有私聊均可触发AI,白名单=仅白名单用户可触发,关闭=不响应私聊" - }, - "group_chat_strategy": { - "key": "group_chat_strategy", "label": "群聊策略", "type": "select", - "options": ["开放", "需要@提及", "白名单", "关闭"], - "default": "开放", - "tooltip": "群聊场景下的AI响应策略:开放=群中所有消息均触发,需要@提及=仅被@Bot的消息触发" - }, - "require_mention": { - "key": "require_mention", "label": "需要@提及", "type": "switch", - "default": True, - "tooltip": "群聊中是否需要@机器人才会触发AI响应" - }, - "whitelist_users": { - "key": "whitelist_users", "label": "白名单用户", "type": "tag_input", - "default": [], - "tooltip": "白名单策略下的用户ID列表,输入用户ID后按回车添加" - }, - "media_dir": { - "key": "media_dir", "label": "媒体文件目录", "type": "text", - "default": "/app/working/workspaces/default/media", - "placeholder": "/app/working/workspaces/default/media", - "tooltip": "IM端传输的媒体文件(图片/视频/音频)在服务端的存储目录" - }, -} - -# 飞书频道配置 -PLATFORM_FEISHU = { - "name": "飞书", - "description": "通过飞书开放平台应用接入,支持飞书国内/海外版本,可扫码自动创建飞书Bot", - "doc_url": "https://open.feishu.cn/document/home/index", - "enable_region": True, # 支持地区选择 - "regions": ["飞书国内", "飞书海外"], - "qr_config": { - "title": "扫码创建飞书机器人", - "description": "打开飞书开发者后台,使用飞书管理员账号扫码授权,系统将自动创建飞书应用并获取 App ID 和 App Secret。请将获取到的凭证填入下方表单。", - "qr_url": "https://open.feishu.cn/app", # 飞书开放平台 - "action_label": "打开飞书开发者后台", - "flow_hint": "扫码后请将获取的 App ID 和 App Secret 填入以下表单完成绑定" - }, - "credential_fields": [ - {"key": "app_id", "label": "App ID", "type": "text", "required": True, - "placeholder": "从飞书开放平台获取应用App ID", "tooltip": "飞书应用的唯一标识,在开发者后台-凭证与基础信息中获取"}, - {"key": "app_secret", "label": "App Secret", "type": "password", "required": True, - "placeholder": "从飞书开放平台获取应用App Secret", "tooltip": "飞书应用的密钥,需保密存储。在开发者后台-凭证与基础信息中获取"}, - {"key": "encrypt_key", "label": "Encrypt Key", "type": "password", "required": False, - "placeholder": "飞书事件订阅的Encrypt Key(选填)", "tooltip": "事件订阅的加密密钥,配置了事件订阅时需要填写"}, - {"key": "verification_token", "label": "Verification Token", "type": "password", "required": False, - "placeholder": "飞书事件订阅的Verification Token(选填)", "tooltip": "事件订阅的验证Token,配置了事件订阅时需要填写"}, - ], - "ai_settings": COMMON_AI_SETTINGS, -} - -# 微信开放平台配置(需要用户提供微信开放平台AppID和AppSecret) -PLATFORM_WECHAT = { - "name": "微信", - "description": "通过微信ClawBot官方API接入,扫描二维码授权后AI机器人将成为您的微信联系人,可通过微信个人号与AI对话。", - "doc_url": "https://developers.weixin.qq.com/doc/aispeech/knowledge/openapi/Clawbotrelated.html", - "qr_config": { - "title": "扫码绑定微信个人号", - "description": "调用微信ClawBot官方API生成授权二维码,使用微信扫码后确认授权,即可将AI机器人绑定到您的微信个人号。", - "qr_url": "", # 动态生成 - 调用ClawBot API获取 - "action_label": "生成绑定二维码", - "flow_hint": "点击下方按钮调用微信ClawBot API生成授权二维码,使用微信扫码后确认授权", - "config_guide": False - }, - "platform_tips": [ - {"type": "info", "content": "需要先在微信开放平台(open.weixin.qq.com)注册网站应用并获取AppID和AppSecret,点击下方按钮生成授权二维码,用微信扫码完成授权"}, - {"type": "warning", "content": "微信个人号每条消息最多回复10条内容,建议关闭「显示工具消息」和「显示思考过程」"}, - ], - "credential_fields": [ - {"key": "appid", "label": "AppID", "type": "text", "required": True, - "placeholder": "微信开放平台网站应用的AppID", "tooltip": "在微信开放平台-管理中心-网站应用详情页获取"}, - {"key": "appsecret", "label": "AppSecret", "type": "password", "required": True, - "placeholder": "微信开放平台网站应用的AppSecret", "tooltip": "在微信开放平台-管理中心-网站应用详情页获取,需保密存储"}, - {"key": "bot_token", "label": "Bot Token", "type": "password", "required": False, - "placeholder": "微信机器人Token(选填)", "tooltip": "如已获取Bot Token可填入,否则可留空"}, - ], - "extra_settings": { - "message_merge": { - "key": "message_merge", "label": "消息合并", "type": "switch", - "default": False, - "tooltip": "将多条回复合并为一条消息发送(可突破单条10条回复限制)" - }, - "merge_delay": { - "key": "merge_delay", "label": "合并延时(毫秒)", "type": "number", - "default": 500, "min": 100, "max": 5000, - "show_if": {"message_merge": True}, - "tooltip": "合并窗口等待时长" - }, - }, - "ai_settings": { - **COMMON_AI_SETTINGS, - "show_tool_msg": {**COMMON_AI_SETTINGS["show_tool_msg"], "default": False}, - "show_thinking": {**COMMON_AI_SETTINGS["show_thinking"], "default": False}, - }, -} - -# 钉钉配置 -PLATFORM_DINGTALK = { - "name": "钉钉", - "description": "钉钉机器人接入,通过钉钉开放平台创建企业内部应用或自定义机器人", - "doc_url": "https://open.dingtalk.com/document/", - "qr_config": { - "title": "扫码登录钉钉开发者后台", - "description": "使用钉钉管理员账号扫码登录钉钉开放平台,创建企业内部应用获取 AppKey 和 AppSecret。", - "qr_url": "https://open-dev.dingtalk.com/", - "action_label": "打开钉钉开放平台", - "flow_hint": "扫码后在开发者后台创建应用,将获取的凭证填入下方表单" - }, - "credential_fields": [ - {"key": "app_key", "label": "AppKey", "type": "text", "required": True, - "placeholder": "钉钉开放平台应用的AppKey", "tooltip": "钉钉应用的唯一标识,在钉钉开发者后台获取"}, - {"key": "app_secret", "label": "AppSecret", "type": "password", "required": True, - "placeholder": "钉钉开放平台应用的AppSecret", "tooltip": "钉钉应用的密钥"}, - {"key": "webhook_url", "label": "Webhook URL", "type": "text", "required": False, - "placeholder": "钉钉群机器人的Webhook地址(选填)", "tooltip": "如果使用自定义机器人方式接入,填写Webhook地址"}, - ], - "ai_settings": COMMON_AI_SETTINGS, -} - -# WhatsApp配置 -PLATFORM_WHATSAPP = { - "name": "WhatsApp", - "description": "Meta WhatsApp Business Platform API接入,需先配置Meta开发者应用并关联WhatsApp Business号码", - "doc_url": "https://developers.facebook.com/docs/whatsapp/cloud-api", - "credential_fields": [ - {"key": "phone_number_id", "label": "Phone Number ID", "type": "text", "required": True, - "placeholder": "WhatsApp Business号码ID", "tooltip": "在Meta开发者后台 WhatsApp-配置 中获取"}, - {"key": "access_token", "label": "Access Token", "type": "password", "required": True, - "placeholder": "Meta Business API的Access Token", "tooltip": "长期访问令牌,在Meta开发者后台生成"}, - {"key": "verify_token", "label": "Verify Token", "type": "text", "required": False, - "placeholder": "Webhook验证Token(选填)", "tooltip": "自定义的Webhook验证字符串"}, - ], - "ai_settings": COMMON_AI_SETTINGS, -} - -# Telegram配置 -PLATFORM_TELEGRAM = { - "name": "Telegram", - "description": "Telegram Bot API接入,通过@BotFather创建Bot获取Token", - "doc_url": "https://core.telegram.org/bots/api", - "credential_fields": [ - {"key": "bot_token", "label": "Bot Token", "type": "password", "required": True, - "placeholder": "从@BotFather获取的Bot Token", "tooltip": "通过Telegram @BotFather 创建机器人后获取的Token"}, - ], - "ai_settings": COMMON_AI_SETTINGS, -} - -# Slack配置 -PLATFORM_SLACK = { - "name": "Slack", - "description": "Slack App接入,通过Slack API创建Bot并配置OAuth权限", - "doc_url": "https://api.slack.com/docs", - "credential_fields": [ - {"key": "bot_token", "label": "Bot User OAuth Token", "type": "password", "required": True, - "placeholder": "Slack App的Bot User OAuth Token (xoxb-...)", "tooltip": "在Slack API后台 OAuth & Permissions 中获取"}, - {"key": "app_token", "label": "App-Level Token", "type": "password", "required": False, - "placeholder": "Socket Mode使用的App Token (xapp-...)(选填)", "tooltip": "如使用Socket Mode连接,需要配置App-Level Token"}, - {"key": "signing_secret", "label": "Signing Secret", "type": "password", "required": False, - "placeholder": "Slack签名密钥(选填)", "tooltip": "用于验证Slack请求签名的密钥"}, - ], - "ai_settings": COMMON_AI_SETTINGS, -} - -PLATFORM_CONFIGS = { - "feishu": PLATFORM_FEISHU, - "wechat": PLATFORM_WECHAT, - "dingtalk": PLATFORM_DINGTALK, - "whatsapp": PLATFORM_WHATSAPP, - "telegram": PLATFORM_TELEGRAM, - "slack": PLATFORM_SLACK, -} - -VALID_PLATFORMS = ("feishu", "wechat", "wecom", "dingtalk", "whatsapp", "telegram", "slack") - -SENSITIVE_FIELDS = {"app_secret", "secret", "access_token", "bot_token", "signing_secret", "token", - "api_key", "password", "encrypt_key", "verification_token", "app_token", - "verify_token", "app_key", "encoding_aes_key", "appsecret"} - - -# ============================================================ -# Pydantic Models -# ============================================================ - -class CreateChannelRequest(BaseModel): - name: str = Field(min_length=1, max_length=100) - description: Optional[str] = None - platform: str = Field(default="feishu") - credential: Optional[dict] = None - webhook_url: Optional[str] = None - settings: Optional[dict] = None - region: Optional[str] = None - - -class UpdateChannelRequest(BaseModel): - name: Optional[str] = None - description: Optional[str] = None - credential: Optional[dict] = None - webhook_url: Optional[str] = None - settings: Optional[dict] = None - is_active: Optional[bool] = None - region: Optional[str] = None - - -class WebhookMessageRequest(BaseModel): - from_user: str = Field(min_length=1, max_length=100) - content: str = Field(min_length=1, max_length=10000) - message_type: str = Field(default="text") - - -class SendChannelMessageRequest(BaseModel): - content: str = Field(min_length=1, max_length=10000) - - -class AuthorizeRequest(BaseModel): - platform: str = Field(default="feishu") - - -class InviteMemberRequest(BaseModel): - user_id: str - - -# ============================================================ -# Helper Functions -# ============================================================ - -def _check_channel_access(db: Session, channel_id: str, user_id: str): - member = db.query(ChannelMember).filter( - and_(ChannelMember.channel_id == channel_id, ChannelMember.user_id == user_id) - ).first() - if not member: - raise HTTPException(status_code=403, detail="您不是该频道成员") - return member - - -def _check_channel_owner(db: Session, channel_id: str, user_id: str): - member = db.query(ChannelMember).filter( - and_(ChannelMember.channel_id == channel_id, ChannelMember.user_id == user_id) - ).first() - if not member or member.role != "owner": - raise HTTPException(status_code=403, detail="仅频道所有者可执行此操作") - return member - - -def _mask_credential(credential: dict) -> dict: - """对凭证中的敏感字段进行脱敏处理""" - if not credential: - return {} - masked = {} - for key, value in credential.items(): - value_str = str(value) if value else "" - if key in SENSITIVE_FIELDS and len(value_str) > 8: - masked[key] = value_str[:4] + "***...***" + value_str[-4:] - elif key in SENSITIVE_FIELDS and len(value_str) > 0: - masked[key] = value_str[:2] + "****" - else: - masked[key] = value - return masked - - -def _verify_credential(platform: str, credential: dict) -> tuple: - """验证平台凭证是否完整""" - if platform not in PLATFORM_CONFIGS: - return False, f"不支持的平台类型: {platform}" - - config = PLATFORM_CONFIGS[platform] - required_fields = [f for f in config["credential_fields"] if f.get("required")] - - for field in required_fields: - if not credential or field["key"] not in credential or not credential[field["key"]]: - return False, f"缺少必填字段: {field['label']} ({field['key']})" - - return True, "凭证验证通过" - - -def _get_default_settings(platform: str) -> dict: - """获取平台默认的AI行为设置""" - if platform not in PLATFORM_CONFIGS: - return {} - ai_settings = PLATFORM_CONFIGS[platform].get("ai_settings", {}) - defaults = {} - for key, cfg in ai_settings.items(): - if cfg.get("type") == "switch": - defaults[key] = cfg.get("default", False) - elif cfg.get("type") == "select": - defaults[key] = cfg.get("default", "") - elif cfg.get("type") == "tag_input": - defaults[key] = cfg.get("default", []) - elif cfg.get("type") == "text": - defaults[key] = cfg.get("default", "") - elif cfg.get("type") == "number": - defaults[key] = cfg.get("default", 0) - return defaults - - -def _build_channel_response(channel: Channel, db: Session, user_id: str, include_credential: bool = False) -> dict: - """构建频道响应数据""" - member = db.query(ChannelMember).filter( - and_(ChannelMember.channel_id == channel.id, ChannelMember.user_id == user_id) - ).first() - - member_count = db.query(ChannelMember).filter(ChannelMember.channel_id == channel.id).count() - message_count = db.query(ChannelMessage).filter(ChannelMessage.channel_id == channel.id).count() - - data = { - "id": channel.id, - "name": channel.name, - "description": channel.description, - "platform": channel.platform, - "status": channel.status, - "is_active": channel.is_active, - "webhook_url": channel.webhook_url, - "role": member.role if member else None, - "member_count": member_count, - "message_count": message_count, - "default_agent_id": channel.default_agent_id, - "default_skill_id": channel.default_skill_id, - "knowledge_base_id": channel.knowledge_base_id, - "bound_model_id": channel.bound_model_id, - "settings": channel.settings, - "last_active": str(channel.last_active) if channel.last_active else None, - "created_at": str(channel.created_at) if channel.created_at else None, - "updated_at": str(channel.updated_at) if channel.updated_at else None, - } - - if include_credential: - data["credential"] = channel.credential - data["credential_masked"] = _mask_credential(channel.credential) if channel.credential else None - else: - data["credential_masked"] = _mask_credential(channel.credential) if channel.credential else None - - return data - - -# ============================================================ -# API Endpoints -# ============================================================ - -# ---- 平台配置 ---- - -@router.get("/platforms/config") -def get_platform_configs(): - """获取各平台完整接入配置(QwenPaw标准),含凭证字段+AI行为配置+平台提示""" - result = {} - for platform_key, config in PLATFORM_CONFIGS.items(): - result[platform_key] = { - "name": config["name"], - "description": config["description"], - "doc_url": config["doc_url"], - "credential_fields": config["credential_fields"], - "ai_settings": config.get("ai_settings", {}), - "extra_settings": config.get("extra_settings", {}), - "platform_tips": config.get("platform_tips", []), - "enable_region": config.get("enable_region", False), - "regions": config.get("regions", []), - "qr_config": config.get("qr_config"), # QR scan config for WeChat/Feishu/DingTalk - } - return {"code": 0, "data": result, "message": "ok"} - - -# ---- 扫码授权 ---- - -@router.post("/authorize/scan") -def scan_authorize( - platform: str = Query(...), - body: dict = Body({}), - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - """扫码授权流程 - 返回授权二维码(微信平台统一调用ClawBot API)""" - if platform not in VALID_PLATFORMS: - raise HTTPException(status_code=400, detail="不支持的平台类型") - - platform_name = PLATFORM_CONFIGS.get(platform, {}).get("name", platform) - - # 微信平台:调用微信ClawBot API获取授权二维码 - if platform == "wechat": - # 获取AppID和Secret(优先从请求体 > 已保存频道配置 > 环境变量) - appid = "" - secret = "" - - # 1. 从请求体获取 - req_creds = body.get("credentials", {}) or {} - appid = req_creds.get("appid", "") - secret = req_creds.get("appsecret", "") or req_creds.get("secret", "") - - # 查找已保存的微信频道 - existing_channel = db.query(Channel).filter( - and_(Channel.platform == "wechat", Channel.owner_id == current_user.id) - ).first() - - # 2. 从已保存的频道配置获取 - if not appid or not secret: - if existing_channel and existing_channel.credential: - creds = existing_channel.credential - if isinstance(creds, str): - creds = json.loads(creds) - if not appid: - appid = creds.get("appid", "") - if not secret: - secret = creds.get("appsecret", "") - - # 3. 从环境变量获取兜底 - if not appid: - appid = os.environ.get("WECHAT_APPID", "") - if not secret: - secret = os.environ.get("WECHAT_SECRET", "") - - # 没有凭证时直接返回错误,不回退到本地QR - if not appid or not secret: - print(f"[WARN] WeChat credentials not found: appid={'set' if appid else 'missing'}, secret={'set' if secret else 'missing'}") - return { - "code": 1, - "data": None, - "message": "缺少微信AppID/AppSecret凭证,请在频道设置中填写或通过请求体提供", - } - - # 有凭证 - 调用微信ClawBot API - import hmac - import hashlib - import time as time_module - - clawbot_base = "https://weknora.weixin.qq.com" - request_id = secrets.token_hex(16) - timestamp = str(int(time_module.time())) - nonce = secrets.token_hex(8) - request_body = "{}" - - sign_string = f"{timestamp}\n{nonce}\n{request_id}\n{request_body}" - if isinstance(sign_string, str): - sign_string = sign_string.encode('utf-8') - signature = hmac.new( - secret.encode('utf-8'), - sign_string, - hashlib.sha256 - ).hexdigest() - - headers = { - "Content-Type": "application/json", - "X-APPID": appid, - "X-Request-ID": request_id, - "X-Timestamp": timestamp, - "X-Nonce": nonce, - "X-Signature": signature, - } - - print(f"[INFO] Calling ClawBot API: {clawbot_base}/api/v1/wechat/qrcode with appid={appid[:8]}***") - - try: - import requests as http_requests - resp = http_requests.post( - f"{clawbot_base}/api/v1/wechat/qrcode", - json={}, - headers=headers, - timeout=10, - ) - resp_body = resp.text - print(f"[INFO] ClawBot API response: HTTP {resp.status_code}, body={resp_body[:300]}") - - # 尝试解析JSON - try: - qrcode_data = resp.json() - except ValueError: - print(f"[WARN] ClawBot API returned non-JSON response") - return { - "code": 1, - "data": None, - "message": f"微信ClawBot API返回了非JSON响应: {resp_body[:200]}", - } - - # 微信API成功响应: {"success": true, "data": {"qrcode_url": "...", "qrcode": "..."}} - if resp.status_code == 200 and qrcode_data.get("success") and qrcode_data.get("data"): - qr_url = qrcode_data["data"].get("qrcode_url", "") - qrcode_token = qrcode_data["data"].get("qrcode", "") - - if existing_channel: - existing_channel.oauth_state = qrcode_token - db.commit() - else: - # 如果没有现有频道,生成一个临时token保存在响应中 - print(f"[INFO] No existing channel found, returning qrcode token directly") - - print(f"[INFO] ClawBot real QR obtained: token={qrcode_token}") - return { - "code": 0, - "data": { - "platform": platform, - "platform_name": platform_name, - "qr_url": qr_url, - "qrcode": qrcode_token, - "qrcode_token": qrcode_token, - "status": "waiting", - "is_mock": False, - "message": "请使用微信扫描二维码完成授权", - "expires_in": 300, - }, - "message": "ok", - } - - # ClawBot API返回了错误 - error_msg = "" - if qrcode_data.get("error"): - err = qrcode_data["error"] - error_msg = err.get("message", "") or err.get("detail", "") or str(err) - elif qrcode_data.get("message"): - error_msg = qrcode_data["message"] - print(f"[WARN] ClawBot API returned error (HTTP {resp.status_code}): {error_msg}") - return { - "code": 1, - "data": {"error": error_msg or f"HTTP {resp.status_code}"}, - "message": f"微信ClawBot API调用失败: {error_msg or '未知错误'}", - } - - except Exception as e: - print(f"[ERROR] ClawBot API call exception: {e}") - import traceback - traceback.print_exc() - return { - "code": 1, - "data": {"error": str(e)}, - "message": f"微信ClawBot API调用异常: {str(e)}", - } - - # 其他平台:返回模拟授权数据 - binding_token = str(uuid.uuid4()) - - # 查找已有频道并关联binding_token - existing_channel = db.query(Channel).filter( - and_(Channel.platform == platform, Channel.owner_id == current_user.id) - ).first() - if existing_channel: - existing_channel.oauth_state = binding_token - db.commit() - - return { - "code": 0, - "data": { - "platform": platform, - "platform_name": platform_name, - "qr_url": "", - "qrcode": binding_token, - "status": "waiting", - "message": f"请在{platform_name}客户端完成授权", - "expires_in": 300, - }, - "message": "ok", - } - - -@router.post("/authorize/scan/status") -def scan_authorize_status( - platform: str = Query(...), - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - """查询扫码授权状态(微信平台调用ClawBot API轮询)""" - if platform not in VALID_PLATFORMS: - raise HTTPException(status_code=400, detail="不支持的平台类型") - - # 微信平台:调用ClawBot API轮询扫码状态 - if platform == "wechat": - channel = db.query(Channel).filter( - and_(Channel.platform == "wechat", Channel.owner_id == current_user.id) - ).first() - - qrcode_token = channel.oauth_state if channel else None - - if not qrcode_token: - return { - "code": 0, - "data": {"platform": platform, "status": "expired"}, - "message": "二维码已过期,请重新生成", - } - - # 获取AppID和Secret用于签名 - appid = os.environ.get("WECHAT_APPID", "") - secret = os.environ.get("WECHAT_SECRET", "") - if channel and channel.credential: - creds = channel.credential - if isinstance(creds, str): - creds = json.loads(creds) - appid = creds.get("appid", appid) - secret = creds.get("appsecret", secret) - - import hmac - import hashlib - import time as time_module - - clawbot_base = "https://weknora.weixin.qq.com" - request_id = secrets.token_hex(16) - timestamp = str(int(time_module.time())) - nonce = secrets.token_hex(8) - request_body = json.dumps({"qrcode": qrcode_token}) - - sign_string = f"{timestamp}\n{nonce}\n{request_id}\n{request_body}" - if isinstance(sign_string, str): - sign_string = sign_string.encode('utf-8') - signature = hmac.new( - secret.encode('utf-8'), - sign_string, - hashlib.sha256 - ).hexdigest() - - headers = { - "Content-Type": "application/json", - "X-APPID": appid, - "X-Request-ID": request_id, - "X-Timestamp": timestamp, - "X-Nonce": nonce, - "X-Signature": signature, - } - - try: - import requests as http_requests - resp = http_requests.post( - f"{clawbot_base}/api/v1/wechat/qrcode/status", - json={"qrcode": qrcode_token}, - headers=headers, - timeout=30, - ) - status_data = resp.json() - print(f"[INFO] ClawBot QR status: {json.dumps(status_data, ensure_ascii=False)[:300]}") - - if status_data.get("success") and status_data.get("data"): - data = status_data["data"] - status = data.get("status", "wait") - - if status == "confirmed": - # 保存授权凭据 - credentials = data.get("credentials", {}) - baseurl = data.get("baseurl", clawbot_base) - if channel: - creds_store = channel.credential or {} - if isinstance(creds_store, str): - creds_store = json.loads(creds_store) - creds_store["bot_token"] = credentials.get("bot_token", "") - creds_store["ilink_bot_id"] = credentials.get("ilink_bot_id", "") - creds_store["ilink_user_id"] = credentials.get("ilink_user_id", "") - creds_store["baseurl"] = baseurl - creds_store["authorized_at"] = datetime.now().isoformat() - channel.credential = creds_store - channel.status = "connected" - channel.oauth_state = "" - db.commit() - print(f"[INFO] WeChat channel connected: channel_id={channel.id}") - - return { - "code": 0, - "data": { - "platform": platform, - "status": "authorized", - "credentials": credentials, - "baseurl": baseurl, - }, - "message": "授权成功!微信个人号已绑定", - } - else: - return { - "code": 0, - "data": {"platform": platform, "status": status}, - "message": f"状态: {status}", - } - else: - print(f"[WARN] ClawBot QR status API returned unexpected response: {json.dumps(status_data, ensure_ascii=False)[:200]}") - return { - "code": 0, - "data": {"platform": platform, "status": "error"}, - "message": status_data.get("message", "查询状态失败"), - } - except Exception as e: - print(f"[ERROR] ClawBot status check failed: {e}") - import traceback - traceback.print_exc() - return { - "code": 1, - "data": {"platform": platform, "status": "error"}, - "message": f"微信ClawBot状态查询异常: {str(e)}", - } - - # 其他平台:返回模拟授权状态 - access_token = f"sim_{platform}_{secrets.token_hex(16)}" - return { - "code": 0, - "data": { - "platform": platform, - "status": "authorized", - "access_token": access_token, - "credential": _generate_mock_credential(platform), - }, - "message": "授权成功", - } - - -def _generate_mock_credential(platform: str) -> dict: - """根据平台生成模拟凭证""" - mock_map = { - "feishu": {"app_id": "cli_a" + secrets.token_hex(8), "app_secret": secrets.token_hex(16)}, - "wechat": {"appid": "wx" + secrets.token_hex(8), "appsecret": secrets.token_hex(16)}, - "dingtalk": {"app_key": "ding" + secrets.token_hex(8), "app_secret": secrets.token_hex(16)}, - "whatsapp": {"phone_number_id": "10" + "".join([str(secrets.randbelow(10)) for _ in range(13)]), - "access_token": "EAA" + secrets.token_hex(32)}, - "telegram": {"bot_token": f"{secrets.randbelow(10**9)}:AA" + secrets.token_hex(16)}, - "slack": {"bot_token": "xoxb-" + secrets.token_hex(20)}, - } - return mock_map.get(platform, {"token": secrets.token_hex(16)}) - - -# ---- 频道列表 ---- - -@router.get("/") -def list_channels( - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - """获取用户所在的所有频道""" - memberships = db.query(ChannelMember).filter( - ChannelMember.user_id == current_user.id - ).all() - - channel_ids = [m.channel_id for m in memberships] - if not channel_ids: - return {"code": 0, "data": [], "message": "ok"} - - channels = db.query(Channel).filter(Channel.id.in_(channel_ids)).order_by(desc(Channel.updated_at)).all() - - result = [] - for ch in channels: - member = next((m for m in memberships if m.channel_id == ch.id), None) - member_count = db.query(ChannelMember).filter(ChannelMember.channel_id == ch.id).count() - message_count = db.query(ChannelMessage).filter(ChannelMessage.channel_id == ch.id).count() - result.append({ - "id": ch.id, - "name": ch.name, - "description": ch.description, - "platform": ch.platform, - "status": ch.status, - "is_active": ch.is_active, - "role": member.role if member else None, - "member_count": member_count, - "message_count": message_count, - "credential_masked": _mask_credential(ch.credential) if ch.credential else None, - "created_at": str(ch.created_at) if ch.created_at else None, - "updated_at": str(ch.updated_at) if ch.updated_at else None, - }) - - return {"code": 0, "data": result, "message": "ok"} - - -# ---- 创建频道 (静态路由必须在动态路由之前) ---- - -@router.post("/") -def create_channel( - req: CreateChannelRequest, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - """创建新频道,支持QwenPaw标准配置""" - if req.platform not in VALID_PLATFORMS: - raise HTTPException(status_code=400, detail="不支持的平台类型") - - # 构建默认 settings(AI行为配置) - default_ai_settings = _get_default_settings(req.platform) - merged_settings = {**default_ai_settings, **(req.settings or {})} - - if req.region: - merged_settings["region"] = req.region - - channel = Channel( - id=gen_uuid(), - owner_id=current_user.id, - name=req.name, - description=req.description, - platform=req.platform, - webhook_url=req.webhook_url, - credential=req.credential, - settings=merged_settings, - status="pending", - is_active=True, - created_at=datetime.now(), - updated_at=datetime.now(), - ) - db.add(channel) - db.flush() - - member = ChannelMember( - id=gen_uuid(), - channel_id=channel.id, - user_id=current_user.id, - role="owner", - joined_at=datetime.now(), - ) - db.add(member) - - db.commit() - db.refresh(channel) - - return { - "code": 0, - "data": _build_channel_response(channel, db, current_user.id, include_credential=True), - "message": "频道创建成功", - } - - -# ---- 频道详情 ---- - -@router.get("/{channel_id}") -def get_channel( - channel_id: str, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - """获取频道详情""" - _check_channel_access(db, channel_id, current_user.id) - channel = db.query(Channel).filter(Channel.id == channel_id).first() - if not channel: - raise HTTPException(status_code=404, detail="频道不存在") - - return { - "code": 0, - "data": _build_channel_response(channel, db, current_user.id, include_credential=True), - "message": "ok", - } - - -# ---- 更新频道配置 ---- - -@router.put("/{channel_id}") -def update_channel( - channel_id: str, - req: UpdateChannelRequest, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - """更新频道配置,支持完整QwenPaw标准settings""" - _check_channel_owner(db, channel_id, current_user.id) - channel = db.query(Channel).filter(Channel.id == channel_id).first() - if not channel: - raise HTTPException(status_code=404, detail="频道不存在") - - if req.name is not None: - channel.name = req.name - if req.description is not None: - channel.description = req.description - if req.webhook_url is not None: - channel.webhook_url = req.webhook_url - if req.region is not None: - existing_settings = channel.settings or {} - existing_settings["region"] = req.region - channel.settings = existing_settings - if req.credential is not None: - is_valid, err_msg = _verify_credential(channel.platform, req.credential) - if not is_valid: - raise HTTPException(status_code=400, detail=err_msg) - channel.credential = req.credential - if req.settings is not None: - # 合并 settings(保留未提交的配置项) - existing = channel.settings or {} - channel.settings = {**existing, **req.settings} - settings = channel.settings - if "agent_id" in settings: - channel.default_agent_id = settings["agent_id"] - if "knowledge_base_id" in settings: - channel.knowledge_base_id = settings["knowledge_base_id"] - if "skill_ids" in settings: - skill_ids = settings["skill_ids"] - if isinstance(skill_ids, list) and len(skill_ids) > 0: - channel.default_skill_id = skill_ids[0] - if req.is_active is not None: - channel.is_active = req.is_active - - channel.updated_at = datetime.now() - - db.commit() - db.refresh(channel) - - return { - "code": 0, - "data": _build_channel_response(channel, db, current_user.id, include_credential=True), - "message": "更新成功", - } - - -# ---- 删除频道 ---- - -@router.delete("/{channel_id}") -def delete_channel( - channel_id: str, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - """删除频道及其关联数据""" - _check_channel_owner(db, channel_id, current_user.id) - channel = db.query(Channel).filter(Channel.id == channel_id).first() - if not channel: - raise HTTPException(status_code=404, detail="频道不存在") - - db.query(ChannelMessage).filter(ChannelMessage.channel_id == channel_id).delete() - db.query(ChannelMember).filter(ChannelMember.channel_id == channel_id).delete() - db.delete(channel) - db.commit() - - return {"code": 0, "message": "频道已删除"} - - -# ---- 连接/断开频道 ---- - -@router.post("/{channel_id}/connect") -def connect_channel( - channel_id: str, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - """连接频道""" - _check_channel_owner(db, channel_id, current_user.id) - channel = db.query(Channel).filter(Channel.id == channel_id).first() - if not channel: - raise HTTPException(status_code=404, detail="频道不存在") - - platform = channel.platform - if platform not in PLATFORM_CONFIGS: - raise HTTPException(status_code=400, detail=f"不支持的平台类型: {platform}") - - if not channel.credential: - raise HTTPException(status_code=400, detail="尚未配置平台凭证,请先填写接入配置") - - is_valid, err_msg = _verify_credential(platform, channel.credential) - if not is_valid: - raise HTTPException(status_code=400, detail=f"凭证配置不完整: {err_msg}") - - channel.status = "connected" - channel.is_active = True - channel.last_active = datetime.now() - channel.updated_at = datetime.now() - db.commit() - db.refresh(channel) - - platform_name = PLATFORM_CONFIGS[platform]["name"] - return { - "code": 0, - "data": { - "connected": True, - "platform": platform, - "platform_name": platform_name, - "status": "connected", - "message": f"{platform_name}平台连接成功,AI机器人已在线", - "connected_at": str(datetime.now()), - }, - "message": "连接成功", - } - - -@router.post("/{channel_id}/disconnect") -def disconnect_channel( - channel_id: str, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - """断开频道连接""" - _check_channel_owner(db, channel_id, current_user.id) - channel = db.query(Channel).filter(Channel.id == channel_id).first() - if not channel: - raise HTTPException(status_code=404, detail="频道不存在") - - channel.status = "disconnected" - channel.updated_at = datetime.now() - db.commit() - db.refresh(channel) - - return { - "code": 0, - "data": {"id": channel.id, "status": channel.status, "message": "频道已断开连接"}, - "message": "已断开连接", - } - - -# ---- Webhook 模拟消息 ---- - -@router.post("/{channel_id}/webhook") -def webhook_receive_message( - channel_id: str, - req: WebhookMessageRequest, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - """模拟IM平台消息接收""" - _check_channel_access(db, channel_id, current_user.id) - channel = db.query(Channel).filter(Channel.id == channel_id).first() - if not channel: - raise HTTPException(status_code=404, detail="频道不存在") - - if channel.status != "connected": - raise HTTPException(status_code=400, detail="频道未连接,无法接收消息") - - user_msg = ChannelMessage( - id=gen_uuid(), - channel_id=channel_id, - user_id=current_user.id, - role="user", - content=f"[{req.from_user}] {req.content}", - created_at=datetime.now(), - ) - db.add(user_msg) - - platform_name = PLATFORM_CONFIGS.get(channel.platform, {}).get("name", "IM平台") - ai_reply = _generate_ai_reply(req.content, channel, platform_name, current_user) - - ai_msg = ChannelMessage( - id=gen_uuid(), - channel_id=channel_id, - user_id=current_user.id, - role="assistant", - content=ai_reply, - token_used=len(ai_reply), - created_at=datetime.now(), - ) - db.add(ai_msg) - - channel.last_active = datetime.now() - channel.updated_at = datetime.now() - - db.commit() - db.refresh(user_msg) - db.refresh(ai_msg) - - return { - "code": 0, - "data": { - "received": { - "id": user_msg.id, "from_user": req.from_user, "content": req.content, - "message_type": req.message_type, "created_at": str(user_msg.created_at), - }, - "reply": { - "id": ai_msg.id, "content": ai_reply, "role": "assistant", - "created_at": str(ai_msg.created_at), - }, - }, - "message": "消息已处理", - } - - -def _generate_ai_reply(user_content: str, channel: Channel, platform_name: str, user: User) -> str: - """生成模拟AI回复消息""" - content_lower = user_content.lower().strip() - - greetings = ["你好", "hi", "hello", "嗨", "早上好", "下午好", "晚上好"] - for g in greetings: - if g in content_lower: - return f"你好!我是通过{platform_name}接入的AI助手{channel.name}。我可以帮你解答问题、查询数据、执行任务等,请随时告诉我你的需求。" - - if "天气" in content_lower: - return "根据最新气象数据,今天天气晴好,气温22-30℃,适合外出活动。如需详细天气预报,请告知具体城市。" - if "销售" in content_lower or "数据" in content_lower: - return "已查询今日数据。截至当前,GMV为12.8万元,订单量326单,环比昨日同期增长8.5%。详细报表已整理完成,可随时导出。" - if "任务" in content_lower or "待办" in content_lower: - return "你当前有3项待办任务:\n1. 审核市场推广方案(截止今日18:00)\n2. 周报提交(截止明日10:00)\n3. 团队例会准备(截止本周五)\n需要我帮你处理哪一项?" - if "帮助" in content_lower or "help" in content_lower or "能做什么" in content_lower: - return f"我是通过{platform_name}接入的AI智能助手,可以为你提供以下服务:\n- 数据查询与分析\n- 任务管理与提醒\n- 文档撰写与总结\n- 智能问答与信息检索\n- 图表生成与报表导出\n请直接告诉我你的需求。" - if "谢谢" in content_lower or "感谢" in content_lower: - return "不客气!很高兴能帮到你。如有其他需要,随时找我。" - - content_preview = user_content[:50] + ("..." if len(user_content) > 50 else "") - return f"收到你的消息:{content_preview}。\n\n我是通过{platform_name}接入的AI助手,正在为你处理这个请求。请稍候,我会尽快给出回复。如果需要更具体的帮助,请详细描述你的需求。" - - -# ---- 频道消息 ---- - -@router.get("/{channel_id}/messages") -def get_channel_messages( - channel_id: str, - limit: int = Query(default=50, ge=1, le=200), - offset: int = Query(default=0, ge=0), - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - """获取频道消息列表""" - _check_channel_access(db, channel_id, current_user.id) - - messages = ( - db.query(ChannelMessage) - .filter(ChannelMessage.channel_id == channel_id) - .order_by(desc(ChannelMessage.created_at)) - .offset(offset) - .limit(limit) - .all() - ) - - result = [] - for msg in messages: - sender = db.query(User).filter(User.id == msg.user_id).first() - result.append({ - "id": msg.id, "channel_id": msg.channel_id, "user_id": msg.user_id, - "username": sender.username if sender else "unknown", - "avatar": sender.avatar if sender else None, - "role": msg.role, "content": msg.content, - "token_used": msg.token_used, - "created_at": str(msg.created_at) if msg.created_at else None, - }) - - return {"code": 0, "data": result, "message": "ok"} - - -@router.post("/{channel_id}/messages") -def send_channel_message( - channel_id: str, - req: SendChannelMessageRequest, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - """用户手动发送消息""" - _check_channel_access(db, channel_id, current_user.id) - channel = db.query(Channel).filter(Channel.id == channel_id).first() - if not channel: - raise HTTPException(status_code=404, detail="频道不存在") - - msg = ChannelMessage( - id=gen_uuid(), channel_id=channel_id, user_id=current_user.id, - role="user", content=req.content, created_at=datetime.now(), - ) - db.add(msg) - - platform_name = PLATFORM_CONFIGS.get(channel.platform, {}).get("name", "IM平台") - ai_reply = _generate_ai_reply(req.content, channel, platform_name, current_user) - ai_msg = ChannelMessage( - id=gen_uuid(), channel_id=channel_id, user_id=current_user.id, - role="assistant", content=ai_reply, token_used=len(ai_reply), - created_at=datetime.now(), - ) - db.add(ai_msg) - - channel.last_active = datetime.now() - channel.updated_at = datetime.now() - - db.commit() - db.refresh(msg) - db.refresh(ai_msg) - - return { - "code": 0, - "data": { - "message": {"id": msg.id, "channel_id": msg.channel_id, "user_id": msg.user_id, - "content": msg.content, "created_at": str(msg.created_at)}, - "reply": {"id": ai_msg.id, "content": ai_reply, "role": "assistant", - "created_at": str(ai_msg.created_at)}, - "webhook_response": {"status": "delivered", "platform": channel.platform, - "message": f"消息已通过{platform_name}通道成功推送"}, - }, - "message": "发送成功", - } - - -# ---- 频道成员管理 ---- - -@router.get("/{channel_id}/members") -def get_channel_members( - channel_id: str, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - """获取频道成员列表""" - _check_channel_access(db, channel_id, current_user.id) - - members = db.query(ChannelMember).filter(ChannelMember.channel_id == channel_id).all() - - result = [] - for m in members: - user = db.query(User).filter(User.id == m.user_id).first() - result.append({ - "id": m.id, "user_id": m.user_id, - "username": user.username if user else "unknown", - "avatar": user.avatar if user else None, - "role": m.role, - "joined_at": str(m.joined_at) if m.joined_at else None, - }) - - return {"code": 0, "data": result, "message": "ok"} - - -@router.post("/{channel_id}/members") -def invite_member( - channel_id: str, - req: InviteMemberRequest, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - """邀请成员加入频道""" - _check_channel_owner(db, channel_id, current_user.id) - - channel = db.query(Channel).filter(Channel.id == channel_id).first() - if not channel: - raise HTTPException(status_code=404, detail="频道不存在") - - target_user = db.query(User).filter(User.id == req.user_id).first() - if not target_user: - raise HTTPException(status_code=404, detail="用户不存在") - - existing = db.query(ChannelMember).filter( - and_(ChannelMember.channel_id == channel_id, ChannelMember.user_id == req.user_id) - ).first() - if existing: - raise HTTPException(status_code=400, detail="该用户已是频道成员") - - member = ChannelMember( - id=gen_uuid(), channel_id=channel_id, user_id=req.user_id, - role="member", joined_at=datetime.now(), - ) - db.add(member) - channel.updated_at = datetime.now() - db.commit() - db.refresh(member) - - return {"code": 0, "data": { - "id": member.id, "user_id": member.user_id, "role": member.role, - "joined_at": str(member.joined_at), - }, "message": "邀请成功"} - - -@router.delete("/{channel_id}/members/{member_user_id}") -def remove_member( - channel_id: str, - member_user_id: str, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - """移除频道成员""" - _check_channel_owner(db, channel_id, current_user.id) - - if member_user_id == current_user.id: - raise HTTPException(status_code=400, detail="不能移除自己,请使用删除频道功能") - - member = db.query(ChannelMember).filter( - and_(ChannelMember.channel_id == channel_id, ChannelMember.user_id == member_user_id) - ).first() - if not member: - raise HTTPException(status_code=404, detail="该用户不在频道中") - - db.delete(member) - - channel = db.query(Channel).filter(Channel.id == channel_id).first() - if channel: - channel.updated_at = datetime.now() - - db.commit() - return {"code": 0, "message": "成员已移除"} - - -# ---- 授权(兼容) ---- - -@router.post("/authorize") -def authorize_platform( - req: AuthorizeRequest, - current_user: User = Depends(get_current_user), -): - """OAuth授权流程(兼容旧接口)""" - if req.platform not in VALID_PLATFORMS: - raise HTTPException(status_code=400, detail="不支持的平台类型") - - access_token = f"sim_{req.platform}_{secrets.token_hex(16)}" - - return { - "code": 0, - "data": { - "authorized": True, - "platform": req.platform, - "access_token": access_token, - }, - "message": "ok", - } - - -# ---- OAuth2回调与频道重置 ---- - -@router.post("/authorize/scan/channel_reset") -def channel_reset( - channel_id: str = Query(...), - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - """重置微信IM通道 - 对应ClawBot文档 POST /api/v1/wechat/channel_reset""" - channel = db.query(Channel).filter( - and_(Channel.id == channel_id, Channel.owner_id == current_user.id) - ).first() - if not channel: - raise HTTPException(status_code=404, detail="频道不存在") - - # 微信平台:调用ClawBot API重置通道 - if channel.platform == "wechat": - # 获取AppID和Secret用于签名 - appid = os.environ.get("WECHAT_APPID", "") - secret = os.environ.get("WECHAT_SECRET", "") - if channel.credential: - creds = channel.credential - if isinstance(creds, str): - creds = json.loads(creds) - appid = creds.get("appid", appid) - secret = creds.get("appsecret", secret) - - import hmac - import hashlib - import time as time_module - - clawbot_base = "https://weknora.weixin.qq.com" - request_id = secrets.token_hex(16) - timestamp = str(int(time_module.time())) - nonce = secrets.token_hex(8) - request_body = json.dumps({"channel_id": channel_id}) - - sign_string = f"{timestamp}\n{nonce}\n{request_id}\n{request_body}" - if isinstance(sign_string, str): - sign_string = sign_string.encode('utf-8') - signature = hmac.new( - secret.encode('utf-8'), - sign_string, - hashlib.sha256 - ).hexdigest() - - headers = { - "Content-Type": "application/json", - "X-APPID": appid, - "X-Request-ID": request_id, - "X-Timestamp": timestamp, - "X-Nonce": nonce, - "X-Signature": signature, - } - - print(f"[INFO] Calling ClawBot channel_reset: channel_id={channel_id}") - - try: - import requests as http_requests - resp = http_requests.post( - f"{clawbot_base}/api/v1/wechat/channel_reset", - json={"channel_id": channel_id}, - headers=headers, - timeout=10, - ) - print(f"[INFO] ClawBot channel_reset response: HTTP {resp.status_code}, body={resp.text[:300]}") - except Exception as e: - print(f"[WARN] ClawBot channel_reset API call failed: {e}") - import traceback - traceback.print_exc() - - # 清空本地 channel 的 oauth_state 和 credential - channel.status = "pending" - channel.oauth_state = "" - channel.credential = {} - channel.updated_at = datetime.now() - db.commit() - - return { - "code": 0, - "data": { - "message": "IM通道重置任务已提交", - "channel_id": channel_id, - }, - "message": "通道已重置", - } - - -# ============================================================ -# 统一Webhook端点 - AstrBot借鉴优化方案 P0.2 + P1 -# 所有IM平台消息通过此入口接入,使用平台适配器模式 -# ============================================================ - -@router.post("/webhook/{platform}") -def unified_webhook( - platform: str, - payload: dict = Body(...), - signature: str = Header(None, alias="X-Webhook-Signature"), - timestamp: str = Header(None, alias="X-Webhook-Timestamp"), - db: Session = Depends(get_db), -): - """统一Webhook端点:所有IM平台消息通过此入口接入(使用适配器模式) - - 流程: - 1. 根据platform参数识别平台(使用platform_cls_map注册表) - 2. 查找活跃频道获取配置 - 3. 使用平台适配器验证签名 - 4. 使用平台适配器解析消息为统一格式 - 5. 创建MessageSession和ChannelMessage记录 - 6. 返回标准Webhook响应 - """ - # 使用适配器注册表验证平台 - if platform not in platform_cls_map: - raise HTTPException( - status_code=400, - detail=f"不支持的平台: {platform}. 可用: {list(platform_cls_map.keys())}" - ) - - logger.info(f"[Webhook] 收到 {platform} 平台消息: signature={signature is not None}") - - # URL验证必须在频道存在性检查之前——平台验证URL时与频道是否存在无关 - if platform == "feishu" and payload.get("challenge"): - logger.info(f"[Webhook-FeiShu] URL验证请求") - return {"challenge": payload["challenge"]} - - if platform == "slack" and payload.get("type") == "url_verification": - logger.info(f"[Webhook-Slack] URL验证请求") - return {"challenge": payload.get("challenge", "")} - - # 查找活跃频道获取配置 - channel = db.query(Channel).filter( - Channel.platform == platform, - Channel.is_active == True, - ).first() - - if not channel: - return { - "code": 0, - "data": {"handled": False}, - "message": f"无活跃的{platform}频道", - } - - # 创建平台适配器实例 - try: - adapter = get_platform_adapter(platform, channel.credential or {}) - except Exception as e: - logger.error(f"[Webhook] 创建平台适配器失败: {e}") - raise HTTPException(status_code=500, detail=f"平台适配器初始化失败: {str(e)}") - - # 验证签名 - if signature: - if not adapter.verify_signature(payload, signature, timestamp): - logger.warning(f"[Webhook] {platform} 签名验证失败") - raise HTTPException(status_code=401, detail="Webhook签名验证失败") - - # 解析消息为统一格式 - parsed = adapter.parse_webhook(payload) - - # 创建MessageSession统一会话标识 - session = MessageSession.from_channel_event( - platform=platform, - channel_id=channel.id, - user_id=parsed.get("sender_id", ""), - group_id=parsed.get("group_id"), - ) - - # 创建消息记录 - msg_id = gen_uuid() - channel_msg = ChannelMessage( - id=msg_id, - channel_id=channel.id, - user_id=channel.owner_id, - role="user", - content=parsed.get("content", ""), - token_used=0, - created_at=datetime.now(), - ) - db.add(channel_msg) - - # 更新频道最后活跃时间 - channel.last_active = datetime.now() - channel.updated_at = datetime.now() - - db.commit() - - # 发布到事件总线(Redis队列),异步处理 - try: - from ..event_bus import MessageEvent, get_event_bus - event = MessageEvent( - platform=platform, - channel_id=channel.id, - content=parsed.get("content", ""), - sender_id=parsed.get("sender_id", ""), - sender_name=parsed.get("sender_name", ""), - message_id=str(msg_id), - group_id=parsed.get("group_id"), - content_type=parsed.get("content_type", "text"), - raw=payload, - ) - get_event_bus().publish(event) - logger.info(f"[Webhook] 事件已发布到EventBus: {platform}:{msg_id}") - except Exception as e: - logger.warning(f"[Webhook] EventBus发布失败(不影响消息接收): {e}") - - logger.info( - f"[Webhook] 消息已记录: platform={platform}, " - f"channel={channel.name}, msg_id={msg_id}, session={session}" - ) - - return { - "code": 0, - "data": { - "handled": True, - "message_id": msg_id, - "session": str(session), - "platform": platform, - }, - "message": "消息已接收", - } - - -@router.get("/webhook/platforms") -def list_supported_platforms(): - """列出所有支持的平台(从适配器注册表获取)""" - platforms = [] - for name, meta in platform_registry.items(): - platforms.append({ - "name": meta.name, - "display_name": meta.display_name, - "description": meta.description, - "icon": meta.icon, - "support_streaming": meta.support_streaming, - "support_image": meta.support_image, - "support_file": meta.support_file, - "support_voice": meta.support_voice, - }) - return {"code": 0, "data": {"platforms": platforms}, "message": "ok"} diff --git a/backend/python/app/api/im_bot_user.py b/backend/python/app/api/im_bot_user.py index 1c9a7f0..9453301 100644 --- a/backend/python/app/api/im_bot_user.py +++ b/backend/python/app/api/im_bot_user.py @@ -1,601 +1,601 @@ -""" -Phase 4: 个人微信 (iLink 协议 / 微信 ClawBot 插件) —— 前台用户模块 -im_bot_user.py - -为【平台注册用户】提供"各自绑定自己的个人微信"能力: -- 每个用户独立一条 im_bot_wechat_account 记录(按 user_id 隔离) -- 扫码登录免 AppID;消息收发走 Bearer bot_token + X-WECHAT-UIN -- 绑定后由七虎AI 大脑自动应答(复用 channels 的真实 LLM 调用) - -与后台 im_bot_admin 的区别: -- 后台:平台级 IM Bot 接入中枢(飞书/企微/Telegram 等多平台、路由、统计),其个人微信 - 端点仅限 user_id IS NULL 的"全局 Bot",与用户绑定互不干扰。 -- 前台:每个登录用户管理自己的个人微信,数据严格按 user_id 隔离。 -""" -import base64 -import json -import random -import threading -import time -from datetime import datetime, timedelta -from typing import Optional - -import requests -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy import text -from sqlalchemy.orm import Session - -from ..auth import get_current_user -from ..database import SessionLocal -from ..models import User - -router = APIRouter() - -# ============================================================ -# iLink 协议常量(腾讯官方 iLink Bot HTTP API,直连免 AppID) -# ============================================================ -ILINK_BASE = "https://ilinkai.weixin.qq.com" - -# 每个已连接账号一个后台接收线程,key = 账号表主键 id -_wechat_receiver_threads: dict[int, threading.Thread] = {} - - -# ============================================================ -# 通用 DB 工具 -# ============================================================ -def _execute_raw(db: Session, sql_text, params=None): - result = db.execute(sql_text, params or {}) - if result.returns_rows: - return result.fetchall() - db.commit() - return result - - -def _row_to_dict(row) -> 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 - - -# ============================================================ -# iLink 协议请求工具 -# ============================================================ -def _wechat_headers(bot_token: str) -> dict: - """iLink 协议请求头:Bearer bot_token + 随机 X-WECHAT-UIN 防重放""" - uin = base64.b64encode(str(random.randint(0, 2 ** 32 - 1)).encode("utf-8")).decode("utf-8") - return { - "Content-Type": "application/json", - "Authorization": f"Bearer {bot_token}", - "AuthorizationType": "ilink_bot_token", - "X-WECHAT-UIN": uin, - } - - -def _wechat_row_for_user(db: Session, user_id: str): - rows = _execute_raw( - db, - text("SELECT * FROM im_bot_wechat_account WHERE user_id = :uid ORDER BY id DESC LIMIT 1"), - {"uid": user_id}, - ) - return rows[0] if rows else None - - -def _wechat_mask(row) -> dict: - d = _row_to_dict(row) - if d.get("bot_token"): - bt = d["bot_token"] - d["bot_token"] = ("***" + bt[-6:]) if len(bt) > 6 else "***" - d.pop("appid", None) - d.pop("secret_key", None) - return d - - -# ============================================================ -# 七虎AI 大脑(复用 channels 的真实 LLM 调用) -# ============================================================ -def _recent_history(user_id: str, limit: int = 6) -> str: - """拼接最近若干轮对话,作为多轮上下文。""" - db = None - try: - db = SessionLocal() - rows = _execute_raw(db, text( - "SELECT role, content FROM im_bot_chat_log " - "WHERE user_id=:uid ORDER BY id DESC LIMIT :lim" - ), {"uid": user_id, "lim": limit}) - pairs = [] - for r in reversed(rows): - dd = _row_to_dict(r) - role = "用户" if dd["role"] == "user" else "七虎AI" - pairs.append(f"{role}:{dd['content']}") - return "\n".join(pairs) - except Exception: - return "" - finally: - if db: - db.close() - - -def _qihua_reply(text: str, user_id: str = None) -> str: - """调用七虎AI 生成回复。自行创建 DB 会话,避免上层线程无可用 session。""" - prompt = text - if user_id: - hist = _recent_history(user_id) - if hist: - prompt = ( - "以下是你与该用户最近的对话历史,请结合上下文作答:\n" - f"{hist}\n\n" - f"用户当前问题:{text}" - ) - db = None - try: - from app.api.channels import _call_real_llm - db = SessionLocal() - return _call_real_llm(db, None, prompt, "个人微信") - except Exception as e: - return f"(AI 回复生成失败:{e})" - finally: - if db: - db.close() - - -# ============================================================ -# iLink 消息收发 -# ============================================================ -def _wechat_get_updates(baseurl: str, bot_token: str, cursor: str): - """长轮询收消息,返回 (msgs, new_cursor)。最多 hold 35s。""" - try: - resp = requests.post( - baseurl.rstrip("/") + "/ilink/bot/getupdates", - headers=_wechat_headers(bot_token), - json={"get_updates_buf": cursor, "base_info": {"channel_version": "1.0.2"}}, - timeout=40, - ) - if not resp.ok: - return [], cursor - j = resp.json() if resp.content else {} - return j.get("msgs", []) or [], j.get("get_updates_buf", cursor) - except Exception: - return [], cursor - - -def _wechat_send_message(baseurl: str, bot_token: str, to_user_id: str, context_token: str, text: str): - """发送文本回复(必须带 context_token 关联到正确的对话窗口)。 - 返回 (ok, ret, body)。ret=-14=会话过期需重扫;ret=-2=token 失效。""" - import uuid as _uuid - payload = { - "msg": { - "from_user_id": "", - "to_user_id": to_user_id, - "client_id": "py-" + str(_uuid.uuid4()), - "message_type": 2, - "message_state": 2, - "context_token": context_token, - "item_list": [{"type": 1, "text_item": {"text": text}}], - }, - "base_info": {"channel_version": "1.0.2"}, - } - last_body = {} - for _ in range(2): - try: - resp = requests.post( - baseurl.rstrip("/") + "/ilink/bot/sendmessage", - headers=_wechat_headers(bot_token), - json=payload, - timeout=20, - ) - try: - body = resp.json() if resp.content else {} - except Exception: - body = {} - last_body = body - ret = body.get("ret") if isinstance(body, dict) else None - try: - with open("/tmp/wechat_send.log", "a", encoding="utf-8") as _f: - _f.write(datetime.now().isoformat() + " SEND_HTTP=%s ret=%s body=%s\n" - % (resp.status_code, ret, json.dumps(body, ensure_ascii=False)[:300])) - except Exception: - pass - if resp.ok and (ret is None or ret == 0 or not body): - return True, ret, body - if ret in (-2, -14): - return False, ret, body # 鉴权/会话失效,立即停止重试 - except Exception: - pass - return False, (last_body.get("ret") if isinstance(last_body, dict) else None), last_body - - -def _wechat_extract_text(msg: dict) -> str: - parts = [] - for it in (msg.get("item_list") or []): - if it.get("type") == 1 and it.get("text_item", {}).get("text"): - parts.append(it["text_item"]["text"]) - if not parts: - for k in ("text", "content", "message", "msg"): - v = msg.get(k) - if isinstance(v, str) and v.strip(): - parts.append(v) - return "\n".join(parts) - - -def _wechat_sender_id(m: dict) -> str: - """从入站消息中提取发送者 id(兼容多套字段名)。""" - for k in ("from_user_id", "from_user_name", "from_username", "user_id", - "sender_id", "wxid", "from_wxid", "username"): - v = m.get(k) - if v: - return str(v) - return "" - - -def _wechat_context_token(m: dict) -> str: - """提取 context_token(回复必须回带,否则不关联到对话窗口)。""" - for k in ("context_token", "ctx_token", "conversation_id", "conv_id", - "msg_id", "context_id", "session_id"): - v = m.get(k) - if v: - return str(v) - return "" - - -def _log_chat(user_id, platform, from_user, context_token, role, content): - """持久化 IM Bot 对话历史,便于平台内查阅。""" - db = None - try: - db = SessionLocal() - _execute_raw(db, text( - "INSERT INTO im_bot_chat_log " - "(user_id, platform, from_user, context_token, role, content, created_at) " - "VALUES (:uid, :platform, :fu, :ctx, :role, :content, :created)" - ), { - "uid": user_id, "platform": platform, "fu": from_user, - "ctx": context_token, "role": role, "content": content, - "created": datetime.now(), - }) - except Exception: - pass - finally: - if db: - db.close() - - -# ============================================================ -# 镜像到 AI 聊天(标准 conversations/messages) -# 个人微信对话作为一个独立会话窗(默认名 "WeXin Bot")出现在 AI 聊天中, -# 从而复用 AI 聊天模块的全部功能(滚动翻阅、重命名、置顶、删除等)。 -# ============================================================ -WECHAT_CONV_TITLE = "WeXin Bot" - - -def _get_or_create_wechat_conv(db: Session, user_id: str) -> str: - """按 user_id 找/建 source='wechat_bot' 的会话,返回会话 id。""" - rows = _execute_raw(db, text( - "SELECT id FROM conversations WHERE user_id=:uid AND source='wechat_bot' " - "AND status<>'archived' ORDER BY updated_at DESC LIMIT 1" - ), {"uid": user_id}) - if rows: - return _row_to_dict(rows[0])["id"] - import uuid as _uuid - cid = str(_uuid.uuid4()) - now = datetime.now() - _execute_raw(db, text( - "INSERT INTO conversations " - "(id, user_id, title, status, pinned, knowledge_graph_count, source, created_at, updated_at) " - "VALUES (:id, :uid, :title, 'active', 0, 0, 'wechat_bot', :c, :u)" - ), {"id": cid, "uid": user_id, "title": WECHAT_CONV_TITLE, "c": now, "u": now}) - return cid - - -def _append_ai_message(db: Session, conv_id: str, role: str, content: str, created=None): - import uuid as _uuid - _execute_raw(db, text( - "INSERT INTO messages (id, conversation_id, role, content, token_used, created_at) " - "VALUES (:id, :cid, :role, :content, 0, :c)" - ), { - "id": str(_uuid.uuid4()), "cid": conv_id, "role": role, - "content": content, "c": created or datetime.now(), - }) - - -def _mirror_to_ai_chat(user_id: str, user_content: str, assistant_content: str): - """把一轮微信问答写入用户的 WeXin Bot AI 聊天会话。""" - if not user_id: - return - db = None - try: - db = SessionLocal() - cid = _get_or_create_wechat_conv(db, user_id) - now = datetime.now() - _append_ai_message(db, cid, "user", user_content, created=now) - # assistant 比 user 晚 50ms,保证排序严格递增,避免同秒时 ORDER BY 乱序 - _append_ai_message(db, cid, "assistant", assistant_content, - created=now + timedelta(milliseconds=50)) - _execute_raw(db, text("UPDATE conversations SET updated_at=:u WHERE id=:id"), - {"u": datetime.now(), "id": cid}) - except Exception: - pass - finally: - if db: - db.close() - - -# ============================================================ -# 后台接收线程(每用户独立) -# ============================================================ -def _mark_wechat_expired(account_id: int): - """发送会话失效(ret=-14/-2):标记账号需重扫,并清除失效 token。""" - db = None - try: - db = SessionLocal() - _execute_raw(db, text( - "UPDATE im_bot_wechat_account SET status='expired', bot_token=NULL, " - "updated_at=:ts WHERE id=:aid" - ), {"aid": account_id, "ts": datetime.now()}) - except Exception: - pass - finally: - if db: - db.close() - - -def _wechat_receiver_loop(account_id: int): - """长轮询个人微信消息 → 七虎AI 回复 → 发送。""" - cursor = "" - while True: - db = SessionLocal() - try: - row = _execute_raw( - db, - text("SELECT * FROM im_bot_wechat_account WHERE id = :aid"), - {"aid": account_id}, - ) - if not row: - return - d = _row_to_dict(row[0]) - if d.get("status") != "connected" or not d.get("bot_token"): - return - bot_token = d["bot_token"] - baseurl = d.get("baseurl") or ILINK_BASE - msgs, cursor = _wechat_get_updates(baseurl, bot_token, cursor) - owner_uid = d.get("user_id") - for m in msgs: - if m.get("message_type") == 2: - continue # 仅跳过 bot 自己发出的回声,其余(含后续追问)均尝试处理 - content = _wechat_extract_text(m) - if not content: - continue - sender = _wechat_sender_id(m) - ctx = _wechat_context_token(m) - # 诊断:记录原始入站消息,定位字段名 / 第二轮不回复 - try: - with open("/tmp/wechat_inbound.log", "a", encoding="utf-8") as _f: - _f.write(datetime.now().isoformat() + " sender=" + repr(sender) - + " ctx=" + repr(ctx) + " raw=" + json.dumps(m, ensure_ascii=False) + "\n") - except Exception: - pass - # 先生成回复(此时历史尚未包含本条,避免重复) - reply = _qihua_reply(content, owner_uid) - ok, ret, _ = _wechat_send_message(baseurl, bot_token, sender, ctx, reply) - if ok: - # 仅发送成功才写对话历史(避免“发了但没收到”的误导记录) - if owner_uid: - _log_chat(owner_uid, "personal_wechat", sender, ctx, "user", content) - _log_chat(owner_uid, "personal_wechat", sender, ctx, "assistant", reply) - # 同步镜像到 AI 聊天的 WeXin Bot 会话窗 - _mirror_to_ai_chat(owner_uid, content, reply) - else: - try: - with open("/tmp/wechat_send.log", "a", encoding="utf-8") as _f: - _f.write(datetime.now().isoformat() + " SEND_FAIL ret=%s sender=%s\n" - % (ret, repr(sender))) - except Exception: - pass - if ret in (-2, -14): - # 会话/鉴权失效:标记需重扫并停止接收线程 - _mark_wechat_expired(d["id"]) - return - time.sleep(1) - except Exception: - time.sleep(5) - finally: - db.close() - - -def _start_wechat_receiver(account_id: int): - if account_id in _wechat_receiver_threads and _wechat_receiver_threads[account_id].is_alive(): - return - t = threading.Thread(target=_wechat_receiver_loop, args=(account_id,), daemon=True) - t.start() - _wechat_receiver_threads[account_id] = t - - -# ============================================================ -# 前台用户接口(按 user_id 隔离) -# ============================================================ -@router.get("/status") -async def wechat_status(user: User = Depends(get_current_user)): - """当前登录用户的个人微信连接状态""" - db = SessionLocal() - try: - row = _wechat_row_for_user(db, user.id) - if not row: - return {"success": True, "data": {"connected": False, "configured": False}} - d = _row_to_dict(row) - return {"success": True, "data": { - "connected": d.get("status") == "connected", - "status": d.get("status"), - "configured": True, - "baseurl": d.get("baseurl"), - "ilink_bot_id": d.get("ilink_bot_id"), - "username": d.get("username"), - "connected_at": str(d.get("connected_at")) if d.get("connected_at") else None, - }} - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - finally: - db.close() - - -@router.post("/qrcode") -async def wechat_qrcode(user: User = Depends(get_current_user)): - """获取个人微信扫码登录二维码(iLink 协议,免 AppID)""" - try: - resp = requests.get( - ILINK_BASE + "/ilink/bot/get_bot_qrcode", - params={"bot_type": 3}, - timeout=20, - ) - if not resp.ok: - return {"success": False, "error": f"获取二维码失败 HTTP {resp.status_code}"} - j = resp.json() if resp.content else {} - qr_url = j.get("qrcode_img_content") or "" - qr_img = None - try: - import io as _io - import qrcode as _qrcode - _buf = _io.BytesIO() - _q = _qrcode.QRCode(box_size=10, border=2) - _q.add_data(qr_url) - _q.make(fit=True) - _img = _q.make_image(fill_color="black", back_color="white") - _img.save(_buf, format="PNG") - qr_img = "data:image/png;base64," + base64.b64encode(_buf.getvalue()).decode() - except Exception: - qr_img = None - return {"success": True, "data": { - "qrcode": j.get("qrcode"), - "qrcode_img_content": qr_url, - "qrcode_img": qr_img, - }} - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - - -@router.post("/qrcode/status") -async def wechat_qrcode_status( - qrcode: str = Query(...), - user: User = Depends(get_current_user), -): - """轮询扫码状态;confirmed 时保存该用户的 bot_token 并启动接收线程""" - try: - resp = requests.get( - ILINK_BASE + "/ilink/bot/get_qrcode_status", - params={"qrcode": qrcode}, - timeout=40, - ) - if not resp.ok: - return {"success": False, "error": f"查询状态失败 HTTP {resp.status_code}"} - j = resp.json() if resp.content else {} - data = j.get("data", j) if isinstance(j, dict) else j - status = data.get("status") - if status == "confirmed": - creds = data.get("credentials", {}) or {} - bot_token = data.get("bot_token") or creds.get("bot_token") - baseurl = data.get("baseurl") or creds.get("baseurl") or ILINK_BASE - ilink_bot_id = data.get("ilink_bot_id") or creds.get("ilink_bot_id") - db = SessionLocal() - try: - row = _wechat_row_for_user(db, user.id) - if not row: - _execute_raw(db, text("""INSERT INTO im_bot_wechat_account - (user_id, username, bot_token, ilink_bot_id, baseurl, status, - connected_at, created_at, updated_at) - VALUES (:user_id, :username, :bot_token, :ilink_bot_id, :baseurl, - 'connected', :connected_at, :created_at, :updated_at)"""), - { - "user_id": user.id, "username": user.username, - "bot_token": bot_token, "ilink_bot_id": ilink_bot_id, - "baseurl": baseurl, - "connected_at": datetime.now(), - "created_at": datetime.now(), "updated_at": datetime.now(), - }) - else: - _execute_raw(db, text("""UPDATE im_bot_wechat_account SET - user_id=:user_id, username=:username, bot_token=:bot_token, - ilink_bot_id=:ilink_bot_id, baseurl=:baseurl, - status='connected', connected_at=:connected_at, updated_at=:updated_at - WHERE id=:id"""), - { - "user_id": user.id, "username": user.username, - "bot_token": bot_token, "ilink_bot_id": ilink_bot_id, - "baseurl": baseurl, "connected_at": datetime.now(), - "updated_at": datetime.now(), "id": _row_to_dict(row)["id"], - }) - row = _wechat_row_for_user(db, user.id) - aid = _row_to_dict(row)["id"] if row else 1 - _start_wechat_receiver(aid) - finally: - db.close() - return {"success": True, "data": {"status": "confirmed", "connected": True}} - return {"success": True, "data": {"status": status, "connected": False}} - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - - -@router.post("/disconnect") -async def wechat_disconnect(user: User = Depends(get_current_user)): - """断开当前用户的个人微信连接(清除 bot_token)""" - db = SessionLocal() - try: - row = _wechat_row_for_user(db, user.id) - if row: - _execute_raw(db, text( - "UPDATE im_bot_wechat_account SET bot_token=NULL, status='disconnected', " - "updated_at=:updated_at WHERE id=:id" - ), {"updated_at": datetime.now(), "id": _row_to_dict(row)["id"]}) - return {"success": True, "message": "已断开个人微信连接"} - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - finally: - db.close() - - -# ============================================================ -# 启动恢复:为所有已连接用户账号拉起接收线程 -# ============================================================ -@router.get("/history") -async def wechat_history( - user: User = Depends(get_current_user), - limit: int = Query(200), -): - """查询当前用户通过个人微信与七虎AI 的对话历史""" - db = SessionLocal() - try: - rows = _execute_raw( - db, - text("SELECT id, role, content, platform, from_user, created_at " - "FROM im_bot_chat_log WHERE user_id=:uid ORDER BY id DESC LIMIT :lim"), - {"uid": user.id, "lim": min(int(limit), 500)}, - ) - items = [_row_to_dict(r) for r in rows] - items.reverse() - return {"success": True, "data": {"messages": items, "total": len(items)}} - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - finally: - db.close() - - -def start_all_wechat_receivers(): - """在应用启动时调用,恢复此前已连接用户的个人微信接收线程。""" - db = SessionLocal() - try: - rows = _execute_raw( - db, - text("SELECT id FROM im_bot_wechat_account WHERE status='connected' AND user_id IS NOT NULL"), - ) - for r in rows: - _start_wechat_receiver(r._mapping["id"]) - if rows: - import logging - logging.getLogger("aiflow").info(f"[IM个人微信] 已恢复 {len(rows)} 个用户微信接收线程") - except Exception as e: - import logging - logging.getLogger("aiflow").warning(f"[IM个人微信] 启动恢复失败(不影响服务): {e}") - finally: - db.close() +""" +Phase 4: 个人微信 (iLink 协议 / 微信 ClawBot 插件) —— 前台用户模块 +im_bot_user.py + +为【平台注册用户】提供"各自绑定自己的个人微信"能力: +- 每个用户独立一条 im_bot_wechat_account 记录(按 user_id 隔离) +- 扫码登录免 AppID;消息收发走 Bearer bot_token + X-WECHAT-UIN +- 绑定后由七虎AI 大脑自动应答(复用 channels 的真实 LLM 调用) + +与后台 im_bot_admin 的区别: +- 后台:平台级 IM Bot 接入中枢(飞书/企微/Telegram 等多平台、路由、统计),其个人微信 + 端点仅限 user_id IS NULL 的"全局 Bot",与用户绑定互不干扰。 +- 前台:每个登录用户管理自己的个人微信,数据严格按 user_id 隔离。 +""" +import base64 +import json +import random +import threading +import time +from datetime import datetime, timedelta +from typing import Optional + +import requests +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy import text +from sqlalchemy.orm import Session + +from ..auth import get_current_user +from ..database import SessionLocal +from ..models import User + +router = APIRouter() + +# ============================================================ +# iLink 协议常量(腾讯官方 iLink Bot HTTP API,直连免 AppID) +# ============================================================ +ILINK_BASE = "https://ilinkai.weixin.qq.com" + +# 每个已连接账号一个后台接收线程,key = 账号表主键 id +_wechat_receiver_threads: dict[int, threading.Thread] = {} + + +# ============================================================ +# 通用 DB 工具 +# ============================================================ +def _execute_raw(db: Session, sql_text, params=None): + result = db.execute(sql_text, params or {}) + if result.returns_rows: + return result.fetchall() + db.commit() + return result + + +def _row_to_dict(row) -> 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 + + +# ============================================================ +# iLink 协议请求工具 +# ============================================================ +def _wechat_headers(bot_token: str) -> dict: + """iLink 协议请求头:Bearer bot_token + 随机 X-WECHAT-UIN 防重放""" + uin = base64.b64encode(str(random.randint(0, 2 ** 32 - 1)).encode("utf-8")).decode("utf-8") + return { + "Content-Type": "application/json", + "Authorization": f"Bearer {bot_token}", + "AuthorizationType": "ilink_bot_token", + "X-WECHAT-UIN": uin, + } + + +def _wechat_row_for_user(db: Session, user_id: str): + rows = _execute_raw( + db, + text("SELECT * FROM im_bot_wechat_account WHERE user_id = :uid ORDER BY id DESC LIMIT 1"), + {"uid": user_id}, + ) + return rows[0] if rows else None + + +def _wechat_mask(row) -> dict: + d = _row_to_dict(row) + if d.get("bot_token"): + bt = d["bot_token"] + d["bot_token"] = ("***" + bt[-6:]) if len(bt) > 6 else "***" + d.pop("appid", None) + d.pop("secret_key", None) + return d + + +# ============================================================ +# 七虎AI 大脑(复用 channels 的真实 LLM 调用) +# ============================================================ +def _recent_history(user_id: str, limit: int = 6) -> str: + """拼接最近若干轮对话,作为多轮上下文。""" + db = None + try: + db = SessionLocal() + rows = _execute_raw(db, text( + "SELECT role, content FROM im_bot_chat_log " + "WHERE user_id=:uid ORDER BY id DESC LIMIT :lim" + ), {"uid": user_id, "lim": limit}) + pairs = [] + for r in reversed(rows): + dd = _row_to_dict(r) + role = "用户" if dd["role"] == "user" else "七虎AI" + pairs.append(f"{role}:{dd['content']}") + return "\n".join(pairs) + except Exception: + return "" + finally: + if db: + db.close() + + +def _qihua_reply(text: str, user_id: str = None) -> str: + """调用七虎AI 生成回复。自行创建 DB 会话,避免上层线程无可用 session。""" + prompt = text + if user_id: + hist = _recent_history(user_id) + if hist: + prompt = ( + "以下是你与该用户最近的对话历史,请结合上下文作答:\n" + f"{hist}\n\n" + f"用户当前问题:{text}" + ) + db = None + try: + from app.api.im_llm import _call_real_llm + db = SessionLocal() + return _call_real_llm(db, None, prompt, "个人微信") + except Exception as e: + return f"(AI 回复生成失败:{e})" + finally: + if db: + db.close() + + +# ============================================================ +# iLink 消息收发 +# ============================================================ +def _wechat_get_updates(baseurl: str, bot_token: str, cursor: str): + """长轮询收消息,返回 (msgs, new_cursor)。最多 hold 35s。""" + try: + resp = requests.post( + baseurl.rstrip("/") + "/ilink/bot/getupdates", + headers=_wechat_headers(bot_token), + json={"get_updates_buf": cursor, "base_info": {"channel_version": "1.0.2"}}, + timeout=40, + ) + if not resp.ok: + return [], cursor + j = resp.json() if resp.content else {} + return j.get("msgs", []) or [], j.get("get_updates_buf", cursor) + except Exception: + return [], cursor + + +def _wechat_send_message(baseurl: str, bot_token: str, to_user_id: str, context_token: str, text: str): + """发送文本回复(必须带 context_token 关联到正确的对话窗口)。 + 返回 (ok, ret, body)。ret=-14=会话过期需重扫;ret=-2=token 失效。""" + import uuid as _uuid + payload = { + "msg": { + "from_user_id": "", + "to_user_id": to_user_id, + "client_id": "py-" + str(_uuid.uuid4()), + "message_type": 2, + "message_state": 2, + "context_token": context_token, + "item_list": [{"type": 1, "text_item": {"text": text}}], + }, + "base_info": {"channel_version": "1.0.2"}, + } + last_body = {} + for _ in range(2): + try: + resp = requests.post( + baseurl.rstrip("/") + "/ilink/bot/sendmessage", + headers=_wechat_headers(bot_token), + json=payload, + timeout=20, + ) + try: + body = resp.json() if resp.content else {} + except Exception: + body = {} + last_body = body + ret = body.get("ret") if isinstance(body, dict) else None + try: + with open("/tmp/wechat_send.log", "a", encoding="utf-8") as _f: + _f.write(datetime.now().isoformat() + " SEND_HTTP=%s ret=%s body=%s\n" + % (resp.status_code, ret, json.dumps(body, ensure_ascii=False)[:300])) + except Exception: + pass + if resp.ok and (ret is None or ret == 0 or not body): + return True, ret, body + if ret in (-2, -14): + return False, ret, body # 鉴权/会话失效,立即停止重试 + except Exception: + pass + return False, (last_body.get("ret") if isinstance(last_body, dict) else None), last_body + + +def _wechat_extract_text(msg: dict) -> str: + parts = [] + for it in (msg.get("item_list") or []): + if it.get("type") == 1 and it.get("text_item", {}).get("text"): + parts.append(it["text_item"]["text"]) + if not parts: + for k in ("text", "content", "message", "msg"): + v = msg.get(k) + if isinstance(v, str) and v.strip(): + parts.append(v) + return "\n".join(parts) + + +def _wechat_sender_id(m: dict) -> str: + """从入站消息中提取发送者 id(兼容多套字段名)。""" + for k in ("from_user_id", "from_user_name", "from_username", "user_id", + "sender_id", "wxid", "from_wxid", "username"): + v = m.get(k) + if v: + return str(v) + return "" + + +def _wechat_context_token(m: dict) -> str: + """提取 context_token(回复必须回带,否则不关联到对话窗口)。""" + for k in ("context_token", "ctx_token", "conversation_id", "conv_id", + "msg_id", "context_id", "session_id"): + v = m.get(k) + if v: + return str(v) + return "" + + +def _log_chat(user_id, platform, from_user, context_token, role, content): + """持久化 IM Bot 对话历史,便于平台内查阅。""" + db = None + try: + db = SessionLocal() + _execute_raw(db, text( + "INSERT INTO im_bot_chat_log " + "(user_id, platform, from_user, context_token, role, content, created_at) " + "VALUES (:uid, :platform, :fu, :ctx, :role, :content, :created)" + ), { + "uid": user_id, "platform": platform, "fu": from_user, + "ctx": context_token, "role": role, "content": content, + "created": datetime.now(), + }) + except Exception: + pass + finally: + if db: + db.close() + + +# ============================================================ +# 镜像到 AI 聊天(标准 conversations/messages) +# 个人微信对话作为一个独立会话窗(默认名 "WeXin Bot")出现在 AI 聊天中, +# 从而复用 AI 聊天模块的全部功能(滚动翻阅、重命名、置顶、删除等)。 +# ============================================================ +WECHAT_CONV_TITLE = "WeXin Bot" + + +def _get_or_create_wechat_conv(db: Session, user_id: str) -> str: + """按 user_id 找/建 source='wechat_bot' 的会话,返回会话 id。""" + rows = _execute_raw(db, text( + "SELECT id FROM conversations WHERE user_id=:uid AND source='wechat_bot' " + "AND status<>'archived' ORDER BY updated_at DESC LIMIT 1" + ), {"uid": user_id}) + if rows: + return _row_to_dict(rows[0])["id"] + import uuid as _uuid + cid = str(_uuid.uuid4()) + now = datetime.now() + _execute_raw(db, text( + "INSERT INTO conversations " + "(id, user_id, title, status, pinned, knowledge_graph_count, source, created_at, updated_at) " + "VALUES (:id, :uid, :title, 'active', 0, 0, 'wechat_bot', :c, :u)" + ), {"id": cid, "uid": user_id, "title": WECHAT_CONV_TITLE, "c": now, "u": now}) + return cid + + +def _append_ai_message(db: Session, conv_id: str, role: str, content: str, created=None): + import uuid as _uuid + _execute_raw(db, text( + "INSERT INTO messages (id, conversation_id, role, content, token_used, created_at) " + "VALUES (:id, :cid, :role, :content, 0, :c)" + ), { + "id": str(_uuid.uuid4()), "cid": conv_id, "role": role, + "content": content, "c": created or datetime.now(), + }) + + +def _mirror_to_ai_chat(user_id: str, user_content: str, assistant_content: str): + """把一轮微信问答写入用户的 WeXin Bot AI 聊天会话。""" + if not user_id: + return + db = None + try: + db = SessionLocal() + cid = _get_or_create_wechat_conv(db, user_id) + now = datetime.now() + _append_ai_message(db, cid, "user", user_content, created=now) + # assistant 比 user 晚 50ms,保证排序严格递增,避免同秒时 ORDER BY 乱序 + _append_ai_message(db, cid, "assistant", assistant_content, + created=now + timedelta(milliseconds=50)) + _execute_raw(db, text("UPDATE conversations SET updated_at=:u WHERE id=:id"), + {"u": datetime.now(), "id": cid}) + except Exception: + pass + finally: + if db: + db.close() + + +# ============================================================ +# 后台接收线程(每用户独立) +# ============================================================ +def _mark_wechat_expired(account_id: int): + """发送会话失效(ret=-14/-2):标记账号需重扫,并清除失效 token。""" + db = None + try: + db = SessionLocal() + _execute_raw(db, text( + "UPDATE im_bot_wechat_account SET status='expired', bot_token=NULL, " + "updated_at=:ts WHERE id=:aid" + ), {"aid": account_id, "ts": datetime.now()}) + except Exception: + pass + finally: + if db: + db.close() + + +def _wechat_receiver_loop(account_id: int): + """长轮询个人微信消息 → 七虎AI 回复 → 发送。""" + cursor = "" + while True: + db = SessionLocal() + try: + row = _execute_raw( + db, + text("SELECT * FROM im_bot_wechat_account WHERE id = :aid"), + {"aid": account_id}, + ) + if not row: + return + d = _row_to_dict(row[0]) + if d.get("status") != "connected" or not d.get("bot_token"): + return + bot_token = d["bot_token"] + baseurl = d.get("baseurl") or ILINK_BASE + msgs, cursor = _wechat_get_updates(baseurl, bot_token, cursor) + owner_uid = d.get("user_id") + for m in msgs: + if m.get("message_type") == 2: + continue # 仅跳过 bot 自己发出的回声,其余(含后续追问)均尝试处理 + content = _wechat_extract_text(m) + if not content: + continue + sender = _wechat_sender_id(m) + ctx = _wechat_context_token(m) + # 诊断:记录原始入站消息,定位字段名 / 第二轮不回复 + try: + with open("/tmp/wechat_inbound.log", "a", encoding="utf-8") as _f: + _f.write(datetime.now().isoformat() + " sender=" + repr(sender) + + " ctx=" + repr(ctx) + " raw=" + json.dumps(m, ensure_ascii=False) + "\n") + except Exception: + pass + # 先生成回复(此时历史尚未包含本条,避免重复) + reply = _qihua_reply(content, owner_uid) + ok, ret, _ = _wechat_send_message(baseurl, bot_token, sender, ctx, reply) + if ok: + # 仅发送成功才写对话历史(避免“发了但没收到”的误导记录) + if owner_uid: + _log_chat(owner_uid, "personal_wechat", sender, ctx, "user", content) + _log_chat(owner_uid, "personal_wechat", sender, ctx, "assistant", reply) + # 同步镜像到 AI 聊天的 WeXin Bot 会话窗 + _mirror_to_ai_chat(owner_uid, content, reply) + else: + try: + with open("/tmp/wechat_send.log", "a", encoding="utf-8") as _f: + _f.write(datetime.now().isoformat() + " SEND_FAIL ret=%s sender=%s\n" + % (ret, repr(sender))) + except Exception: + pass + if ret in (-2, -14): + # 会话/鉴权失效:标记需重扫并停止接收线程 + _mark_wechat_expired(d["id"]) + return + time.sleep(1) + except Exception: + time.sleep(5) + finally: + db.close() + + +def _start_wechat_receiver(account_id: int): + if account_id in _wechat_receiver_threads and _wechat_receiver_threads[account_id].is_alive(): + return + t = threading.Thread(target=_wechat_receiver_loop, args=(account_id,), daemon=True) + t.start() + _wechat_receiver_threads[account_id] = t + + +# ============================================================ +# 前台用户接口(按 user_id 隔离) +# ============================================================ +@router.get("/status") +async def wechat_status(user: User = Depends(get_current_user)): + """当前登录用户的个人微信连接状态""" + db = SessionLocal() + try: + row = _wechat_row_for_user(db, user.id) + if not row: + return {"success": True, "data": {"connected": False, "configured": False}} + d = _row_to_dict(row) + return {"success": True, "data": { + "connected": d.get("status") == "connected", + "status": d.get("status"), + "configured": True, + "baseurl": d.get("baseurl"), + "ilink_bot_id": d.get("ilink_bot_id"), + "username": d.get("username"), + "connected_at": str(d.get("connected_at")) if d.get("connected_at") else None, + }} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + finally: + db.close() + + +@router.post("/qrcode") +async def wechat_qrcode(user: User = Depends(get_current_user)): + """获取个人微信扫码登录二维码(iLink 协议,免 AppID)""" + try: + resp = requests.get( + ILINK_BASE + "/ilink/bot/get_bot_qrcode", + params={"bot_type": 3}, + timeout=20, + ) + if not resp.ok: + return {"success": False, "error": f"获取二维码失败 HTTP {resp.status_code}"} + j = resp.json() if resp.content else {} + qr_url = j.get("qrcode_img_content") or "" + qr_img = None + try: + import io as _io + import qrcode as _qrcode + _buf = _io.BytesIO() + _q = _qrcode.QRCode(box_size=10, border=2) + _q.add_data(qr_url) + _q.make(fit=True) + _img = _q.make_image(fill_color="black", back_color="white") + _img.save(_buf, format="PNG") + qr_img = "data:image/png;base64," + base64.b64encode(_buf.getvalue()).decode() + except Exception: + qr_img = None + return {"success": True, "data": { + "qrcode": j.get("qrcode"), + "qrcode_img_content": qr_url, + "qrcode_img": qr_img, + }} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/qrcode/status") +async def wechat_qrcode_status( + qrcode: str = Query(...), + user: User = Depends(get_current_user), +): + """轮询扫码状态;confirmed 时保存该用户的 bot_token 并启动接收线程""" + try: + resp = requests.get( + ILINK_BASE + "/ilink/bot/get_qrcode_status", + params={"qrcode": qrcode}, + timeout=40, + ) + if not resp.ok: + return {"success": False, "error": f"查询状态失败 HTTP {resp.status_code}"} + j = resp.json() if resp.content else {} + data = j.get("data", j) if isinstance(j, dict) else j + status = data.get("status") + if status == "confirmed": + creds = data.get("credentials", {}) or {} + bot_token = data.get("bot_token") or creds.get("bot_token") + baseurl = data.get("baseurl") or creds.get("baseurl") or ILINK_BASE + ilink_bot_id = data.get("ilink_bot_id") or creds.get("ilink_bot_id") + db = SessionLocal() + try: + row = _wechat_row_for_user(db, user.id) + if not row: + _execute_raw(db, text("""INSERT INTO im_bot_wechat_account + (user_id, username, bot_token, ilink_bot_id, baseurl, status, + connected_at, created_at, updated_at) + VALUES (:user_id, :username, :bot_token, :ilink_bot_id, :baseurl, + 'connected', :connected_at, :created_at, :updated_at)"""), + { + "user_id": user.id, "username": user.username, + "bot_token": bot_token, "ilink_bot_id": ilink_bot_id, + "baseurl": baseurl, + "connected_at": datetime.now(), + "created_at": datetime.now(), "updated_at": datetime.now(), + }) + else: + _execute_raw(db, text("""UPDATE im_bot_wechat_account SET + user_id=:user_id, username=:username, bot_token=:bot_token, + ilink_bot_id=:ilink_bot_id, baseurl=:baseurl, + status='connected', connected_at=:connected_at, updated_at=:updated_at + WHERE id=:id"""), + { + "user_id": user.id, "username": user.username, + "bot_token": bot_token, "ilink_bot_id": ilink_bot_id, + "baseurl": baseurl, "connected_at": datetime.now(), + "updated_at": datetime.now(), "id": _row_to_dict(row)["id"], + }) + row = _wechat_row_for_user(db, user.id) + aid = _row_to_dict(row)["id"] if row else 1 + _start_wechat_receiver(aid) + finally: + db.close() + return {"success": True, "data": {"status": "confirmed", "connected": True}} + return {"success": True, "data": {"status": status, "connected": False}} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/disconnect") +async def wechat_disconnect(user: User = Depends(get_current_user)): + """断开当前用户的个人微信连接(清除 bot_token)""" + db = SessionLocal() + try: + row = _wechat_row_for_user(db, user.id) + if row: + _execute_raw(db, text( + "UPDATE im_bot_wechat_account SET bot_token=NULL, status='disconnected', " + "updated_at=:updated_at WHERE id=:id" + ), {"updated_at": datetime.now(), "id": _row_to_dict(row)["id"]}) + return {"success": True, "message": "已断开个人微信连接"} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + finally: + db.close() + + +# ============================================================ +# 启动恢复:为所有已连接用户账号拉起接收线程 +# ============================================================ +@router.get("/history") +async def wechat_history( + user: User = Depends(get_current_user), + limit: int = Query(200), +): + """查询当前用户通过个人微信与七虎AI 的对话历史""" + db = SessionLocal() + try: + rows = _execute_raw( + db, + text("SELECT id, role, content, platform, from_user, created_at " + "FROM im_bot_chat_log WHERE user_id=:uid ORDER BY id DESC LIMIT :lim"), + {"uid": user.id, "lim": min(int(limit), 500)}, + ) + items = [_row_to_dict(r) for r in rows] + items.reverse() + return {"success": True, "data": {"messages": items, "total": len(items)}} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + finally: + db.close() + + +def start_all_wechat_receivers(): + """在应用启动时调用,恢复此前已连接用户的个人微信接收线程。""" + db = SessionLocal() + try: + rows = _execute_raw( + db, + text("SELECT id FROM im_bot_wechat_account WHERE status='connected' AND user_id IS NOT NULL"), + ) + for r in rows: + _start_wechat_receiver(r._mapping["id"]) + if rows: + import logging + logging.getLogger("aiflow").info(f"[IM个人微信] 已恢复 {len(rows)} 个用户微信接收线程") + except Exception as e: + import logging + logging.getLogger("aiflow").warning(f"[IM个人微信] 启动恢复失败(不影响服务): {e}") + finally: + db.close() diff --git a/backend/python/app/api/im_llm.py b/backend/python/app/api/im_llm.py new file mode 100644 index 0000000..7cd0f9e --- /dev/null +++ b/backend/python/app/api/im_llm.py @@ -0,0 +1,45 @@ +""" +IM Bot 真实 LLM 调用。 + +原实现位于 channels.py(通用频道管理模块),该模块已移除。 +为保持 IM Bot(微信机器人等)正常生成回复,将 _call_real_llm 迁移至此独立模块。 +""" +from typing import Optional + +import httpx +from sqlalchemy.orm import Session + +from ..capability_registry import resolve_auto_model +from ..models import AIModel, ModelProvider + + +def _call_real_llm(db: Session, user=None, prompt: str = "", platform: str = "im") -> str: + """调用七虎AI 默认文本模型生成回复,供 IM Bot 复用。""" + try: + ai_model = resolve_auto_model("text", db) + if not ai_model: + return "(未配置默认文本模型,无法生成回复)" + provider = db.query(ModelProvider).filter(ModelProvider.id == ai_model.provider_id).first() + api_key = ai_model.api_key or (provider.api_key if provider else None) + api_url = ai_model.endpoint + if not api_url and provider and provider.api_base_url: + api_url = f"{provider.api_base_url.rstrip('/')}/v1/chat/completions" + if not (api_key and api_url): + return "(模型未配置 API Key 或地址,无法生成回复)" + messages = [ + {"role": "system", "content": "你是七虎AI 助手,负责在即时通讯中回复用户消息。"}, + {"role": "user", "content": prompt}, + ] + request_body = {"model": ai_model.name, "messages": messages, "max_tokens": 2048} + with httpx.Client(timeout=ai_model.timeout or 120) as client: + resp = client.post( + api_url, + headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}, + json=request_body, + ) + if resp.status_code != 200: + return f"(AI 模型调用失败 HTTP {resp.status_code})" + data = resp.json() + return data.get("choices", [{}])[0].get("message", {}).get("content", "") + except Exception as e: + return f"(AI 回复生成失败:{e})" diff --git a/backend/python/main.py b/backend/python/main.py index 6a86aa3..e7839a7 100644 --- a/backend/python/main.py +++ b/backend/python/main.py @@ -1,245 +1,244 @@ -""" -Qihu-AIFlow FastAPI 主应用 -完整后端API,监听18080端口,所有接口以 /app 为前缀 -""" -import sys -import os -import time -import uuid -import logging -from contextlib import asynccontextmanager -from fastapi import FastAPI, Request -from fastapi.middleware.cors import CORSMiddleware -from starlette.middleware.base import BaseHTTPMiddleware -from datetime import datetime - -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.casdoor_auth import router as casdoor_router -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, promotion_admin, achievement_admin, tenant_admin, billing_admin, analytics_admin -from app.api.im_bot_user import router as im_bot_user_router -from app.api.portal import user_portal -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, achievement_admin -from app.config import setup_file_logging, SLOW_REQUEST_THRESHOLD -from app.database import SessionLocal -from app.models import OperationLog as OperationLogModel, TokenUsageLog as TokenUsageLogModel - -# 导入平台适配器触发注册(AstrBot借鉴优化方案 P1) -import app.platform_adapters # noqa: F401 - 触发所有平台适配器装饰器注册 - -# 初始化文件日志(按天轮转) -access_logger = setup_file_logging() -logger = logging.getLogger("aiflow") - - -class APITrackingMiddleware(BaseHTTPMiddleware): - """API调用统计中间件 - 记录请求耗时,标记慢请求""" - - async def dispatch(self, request: Request, call_next): - request_id = str(uuid.uuid4()) - start_time = time.time() - path = request.url.path - method = request.method - - # 跳过健康检查和静态资源 - skip_paths = ["/app/api/health", "/favicon.ico"] - should_skip = any(path.startswith(p) for p in skip_paths) - - try: - response = await call_next(request) - elapsed = time.time() - start_time - status_code = response.status_code - - if not should_skip: - access_logger.info( - f"[{request_id}] {method} {path} -> {status_code} ({elapsed:.3f}s)" - ) - - # 异步记录API调用统计 - try: - db = SessionLocal() - try: - _record_api_call(db, method, path, status_code, elapsed) - finally: - db.close() - except Exception as e: - logger.warning(f"API统计记录失败: {e}") - - return response - except Exception as e: - elapsed = time.time() - start_time - if not should_skip: - access_logger.info( - f"[{request_id}] {method} {path} -> ERROR ({elapsed:.3f}s): {str(e)}" - ) - raise - - async def __call__(self, scope, receive, send): - if scope["type"] != "http": - await self.app(scope, receive, send) - return - await super().__call__(scope, receive, send) - - -def _record_api_call(db, method: str, path: str, status_code: int, elapsed: float): - """记录API调用到数据库统计表""" - # 简化路径以减少基数(去掉动态ID) - parts = path.split("/") - simplified_parts = [] - for part in parts: - # 将UUID样式的路径段替换为{id} - if len(part) == 36 and part.count("-") == 4: - simplified_parts.append("{id}") - # 将纯数字路径段替换为{id} - elif part.isdigit(): - simplified_parts.append("{id}") - else: - simplified_parts.append(part) - simplified_path = "/".join(simplified_parts) - - # 统计记录通过数据库实现,简化版记录到日志 - is_error = status_code >= 400 - is_slow = elapsed >= SLOW_REQUEST_THRESHOLD - - if is_slow: - logger.warning( - f"慢请求: {method} {simplified_path} -> {status_code} ({elapsed:.3f}s, 阈值: {SLOW_REQUEST_THRESHOLD}s)" - ) - - -@asynccontextmanager -async def lifespan(app: FastAPI): - logger.info("Qihu-AIFlow v2.0.0 启动中...") - - # 启动后台 Evolution Pipeline(知识库图谱已存在的用户配置自动恢复运行) - try: - from app.services.evolution_service import EvolutionWatcherManager - from app.models import UserEvolutionConfig - db = SessionLocal() - try: - configs = db.query(UserEvolutionConfig).filter( - UserEvolutionConfig.watcher_status == "running" - ).all() - if configs: - watcher = EvolutionWatcherManager() - for cfg in configs: - try: - watcher.start_user_evolution(cfg.user_id, lambda: SessionLocal()) - logger.info(f"[Evolution] 恢复用户 {cfg.user_id} 的进化Pipeline") - except Exception as e: - logger.warning(f"[Evolution] 恢复用户 {cfg.user_id} 失败: {e}") - logger.info(f"[Evolution] 已恢复 {len(configs)} 个用户的进化Pipeline") - else: - logger.info("[Evolution] 无待恢复的进化配置") - finally: - db.close() - except Exception as e: - logger.warning(f"[Evolution] Pipeline自动启动异常(不影响正常服务): {e}") - - # 启动Redis事件总线 - try: - from app.event_bus import get_event_bus - event_bus = get_event_bus() - event_bus.start(num_consumers=2) - logger.info("[EventBus] Redis事件总线已启动") - except Exception as e: - logger.warning(f"[EventBus] 启动失败(不影响正常服务): {e}") - - logger.info("Qihu-AIFlow v2.0.0 启动完成") - - yield - - # 停止Redis事件总线 - try: - from app.event_bus import get_event_bus - get_event_bus().stop() - logger.info("[EventBus] Redis事件总线已停止") - except Exception: - pass - - # 优雅关闭:停止所有 Evolution Pipeline - try: - from app.services.evolution_service import EvolutionWatcherManager - EvolutionWatcherManager().shutdown_all() - logger.info("[Evolution] 所有Pipeline已停止") - except Exception: - pass - - logger.info("Qihu-AIFlow 正在关闭...") - - -app = FastAPI( - title="Qihu-AIFlow - 智能自动化PaaS平台", - version="1.0.0", - lifespan=lifespan, -) - -# API调用追踪中间件 -app.add_middleware(APITrackingMiddleware) - -# CORS配置 -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# 前台 API -app.include_router(auth.router, prefix="/app/api/auth", tags=["认证"]) -app.include_router(chat.router, prefix="/app/api/chat", tags=["AI聊天"]) -app.include_router(channels.router, prefix="/app/api/channels", tags=["频道"]) -app.include_router(knowledge.router, prefix="/app/api/knowledge", tags=["知识库"]) -app.include_router(market.router, prefix="/app/api/market", tags=["应用市场"]) -app.include_router(token_usage.router, prefix="/app/api/token", tags=["Token驾驶舱"]) -app.include_router(payment.router, prefix="/app/api/payment", tags=["付费中心"]) -app.include_router(profile.router, prefix="/app/api/profile", tags=["个人中心"]) -app.include_router(notifications.router, prefix="/app/api/notifications", tags=["消息通知"]) -app.include_router(assets.router, prefix="/app/api/assets", tags=["资产管理"]) -app.include_router(evolution.router, prefix="/app/api/evolution", tags=["AI自进化"]) - -# 后台 API -app.include_router(dashboard.router, prefix="/app/api/admin/dashboard", tags=["后台-仪表盘"]) -app.include_router(users.router, prefix="/app/api/admin/users", tags=["后台-用户管理"]) -app.include_router(models.router, prefix="/app/api/admin/models", tags=["后台-模型管理"]) -app.include_router(settings.router, prefix="/app/api/admin/settings", tags=["后台-网站设置"]) -app.include_router(payment_admin.router, prefix="/app/api/admin/payment", tags=["后台-支付配置"]) -app.include_router(services.router, prefix="/app/api/admin/services", tags=["后台-增值服务"]) -app.include_router(membership_admin.router, prefix="/app/api/admin/membership", tags=["后台-会员管理"]) -app.include_router(ads.router, prefix="/app/api/admin/ads", tags=["后台-广告管理"]) -app.include_router(market_admin.router, prefix="/app/api/admin/market", tags=["后台-应用审核"]) -app.include_router(logs.router, prefix="/app/api/admin/logs", tags=["后台-日志风控"]) - -app.include_router(skills_admin.router, prefix="/app/api/admin/skills", tags=["后台-技能管理"]) -app.include_router(agents_admin.router, prefix="/app/api/admin/agents", tags=["后台-Agent管理"]) -app.include_router(knowledge_admin.router, prefix="/app/api/admin/knowledge", tags=["后台-知识库管理"]) -app.include_router(workflow_admin.router, prefix="/app/api/admin/workflows", tags=["后台-工作流编排"]) -app.include_router(asset_admin.router, prefix="/app/api/admin/assets", tags=["后台-资产管理"]) -app.include_router(storage_admin.router, prefix="/app/api/admin/storage", tags=["后台-存储配置"]) -app.include_router(automation_admin.router, prefix="/app/api/admin/automation", tags=["后台-自动化任务"]) -app.include_router(mcp_admin.router, prefix="/app/api/admin/mcp", tags=["后台-MCP服务"]) -app.include_router(developer_admin.router, prefix="/app/api/admin/developer", tags=["后台-开发者API"]) -app.include_router(developer_tools_router, prefix="/app/api/admin/developer-tools", tags=["dev-tools"]) -app.include_router(apps_admin.router, prefix="/app/api/admin/apps", tags=["admin-ai-apps"]) -app.include_router(mcp_market_router, prefix="/app/api/admin/mcp-market", tags=["mcp-market"]) -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"]) -app.include_router(im_bot_user_router, prefix="/app/api/im/wechat", tags=["im-wechat-user"]) -app.include_router(promotion_admin.router, prefix="/app/api/admin/promotion", tags=["promotion"]) -app.include_router(tenant_admin.router, prefix="/app/api/admin/tenant", tags=["tenant"]) -app.include_router(billing_admin.router, prefix="/app/api/admin/billing", tags=["billing"]) -app.include_router(analytics_admin.router, prefix="/app/api/admin/analytics", tags=["analytics"]) -app.include_router(user_portal.router, prefix="/app/api/portal", tags=["portal"]) -app.include_router(achievement_admin.router, prefix="/app/api/admin/achievement", tags=["achievement"]) -app.include_router(casdoor_router, prefix="/app/api/auth", tags=["Casdoor SSO"]) - - -@app.get("/app/api/health") -async def health_check(): - return {"status": "ok", "service": "Qihu-AIFlow"} +""" +Qihu-AIFlow FastAPI 主应用 +完整后端API,监听18080端口,所有接口以 /app 为前缀 +""" +import sys +import os +import time +import uuid +import logging +from contextlib import asynccontextmanager +from fastapi import FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware +from starlette.middleware.base import BaseHTTPMiddleware +from datetime import datetime + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from app.api import auth, chat, knowledge, market, token_usage, payment, profile, notifications, assets, evolution +from app.casdoor_auth import router as casdoor_router +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, promotion_admin, achievement_admin, tenant_admin, billing_admin, analytics_admin +from app.api.im_bot_user import router as im_bot_user_router +from app.api.portal import user_portal +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, achievement_admin +from app.config import setup_file_logging, SLOW_REQUEST_THRESHOLD +from app.database import SessionLocal +from app.models import OperationLog as OperationLogModel, TokenUsageLog as TokenUsageLogModel + +# 导入平台适配器触发注册(AstrBot借鉴优化方案 P1) +import app.platform_adapters # noqa: F401 - 触发所有平台适配器装饰器注册 + +# 初始化文件日志(按天轮转) +access_logger = setup_file_logging() +logger = logging.getLogger("aiflow") + + +class APITrackingMiddleware(BaseHTTPMiddleware): + """API调用统计中间件 - 记录请求耗时,标记慢请求""" + + async def dispatch(self, request: Request, call_next): + request_id = str(uuid.uuid4()) + start_time = time.time() + path = request.url.path + method = request.method + + # 跳过健康检查和静态资源 + skip_paths = ["/app/api/health", "/favicon.ico"] + should_skip = any(path.startswith(p) for p in skip_paths) + + try: + response = await call_next(request) + elapsed = time.time() - start_time + status_code = response.status_code + + if not should_skip: + access_logger.info( + f"[{request_id}] {method} {path} -> {status_code} ({elapsed:.3f}s)" + ) + + # 异步记录API调用统计 + try: + db = SessionLocal() + try: + _record_api_call(db, method, path, status_code, elapsed) + finally: + db.close() + except Exception as e: + logger.warning(f"API统计记录失败: {e}") + + return response + except Exception as e: + elapsed = time.time() - start_time + if not should_skip: + access_logger.info( + f"[{request_id}] {method} {path} -> ERROR ({elapsed:.3f}s): {str(e)}" + ) + raise + + async def __call__(self, scope, receive, send): + if scope["type"] != "http": + await self.app(scope, receive, send) + return + await super().__call__(scope, receive, send) + + +def _record_api_call(db, method: str, path: str, status_code: int, elapsed: float): + """记录API调用到数据库统计表""" + # 简化路径以减少基数(去掉动态ID) + parts = path.split("/") + simplified_parts = [] + for part in parts: + # 将UUID样式的路径段替换为{id} + if len(part) == 36 and part.count("-") == 4: + simplified_parts.append("{id}") + # 将纯数字路径段替换为{id} + elif part.isdigit(): + simplified_parts.append("{id}") + else: + simplified_parts.append(part) + simplified_path = "/".join(simplified_parts) + + # 统计记录通过数据库实现,简化版记录到日志 + is_error = status_code >= 400 + is_slow = elapsed >= SLOW_REQUEST_THRESHOLD + + if is_slow: + logger.warning( + f"慢请求: {method} {simplified_path} -> {status_code} ({elapsed:.3f}s, 阈值: {SLOW_REQUEST_THRESHOLD}s)" + ) + + +@asynccontextmanager +async def lifespan(app: FastAPI): + logger.info("Qihu-AIFlow v2.0.0 启动中...") + + # 启动后台 Evolution Pipeline(知识库图谱已存在的用户配置自动恢复运行) + try: + from app.services.evolution_service import EvolutionWatcherManager + from app.models import UserEvolutionConfig + db = SessionLocal() + try: + configs = db.query(UserEvolutionConfig).filter( + UserEvolutionConfig.watcher_status == "running" + ).all() + if configs: + watcher = EvolutionWatcherManager() + for cfg in configs: + try: + watcher.start_user_evolution(cfg.user_id, lambda: SessionLocal()) + logger.info(f"[Evolution] 恢复用户 {cfg.user_id} 的进化Pipeline") + except Exception as e: + logger.warning(f"[Evolution] 恢复用户 {cfg.user_id} 失败: {e}") + logger.info(f"[Evolution] 已恢复 {len(configs)} 个用户的进化Pipeline") + else: + logger.info("[Evolution] 无待恢复的进化配置") + finally: + db.close() + except Exception as e: + logger.warning(f"[Evolution] Pipeline自动启动异常(不影响正常服务): {e}") + + # 启动Redis事件总线 + try: + from app.event_bus import get_event_bus + event_bus = get_event_bus() + event_bus.start(num_consumers=2) + logger.info("[EventBus] Redis事件总线已启动") + except Exception as e: + logger.warning(f"[EventBus] 启动失败(不影响正常服务): {e}") + + logger.info("Qihu-AIFlow v2.0.0 启动完成") + + yield + + # 停止Redis事件总线 + try: + from app.event_bus import get_event_bus + get_event_bus().stop() + logger.info("[EventBus] Redis事件总线已停止") + except Exception: + pass + + # 优雅关闭:停止所有 Evolution Pipeline + try: + from app.services.evolution_service import EvolutionWatcherManager + EvolutionWatcherManager().shutdown_all() + logger.info("[Evolution] 所有Pipeline已停止") + except Exception: + pass + + logger.info("Qihu-AIFlow 正在关闭...") + + +app = FastAPI( + title="Qihu-AIFlow - 智能自动化PaaS平台", + version="1.0.0", + lifespan=lifespan, +) + +# API调用追踪中间件 +app.add_middleware(APITrackingMiddleware) + +# CORS配置 +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 前台 API +app.include_router(auth.router, prefix="/app/api/auth", tags=["认证"]) +app.include_router(chat.router, prefix="/app/api/chat", tags=["AI聊天"]) +app.include_router(knowledge.router, prefix="/app/api/knowledge", tags=["知识库"]) +app.include_router(market.router, prefix="/app/api/market", tags=["应用市场"]) +app.include_router(token_usage.router, prefix="/app/api/token", tags=["Token驾驶舱"]) +app.include_router(payment.router, prefix="/app/api/payment", tags=["付费中心"]) +app.include_router(profile.router, prefix="/app/api/profile", tags=["个人中心"]) +app.include_router(notifications.router, prefix="/app/api/notifications", tags=["消息通知"]) +app.include_router(assets.router, prefix="/app/api/assets", tags=["资产管理"]) +app.include_router(evolution.router, prefix="/app/api/evolution", tags=["AI自进化"]) + +# 后台 API +app.include_router(dashboard.router, prefix="/app/api/admin/dashboard", tags=["后台-仪表盘"]) +app.include_router(users.router, prefix="/app/api/admin/users", tags=["后台-用户管理"]) +app.include_router(models.router, prefix="/app/api/admin/models", tags=["后台-模型管理"]) +app.include_router(settings.router, prefix="/app/api/admin/settings", tags=["后台-网站设置"]) +app.include_router(payment_admin.router, prefix="/app/api/admin/payment", tags=["后台-支付配置"]) +app.include_router(services.router, prefix="/app/api/admin/services", tags=["后台-增值服务"]) +app.include_router(membership_admin.router, prefix="/app/api/admin/membership", tags=["后台-会员管理"]) +app.include_router(ads.router, prefix="/app/api/admin/ads", tags=["后台-广告管理"]) +app.include_router(market_admin.router, prefix="/app/api/admin/market", tags=["后台-应用审核"]) +app.include_router(logs.router, prefix="/app/api/admin/logs", tags=["后台-日志风控"]) + +app.include_router(skills_admin.router, prefix="/app/api/admin/skills", tags=["后台-技能管理"]) +app.include_router(agents_admin.router, prefix="/app/api/admin/agents", tags=["后台-Agent管理"]) +app.include_router(knowledge_admin.router, prefix="/app/api/admin/knowledge", tags=["后台-知识库管理"]) +app.include_router(workflow_admin.router, prefix="/app/api/admin/workflows", tags=["后台-工作流编排"]) +app.include_router(asset_admin.router, prefix="/app/api/admin/assets", tags=["后台-资产管理"]) +app.include_router(storage_admin.router, prefix="/app/api/admin/storage", tags=["后台-存储配置"]) +app.include_router(automation_admin.router, prefix="/app/api/admin/automation", tags=["后台-自动化任务"]) +app.include_router(mcp_admin.router, prefix="/app/api/admin/mcp", tags=["后台-MCP服务"]) +app.include_router(developer_admin.router, prefix="/app/api/admin/developer", tags=["后台-开发者API"]) +app.include_router(developer_tools_router, prefix="/app/api/admin/developer-tools", tags=["dev-tools"]) +app.include_router(apps_admin.router, prefix="/app/api/admin/apps", tags=["admin-ai-apps"]) +app.include_router(mcp_market_router, prefix="/app/api/admin/mcp-market", tags=["mcp-market"]) +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"]) +app.include_router(im_bot_user_router, prefix="/app/api/im/wechat", tags=["im-wechat-user"]) +app.include_router(promotion_admin.router, prefix="/app/api/admin/promotion", tags=["promotion"]) +app.include_router(tenant_admin.router, prefix="/app/api/admin/tenant", tags=["tenant"]) +app.include_router(billing_admin.router, prefix="/app/api/admin/billing", tags=["billing"]) +app.include_router(analytics_admin.router, prefix="/app/api/admin/analytics", tags=["analytics"]) +app.include_router(user_portal.router, prefix="/app/api/portal", tags=["portal"]) +app.include_router(achievement_admin.router, prefix="/app/api/admin/achievement", tags=["achievement"]) +app.include_router(casdoor_router, prefix="/app/api/auth", tags=["Casdoor SSO"]) + + +@app.get("/app/api/health") +async def health_check(): + return {"status": "ok", "service": "Qihu-AIFlow"} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d11bd98..c3af31f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,173 +1,171 @@ -import React, { useState, useEffect, Suspense } from 'react'; -import { Routes, Route, Navigate } from 'react-router-dom'; -import type { User } from '@/types'; -import { AuthContext, AuthContextType } from '@/contexts/AuthContext'; -import { authAPI } from '@/api'; - -// Layouts(保留为即时加载,构成应用外壳) -import FrontLayout from '@/components/layout/FrontLayout'; -import AdminLayout from '@/components/layout/AdminLayout'; - -// 路由级代码分割:每个页面独立打包成 chunk,首屏只加载外壳与公共依赖, -// 访问具体页面时才按需加载对应 chunk,显著加快首屏渲染与路由切换速度。 -const LandingPage = React.lazy(() => import('@/pages/front/LandingPage')); -const LoginPage = React.lazy(() => import('@/pages/front/LoginPage')); -const ChatPage = React.lazy(() => import('@/pages/front/ChatPage')); -const ChannelPage = React.lazy(() => import('@/pages/front/ChannelPage')); -const KnowledgePage = React.lazy(() => import('@/pages/front/KnowledgePage')); -const MarketPage = React.lazy(() => import('@/pages/front/MarketPage')); -const TokenDashboardPage = React.lazy(() => import('@/pages/front/TokenDashboardPage')); -const PricingPage = React.lazy(() => import('@/pages/front/PricingPage')); -const ProfilePage = React.lazy(() => import('@/pages/front/ProfilePage')); -const NotificationPage = React.lazy(() => import('@/pages/front/NotificationPage')); -const AssetsPage = React.lazy(() => import('@/pages/front/AssetsPage')); -const NovelPreviewPage = React.lazy(() => import('@/pages/front/NovelPreviewPage')); -const EvolutionDashboard = React.lazy(() => import('@/pages/front/EvolutionDashboard')); - -const AdminDashboard = React.lazy(() => import('@/pages/admin/Dashboard')); -const AdminUsers = React.lazy(() => import('@/pages/admin/Users')); -const AdminModels = React.lazy(() => import('@/pages/admin/Models')); -const AdminSiteSettings = React.lazy(() => import('@/pages/admin/SiteSettings')); -const AdminSmsConfig = React.lazy(() => import('@/pages/admin/SmsConfig')); -const AdminPayment = React.lazy(() => import('@/pages/admin/Payment')); -const AdminServices = React.lazy(() => import('@/pages/admin/Services')); -const AdminMembership = React.lazy(() => import('@/pages/admin/Membership')); -const AdminAds = React.lazy(() => import('@/pages/admin/Ads')); -const AdminMarket = React.lazy(() => import('@/pages/admin/Market')); -const AdminLogs = React.lazy(() => import('@/pages/admin/Logs')); -const AdminSkills = React.lazy(() => import('@/pages/admin/Skills')); -const AdminAgents = React.lazy(() => import('@/pages/admin/Agents')); -const AdminCodeExec = React.lazy(() => import('@/pages/admin/CodeExec')); -const DistributionBuilder = React.lazy(() => import('@/pages/admin/DistributionBuilder')); -const ImBots = React.lazy(() => import('@/pages/admin/ImBots')); -const PrivacySettings = React.lazy(() => import('@/pages/admin/PrivacySettings')); -const Promotion = React.lazy(() => import('@/pages/admin/Promotion')); -const Achievement = React.lazy(() => import('@/pages/admin/Achievement')); -const Tenant = React.lazy(() => import('@/pages/admin/Tenant')); -const Billing = React.lazy(() => import('@/pages/admin/Billing')); -const Analytics = React.lazy(() => import('@/pages/admin/Analytics')); -const AdminKnowledge = React.lazy(() => import('@/pages/admin/Knowledge')); -const AdminWorkflow = React.lazy(() => import('@/pages/admin/Workflow')); -const AdminStorage = React.lazy(() => import('@/pages/admin/Storage')); -const AdminDeveloper = React.lazy(() => import('@/pages/admin/Developer')); -const AdminDeveloperTools = React.lazy(() => import('@/pages/admin/DeveloperTools')); -const AdminAutomation = React.lazy(() => import('@/pages/admin/Automation')); -const AdminMcp = React.lazy(() => import('@/pages/admin/Mcp')); -const AdminApps = React.lazy(() => import('@/pages/admin/Apps')); -const AssetCenter = React.lazy(() => import('@/pages/admin/AssetCenter')); -const UserPortal = React.lazy(() => import('@/pages/UserPortal')); -const WechatBot = React.lazy(() => import('@/pages/front/WechatBot')); - -const pageFallback =
加载中...
; - -const App: React.FC = () => { - const [user, setUser] = useState(null); - const [loading, setLoading] = useState(true); - - // 初始化:从localStorage恢复登录状态 - useEffect(() => { - const token = localStorage.getItem('aiflow_token'); - const cachedUser = localStorage.getItem('aiflow_user'); - if (token && cachedUser) { - try { - setUser(JSON.parse(cachedUser)); - } catch { - localStorage.removeItem('aiflow_token'); - localStorage.removeItem('aiflow_user'); - } - } - setLoading(false); - }, []); - - const login = (u: User) => { - setUser(u); - localStorage.setItem('aiflow_user', JSON.stringify(u)); - }; - const logout = () => { - setUser(null); - localStorage.removeItem('aiflow_token'); - localStorage.removeItem('aiflow_user'); - }; - const isAdmin = user?.role === 'admin'; - - if (loading) { - return
加载中...
; - } - - return ( - - - - {/* Login */} - : ) : - } /> - - {/* Root - Landing for guests, redirect for authenticated users */} - : ) : - } /> - - {/* Front Routes (authenticated only) */} - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - {/* Admin Routes */} - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - {/* 公开前台首页:不受登录态拦截,供后台「访问前台」直接打开 */} - } /> - } /> - - - - ); -}; - -export default App; +import React, { useState, useEffect, Suspense } from 'react'; +import { Routes, Route, Navigate } from 'react-router-dom'; +import type { User } from '@/types'; +import { AuthContext, AuthContextType } from '@/contexts/AuthContext'; +import { authAPI } from '@/api'; + +// Layouts(保留为即时加载,构成应用外壳) +import FrontLayout from '@/components/layout/FrontLayout'; +import AdminLayout from '@/components/layout/AdminLayout'; + +// 路由级代码分割:每个页面独立打包成 chunk,首屏只加载外壳与公共依赖, +// 访问具体页面时才按需加载对应 chunk,显著加快首屏渲染与路由切换速度。 +const LandingPage = React.lazy(() => import('@/pages/front/LandingPage')); +const LoginPage = React.lazy(() => import('@/pages/front/LoginPage')); +const ChatPage = React.lazy(() => import('@/pages/front/ChatPage')); +const KnowledgePage = React.lazy(() => import('@/pages/front/KnowledgePage')); +const MarketPage = React.lazy(() => import('@/pages/front/MarketPage')); +const TokenDashboardPage = React.lazy(() => import('@/pages/front/TokenDashboardPage')); +const PricingPage = React.lazy(() => import('@/pages/front/PricingPage')); +const ProfilePage = React.lazy(() => import('@/pages/front/ProfilePage')); +const NotificationPage = React.lazy(() => import('@/pages/front/NotificationPage')); +const AssetsPage = React.lazy(() => import('@/pages/front/AssetsPage')); +const NovelPreviewPage = React.lazy(() => import('@/pages/front/NovelPreviewPage')); +const EvolutionDashboard = React.lazy(() => import('@/pages/front/EvolutionDashboard')); + +const AdminDashboard = React.lazy(() => import('@/pages/admin/Dashboard')); +const AdminUsers = React.lazy(() => import('@/pages/admin/Users')); +const AdminModels = React.lazy(() => import('@/pages/admin/Models')); +const AdminSiteSettings = React.lazy(() => import('@/pages/admin/SiteSettings')); +const AdminSmsConfig = React.lazy(() => import('@/pages/admin/SmsConfig')); +const AdminPayment = React.lazy(() => import('@/pages/admin/Payment')); +const AdminServices = React.lazy(() => import('@/pages/admin/Services')); +const AdminMembership = React.lazy(() => import('@/pages/admin/Membership')); +const AdminAds = React.lazy(() => import('@/pages/admin/Ads')); +const AdminMarket = React.lazy(() => import('@/pages/admin/Market')); +const AdminLogs = React.lazy(() => import('@/pages/admin/Logs')); +const AdminSkills = React.lazy(() => import('@/pages/admin/Skills')); +const AdminAgents = React.lazy(() => import('@/pages/admin/Agents')); +const AdminCodeExec = React.lazy(() => import('@/pages/admin/CodeExec')); +const DistributionBuilder = React.lazy(() => import('@/pages/admin/DistributionBuilder')); +const ImBots = React.lazy(() => import('@/pages/admin/ImBots')); +const PrivacySettings = React.lazy(() => import('@/pages/admin/PrivacySettings')); +const Promotion = React.lazy(() => import('@/pages/admin/Promotion')); +const Achievement = React.lazy(() => import('@/pages/admin/Achievement')); +const Tenant = React.lazy(() => import('@/pages/admin/Tenant')); +const Billing = React.lazy(() => import('@/pages/admin/Billing')); +const Analytics = React.lazy(() => import('@/pages/admin/Analytics')); +const AdminKnowledge = React.lazy(() => import('@/pages/admin/Knowledge')); +const AdminWorkflow = React.lazy(() => import('@/pages/admin/Workflow')); +const AdminStorage = React.lazy(() => import('@/pages/admin/Storage')); +const AdminDeveloper = React.lazy(() => import('@/pages/admin/Developer')); +const AdminDeveloperTools = React.lazy(() => import('@/pages/admin/DeveloperTools')); +const AdminAutomation = React.lazy(() => import('@/pages/admin/Automation')); +const AdminMcp = React.lazy(() => import('@/pages/admin/Mcp')); +const AdminApps = React.lazy(() => import('@/pages/admin/Apps')); +const AssetCenter = React.lazy(() => import('@/pages/admin/AssetCenter')); +const UserPortal = React.lazy(() => import('@/pages/UserPortal')); +const WechatBot = React.lazy(() => import('@/pages/front/WechatBot')); + +const pageFallback =
加载中...
; + +const App: React.FC = () => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + // 初始化:从localStorage恢复登录状态 + useEffect(() => { + const token = localStorage.getItem('aiflow_token'); + const cachedUser = localStorage.getItem('aiflow_user'); + if (token && cachedUser) { + try { + setUser(JSON.parse(cachedUser)); + } catch { + localStorage.removeItem('aiflow_token'); + localStorage.removeItem('aiflow_user'); + } + } + setLoading(false); + }, []); + + const login = (u: User) => { + setUser(u); + localStorage.setItem('aiflow_user', JSON.stringify(u)); + }; + const logout = () => { + setUser(null); + localStorage.removeItem('aiflow_token'); + localStorage.removeItem('aiflow_user'); + }; + const isAdmin = user?.role === 'admin'; + + if (loading) { + return
加载中...
; + } + + return ( + + + + {/* Login */} + : ) : + } /> + + {/* Root - Landing for guests, redirect for authenticated users */} + : ) : + } /> + + {/* Front Routes (authenticated only) */} + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + {/* Admin Routes */} + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + {/* 公开前台首页:不受登录态拦截,供后台「访问前台」直接打开 */} + } /> + } /> + + + + ); +}; + +export default App; diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 384195e..4751320 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -1,2016 +1,1959 @@ -/** - - * AIFlow API 客户端 - - * 统一管理所有前后端API请求 - - */ - -const API_BASE = '/app/api'; - - - -// 从localStorage获取token - -function getToken(): string | null { - - return localStorage.getItem('aiflow_token'); - -} - - - -// 通用请求方法 - -export async function request( - - endpoint: string, - - options: RequestInit = {} - -): Promise<{ code: number; data: T; message: string }> { - - const token = getToken(); - - const headers: Record = { - - 'Content-Type': 'application/json', - - ...(options.headers as Record || {}), - - }; - - if (token) { - - headers['Authorization'] = `Bearer ${token}`; - - } - - const res = await fetch(`${API_BASE}${endpoint}`, { - - ...options, - - headers, - - }); - - if (res.status === 401) { - - // 登录接口的401需要精确提示(用户名不存在 / 密码错误) - - if (endpoint === '/auth/login') { - - const err = await res.json().catch(() => ({ detail: '登录失败' })); - - const error: any = new Error(err.detail || '登录失败'); - - error.status = 401; - - error.detail = err.detail; - - throw error; - - } - - // 其他接口的401 = token过期/未登录,清除状态并跳转 - - localStorage.removeItem('aiflow_token'); - - localStorage.removeItem('aiflow_user'); - - window.location.href = '/login'; - - throw new Error('未登录'); - - } - - if (!res.ok) { - - const err = await res.json().catch(() => ({ detail: '请求失败' })); - - const error: any = new Error(err.detail || err.message || '请求失败'); - - error.status = res.status; - - error.response = { status: res.status, data: err }; - - error.detail = err.detail; - - throw error; - - } - - return res.json(); - -} - - - -// GET请求 - -function get(endpoint: string): Promise<{ code: number; data: T; message: string }> { - - return request(endpoint); - -} - - - -// POST请求 - -function post(endpoint: string, body?: any): Promise<{ code: number; data: T; message: string }> { - - return request(endpoint, { - - method: 'POST', - - body: body ? JSON.stringify(body) : undefined, - - }); - -} - - - -// PUT请求 - -function put(endpoint: string, body?: any): Promise<{ code: number; data: T; message: string }> { - - return request(endpoint, { - - method: 'PUT', - - body: body ? JSON.stringify(body) : undefined, - - }); - -} - - - -// DELETE请求 - -function del(endpoint: string): Promise<{ code: number; data: T; message: string }> { - - return request(endpoint, { method: 'DELETE' }); - -} - - - -// ============================================================ - -// 认证相关API - -// ============================================================ - -export const authAPI = { - - login: (username: string, password: string) => - - post('/auth/login', { username, password }), - - register: (data: { username: string; password: string; email?: string; phone?: string }) => - - post('/auth/register', data), - - getMe: () => get('/auth/me'), - - logout: () => post('/auth/logout'), - - getSettings: () => get('/auth/settings'), - -}; - - - -// ============================================================ - -// 聊天相关API - -// ============================================================ - -export const chatAPI = { - - getConversations: (status?: string) => - - get(`/chat/conversations${status ? `?status=${status}` : ''}`), - - createConversation: (data: { title: string; model_id?: string }) => - - post('/chat/conversations', data), - - updateConversation: (id: string, data: any) => - - put(`/chat/conversations/${id}`, data), - - deleteConversation: (id: string) => - - del(`/chat/conversations/${id}`), - - getMessages: (convId: string) => - - get(`/chat/conversations/${convId}/messages`), - - sendMessage: (convId: string, data: { content: string; model_id?: string; agent_ids?: string[]; skill_ids?: string[]; knowledge_base_ids?: string[]; use_tools?: boolean }) => - - post(`/chat/conversations/${convId}/messages`, data), - - clearContext: (convId: string) => - - post(`/chat/conversations/${convId}/clear`), - - convertToKnowledgeGraph: (convId: string) => - - post(`/chat/conversations/${convId}/to-knowledge-graph/`), - - getModels: () => get('/chat/models'), - -}; - - - -// ============================================================ - -// 频道相关API - -// ============================================================ - -export const channelAPI = { - - getChannels: () => get('/channels/'), - - createChannel: (data: any) => post('/channels/', data), - - getChannel: (id: string) => get(`/channels/${id}`), - - updateChannel: (id: string, data: any) => put(`/channels/${id}`, data), - - deleteChannel: (id: string) => del(`/channels/${id}`), - - connectChannel: (id: string) => post(`/channels/${id}/connect`), - - disconnectChannel: (id: string) => post(`/channels/${id}/disconnect`), - - getPlatformConfigs: () => get('/channels/platforms/config'), - - webhookMessage: (id: string, data: { from_user: string; content: string; message_type?: string }) => - - post(`/channels/${id}/webhook`, data), - - getMembers: (id: string) => get(`/channels/${id}/members`), - - addMember: (id: string, userId: string) => post(`/channels/${id}/members`, { user_id: userId }), - - removeMember: (id: string, userId: string) => del(`/channels/${id}/members/${userId}`), - - getMessages: (id: string, params?: { limit?: number; offset?: number }) => { - - const sp = new URLSearchParams(); - - if (params?.limit) sp.set('limit', String(params.limit)); - - if (params?.offset) sp.set('offset', String(params.offset)); - - const qs = sp.toString(); - - return get(`/channels/${id}/messages${qs ? `?${qs}` : ''}`); - - }, - - sendMessage: (id: string, content: string) => post(`/channels/${id}/messages`, { content }), - - authorize: (platform: string) => post('/channels/authorize', { platform }), - - scanAuthorize: (platform: string, credentials?: Record) => post(`/channels/authorize/scan?platform=${platform}`, credentials ? { credentials } : {}), - - scanAuthorizeStatus: (platform: string) => post(`/channels/authorize/scan/status?platform=${platform}`), - -}; - - - -// ============================================================ - -// 知识库相关API - -// ============================================================ - -export const knowledgeAPI = { - - getBases: () => get('/knowledge/bases'), - - createBase: (data: any) => post('/knowledge/bases', data), - - getBase: (id: string) => get(`/knowledge/bases/${id}`), - - updateBase: (id: string, data: any) => put(`/knowledge/bases/${id}`, data), - - deleteBase: (id: string) => del(`/knowledge/bases/${id}`), - - getDocuments: (baseId: string) => get(`/knowledge/bases/${baseId}/documents`), - - getDocument: (baseId: string, docId: string) => get(`/knowledge/bases/${baseId}/documents/${docId}`), - - uploadDocument: (baseId: string, data: any) => post(`/knowledge/bases/${baseId}/documents/`, data), - - deleteDocument: (baseId: string, docId: string) => del(`/knowledge/bases/${baseId}/documents/${docId}`), - - getDirectories: () => get('/knowledge/directories'), - - createDirectory: (data: any) => post('/knowledge/directories', data), - - buildGraph: (baseId: string) => post(`/knowledge/bases/${baseId}/build-graph`), - - getGraph: (baseId: string) => get(`/knowledge/bases/${baseId}/graph`), - - searchQA: (baseId: string, question: string) => post('/knowledge/qa', { base_id: baseId, question }), - - searchDocuments: (baseId: string, keyword: string) => get(`/knowledge/bases/${baseId}/search?keyword=${encodeURIComponent(keyword)}`), - -}; - - - -// ============================================================ - -// 应用市场相关API - -// ============================================================ - -export const marketAPI = { - - getApps: (params?: { type?: string; source?: string; search?: string; page?: number; page_size?: number }) => { - - const searchParams = new URLSearchParams(); - - if (params?.type) searchParams.set('type', params.type); - - if (params?.source) searchParams.set('source', params.source); - - if (params?.search) searchParams.set('search', params.search); - - if (params?.page) searchParams.set('page', String(params.page)); - - if (params?.page_size) searchParams.set('page_size', String(params.page_size)); - - const qs = searchParams.toString(); - - return get(`/market/apps${qs ? `?${qs}` : ''}`); - - }, - - getApp: (id: string) => get(`/market/apps/${id}`), - - createApp: (data: any) => post('/market/apps', data), - - purchaseApp: (id: string) => post(`/market/apps/${id}/purchase`), - - installApp: (id: string) => post(`/market/apps/${id}/install`), - - uninstallApp: (id: string) => del(`/market/apps/${id}/uninstall`), - - reviewApp: (id: string, data: { rating: number; content?: string }) => - - post(`/market/apps/${id}/review`, data), - - getMyApps: () => get('/market/my-apps'), - - getReviews: (appId: string) => get(`/market/reviews/${appId}`), - -}; - - - -// ============================================================ - -// Token相关API - -// ============================================================ - -export const tokenAPI = { - - getOverview: () => get('/token/overview'), - - getRankings: () => get('/token/rankings'), - - getLogs: (params?: any) => { - - const sp = new URLSearchParams(); - - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - - const qs = sp.toString(); - - return get(`/token/logs${qs ? `?${qs}` : ''}`); - - }, - - getBalance: () => get('/token/balance'), - - recharge: (amount: number) => post('/token/recharge', { amount }), - -}; - - - -// ============================================================ - -// 付费相关API - -// ============================================================ - -export const paymentAPI = { - - getPlans: () => get('/payment/plans'), - - createOrder: (data: any) => post('/payment/orders', data), - - getOrders: (status?: string) => get(`/payment/orders${status ? `?status=${status}` : ''}`), - - getOrder: (id: string) => get(`/payment/orders/${id}`), - - payOrder: (id: string, method?: string) => post(`/payment/orders/${id}/pay`, { payment_method: method }), - - getCoupons: () => get('/payment/coupons'), - - useCoupon: (code: string) => post('/payment/coupons/use', { code }), - - getServices: () => get('/payment/services'), - - createCustomOrder: (data: any) => post('/payment/custom-services', data), - - createServiceRequest: (data: any) => post('/payment/service-requests', data), - -}; - - - -// ============================================================ - -// 个人中心相关API - -// ============================================================ - -export const profileAPI = { - - getProfile: () => get('/profile/'), - - updateProfile: (data: any) => put('/profile/', data), - - updatePassword: (data: { old_password: string; new_password: string }) => - - put('/profile/password', data), - - getLoginLogs: () => get('/profile/login-logs'), - - getApiKeys: () => get('/profile/api-keys'), - - createApiKey: (name?: string) => post('/profile/api-keys', { name }), - - updateApiKey: (id: string, status: string) => put(`/profile/api-keys/${id}`, { status }), - - getMembership: () => get('/profile/membership'), - - getPurchasedApps: () => get('/profile/purchased-apps'), - -}; - - - -// ============================================================ - -// 通知相关API - -// ============================================================ - -export const notificationAPI = { - - getNotifications: (params?: { type?: string; page?: number }) => { - - const sp = new URLSearchParams(); - - if (params?.type) sp.set('type', params.type); - - if (params?.page) sp.set('page', String(params.page)); - - const qs = sp.toString(); - - return get(`/notifications/${qs ? `?${qs}` : ''}`); - - }, - - markRead: (id: string) => put(`/notifications/${id}/read`), - - markAllRead: () => put('/notifications/read-all'), - - getUnreadCount: () => get('/notifications/unread-count'), - -}; - - - -// ============================================================ - -// 资产管理相关API - -// ============================================================ - -export const assetsAPI = { - - getAssets: (category?: string) => - - get(`/assets/${category ? `?category=${category}` : ''}`), - - createAsset: (data: any) => post('/assets/', data), - - getAsset: (id: string) => get(`/assets/${id}`), - - updateAsset: (id: string, data: any) => put(`/assets/${id}`, data), - - deleteAsset: (id: string) => del(`/assets/${id}`), - - getCategories: () => get('/assets/categories'), - - getNovelChapters: (novelId: string) => get(`/assets/novels/${novelId}/chapters`), - - addChapter: (novelId: string, data: any) => post(`/assets/novels/${novelId}/chapters`, data), - -}; - - - -// ============================================================ - -// 后台管理API - -// ============================================================ - -export const adminAPI = { - - // 仪表盘 - - getStats: () => get('/admin/dashboard/stats'), - - getRecentLogs: () => get('/admin/dashboard/recent-logs'), - - getRecentAudit: () => get('/admin/dashboard/recent-audit'), - - getDailyStats: (days?: number) => get<{ days: number; data: Array<{ date: string; tokens: number }> }>(`/admin/dashboard/daily-stats${days ? `?days=${days}` : ''}`), - - // 用户管理 - - getUsers: (params?: any) => { - - const sp = new URLSearchParams(); - - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - - return get(`/admin/users/${sp.toString() ? `?${sp.toString()}` : ''}`); - - }, - - getUser: (id: string) => get(`/admin/users/${id}`), - - banUser: (id: string) => put(`/admin/users/${id}/ban`), - - unbanUser: (id: string) => put(`/admin/users/${id}/unban`), - - getRegisterSettings: () => get('/admin/users/register-settings'), - - updateRegisterSettings: (data: any) => put('/admin/users/register-settings', data), - - // 模型管理 - - getProviders: () => get('/admin/models/providers'), - - addProvider: (data: any) => post('/admin/models/providers', data), - - updateProvider: (id: string, data: any) => put(`/admin/models/providers/${id}`, data), - - deleteProvider: (id: string) => del(`/admin/models/providers/${id}`), - - fetchModels: (id: string) => post(`/admin/models/providers/${id}/fetch-models`), - - addModels: (id: string, modelIds: string[]) => post(`/admin/models/providers/${id}/add-models`, { model_ids: modelIds }), - - getProtocols: () => get('/admin/models/protocols'), - - getAdminModels: (params?: any) => { - - const sp = new URLSearchParams(); - - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - - return get(`/admin/models/${sp.toString() ? `?${sp.toString()}` : ''}`); - - }, - - addModel: (data: any) => post('/admin/models/', data), - - updateModel: (id: string, data: any) => put(`/admin/models/${id}`, data), - - deleteModel: (id: string) => del(`/admin/models/${id}`), - - toggleModel: (id: string, isEnabled: boolean) => put(`/admin/models/${id}/toggle?is_enabled=${isEnabled}`), - - setDefaultModel: (id: string) => put(`/admin/models/${id}/set-default`), - - // 按能力默认模型(Auto 自动模式使用) - getCapabilities: () => get('/admin/models/capabilities'), - getCapabilityDefaults: () => get('/admin/models/capability-defaults'), - setCapabilityDefault: (capability: string, modelId: string | null) => - put('/admin/models/capability-defaults', { capability, model_id: modelId }), - - // Phase 2: Enhanced model management - - testModelConnection: (id: string) => post(`/admin/models/${id}/test-connection`), - - testAllModels: () => post('/admin/models/test-all'), - - fetchProviderModels: (id: string) => post(`/admin/models/providers/${id}/fetch-models`), - - createAdminModel: (data: any) => post('/admin/models/', data), - - updateAdminModel: (id: string, data: any) => put(`/admin/models/${id}`, data), - - deleteAdminModel: (id: string) => del(`/admin/models/${id}`), - - getModelStats: () => get('/admin/models/stats'), - - // 网站设置 - - getSiteSettings: () => get('/admin/settings/site'), - - updateSiteSettings: (data: any) => put('/admin/settings/site', data), - - getEmailConfig: () => get('/admin/settings/email'), - - updateEmailConfig: (data: any) => put('/admin/settings/email', data), - - testEmail: (params?: { test_to?: string; test_subject?: string; test_body?: string }) => { - - const qs = new URLSearchParams(); - - if (params?.test_to) qs.set('test_to', params.test_to); - - if (params?.test_subject) qs.set('test_subject', params.test_subject); - - if (params?.test_body) qs.set('test_body', params.test_body); - - const qstr = qs.toString(); - - return post(`/admin/settings/email/test${qstr ? `?${qstr}` : ''}`); - - }, - - generateEmailContent: (topic: string) => post(`/admin/settings/email/generate-content?topic=${encodeURIComponent(topic)}`), - - getSmsConfig: () => get('/admin/settings/sms'), - - updateSmsConfig: (data: any) => put('/admin/settings/sms', data), - - testSms: () => post('/admin/settings/sms/test'), - - getSmsBalance: () => post('/admin/settings/sms/balance'), - - // 支付配置 - - getPaymentConfigs: () => get('/admin/payment/configs'), - - updatePaymentConfig: (id: string, data: any) => put(`/admin/payment/configs/${id}`, data), - - getAdminOrders: (params?: any) => { - - const sp = new URLSearchParams(); - - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - - return get(`/admin/payment/orders${sp.toString() ? `?${sp.toString()}` : ''}`); - - }, - - refundOrder: (id: string) => put(`/admin/payment/orders/${id}/refund`), - - // 增值服务 - - getServices: (params?: any) => { - - const sp = new URLSearchParams(); - - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - - return get(`/admin/services/${sp.toString() ? `?${sp.toString()}` : ''}`); - - }, - - addService: (data: any) => post('/admin/services/', data), - - updateService: (id: string, data: any) => put(`/admin/services/${id}`, data), - - deleteService: (id: string) => del(`/admin/services/${id}`), - - getCustomOrders: (status?: string) => get(`/admin/services/custom-orders${status ? `?status=${status}` : ''}`), - - updateCustomOrder: (id: string, data: any) => put(`/admin/services/custom-orders/${id}`, data), - - getCustomRequests: (params?: any) => { - - const sp = new URLSearchParams(); - - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - - return get(`/admin/services/custom-requests${sp.toString() ? `?${sp.toString()}` : ''}`); - - }, - - updateCustomRequest: (id: string, data: any) => put(`/admin/services/custom-requests/${id}`, data), - - getAvailableAgents: () => get('/admin/services/agents'), - - assignAgentToService: (id: string, agentId: string) => put(`/admin/services/${id}/agent?agent_id=${agentId}`), - - // 会员管理(增强版) - - getPlans: () => get('/admin/membership/plans'), - - addPlan: (params: string) => post(`/admin/membership/plans${params}`), - - updatePlan: (id: string, params: string) => put(`/admin/membership/plans/${id}${params}`), - - deletePlan: (id: string) => del(`/admin/membership/plans/${id}`), - - getSubscriptions: (page?: number) => get(`/admin/membership/subscriptions${page ? `?page=${page}` : ''}`), - - getAutomationRules: () => get('/admin/membership/automation-rules'), - - getBenefitsComparison: () => get('/admin/membership/benefits-comparison'), - - getMembershipConfigs: () => get('/admin/membership/global-configs'), - - updateMembershipConfig: (key: string, params: string) => put(`/admin/membership/global-configs/${key}${params}`), - - getUserMemberships: (page?: number, planId?: string, status?: string) => { - - const p = new URLSearchParams(); - - if (page) p.append('page', String(page)); - - if (planId) p.append('plan_id', planId); - - if (status) p.append('status', status); - - const qs = p.toString(); - - return get(`/admin/membership/user-memberships${qs ? `?${qs}` : ''}`); - - }, - - assignPlan: (userId: string, planId: string, days: number) => put(`/admin/membership/users/${userId}/assign-plan?plan_id=${planId}&duration_days=${days}`), - - getPaymentLogs: (params?: any) => { - - const sp = new URLSearchParams(); - - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - - return get(`/admin/logs/payment-logs${sp.toString() ? `?${sp.toString()}` : ''}`); - - }, - - getAuditLogs: (params?: any) => { - - const sp = new URLSearchParams(); - - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - - return get(`/admin/logs/audit-logs${sp.toString() ? `?${sp.toString()}` : ''}`); - - }, - - // 新增:日志统计 - - getLogStats: () => get('/admin/logs/stats'), - - // 新增:API调用统计 - - getApiStats: (params?: any) => { - - const sp = new URLSearchParams(); - - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - - return get(`/admin/logs/api-stats${sp.toString() ? `?${sp.toString()}` : ''}`); - - }, - - // 新增:慢请求记录 - - getSlowRequests: (params?: any) => { - - const sp = new URLSearchParams(); - - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - - return get(`/admin/logs/slow-requests${sp.toString() ? `?${sp.toString()}` : ''}`); - - }, - - // 新增:操作类型枚举 - - getActionTypes: () => get('/admin/logs/action-types'), - - // 新增:审计日志导出 - - exportAuditLogs: (params?: any) => { - - const sp = new URLSearchParams(); - - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - - return get(`/admin/logs/export/audit-logs${sp.toString() ? `?${sp.toString()}` : ''}`); - - }, - - exportOperationLogs: (params?: any) => { - - const sp = new URLSearchParams(); - - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - - return get(`/admin/logs/export/operations${sp.toString() ? `?${sp.toString()}` : ''}`); - - }, - - exportTokenLogs: (params?: any) => { - - const sp = new URLSearchParams(); - - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - - return get(`/admin/logs/export/token-logs${sp.toString() ? `?${sp.toString()}` : ''}`); - - }, - - getRiskRules: () => get('/admin/logs/risk-rules'), - - addRiskRule: (data: any) => post('/admin/logs/risk-rules', data), - - updateRiskRule: (id: string, data: any) => put(`/admin/logs/risk-rules/${id}`, data), - - deleteRiskRule: (id: string) => del(`/admin/logs/risk-rules/${id}`), - - // 技能管理 (Phase 2.2) - - getAdminSkills: (params?: any) => { - - const sp = new URLSearchParams(); - - if (params) Object.entries(params).forEach(([k, v]) => { if (v !== undefined && v !== null && v !== '') sp.set(k, String(v)); }); - - return get(`/admin/skills/${sp.toString() ? `?${sp.toString()}` : ''}`); - - }, - - getAdminSkill: (id: string) => get(`/admin/skills/${id}`), - - createAdminSkill: (data: any) => post('/admin/skills/', data), - - updateAdminSkill: (id: string, data: any) => put(`/admin/skills/${id}`, data), - - deleteAdminSkill: (id: string) => del(`/admin/skills/${id}`), - - changeAdminSkillState: (id: string, data: any) => put(`/admin/skills/${id}/state`, data), - - getAdminSkillVersions: (id: string) => get(`/admin/skills/${id}/versions`), - - rollbackAdminSkill: (id: string, data: any) => post(`/admin/skills/${id}/rollback`, data), - - publishAdminSkill: (id: string, data: any) => post(`/admin/skills/${id}/publish`, data), - - unpublishAdminSkill: (id: string) => post(`/admin/skills/${id}/unpublish`), - - getAdminSkillsStats: () => get('/admin/skills/stats/overview'), - -// 智能体管理 (Phase 2.3) - - getAdminAgents: (params?: any) => { - - const query = params ? '?' + Object.entries(params).filter(([_, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`).join('&') : ''; - - return get(`/admin/agents/${query}`); - - }, - - getAdminAgent: (id: string) => get(`/admin/agents/${id}`), - - createAdminAgent: (data: any) => post('/admin/agents/', data), - - updateAdminAgent: (id: string, data: any) => put(`/admin/agents/${id}`, data), - - deleteAdminAgent: (id: string) => del(`/admin/agents/${id}`), - - toggleAdminAgent: (id: string, isActive: boolean) => put(`/admin/agents/${id}/toggle`, { is_active: isActive }), - - getAdminAgentsStats: () => get('/admin/agents/stats/overview'), - - getAdminAgentReviews: (id: string) => get(`/admin/agents/${id}/reviews`), - - getAdminAvailableSkills: (params?: any) => { - - const query = params ? '?' + Object.entries(params).filter(([_, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`).join('&') : ''; - - return get(`/admin/agents/available/skills${query}`); - - }, - - getAdminAvailableModels: (params?: any) => { - - const query = params ? '?' + Object.entries(params).filter(([_, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`).join('&') : ''; - - return get(`/admin/agents/available/models${query}`); - - }, - - // 知识库管理 (Phase 2.4) - - getAdminKnowledgeStats: () => get('/admin/knowledge/stats/overview'), - - getAdminKnowledge: (params?: any) => { - - const query = params ? '?' + Object.entries(params).filter(([_, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`).join('&') : ''; - - return get(`/admin/knowledge/${query}`); - - }, - - getAdminKnowledgeDetail: (id: string) => get(`/admin/knowledge/${id}`), - - createAdminKnowledge: (data: any) => post('/admin/knowledge/', data), - - updateAdminKnowledge: (id: string, data: any) => put(`/admin/knowledge/${id}`, data), - - deleteAdminKnowledge: (id: string) => del(`/admin/knowledge/${id}`), - - toggleAdminKnowledge: (id: string, isActive: boolean) => put(`/admin/knowledge/${id}/toggle`, { is_active: isActive }), - - getAdminKnowledgeDocs: (kbId: string, params?: any) => { - - const query = params ? '?' + Object.entries(params).filter(([_, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`).join('&') : ''; - - return get(`/admin/knowledge/${kbId}/documents${query}`); - - }, - - deleteAdminKnowledgeDoc: (kbId: string, docId: string) => del(`/admin/knowledge/${kbId}/documents/${docId}`), - - getAdminKnowledgeExtServices: () => get('/admin/knowledge/external/services'), - - createAdminKnowledgeExtService: (data: any) => post('/admin/knowledge/external/services', data), - - updateAdminKnowledgeExtService: (id: string, data: any) => put(`/admin/knowledge/external/services/${id}`, data), - - deleteAdminKnowledgeExtService: (id: string) => del(`/admin/knowledge/external/services/${id}`), - - checkAdminKnowledgeExtStatus: (id: string) => get(`/admin/knowledge/external/services/${id}/status`), - - syncAdminKnowledge: (kbId: string, data: any) => post(`/admin/knowledge/external/sync/${kbId}`, data), - - batchDeleteAdminKnowledge: (ids: string[]) => post('/admin/knowledge/batch/delete', ids), - - // OSS存储配置 (Phase 3.1) - - getAssetStats: () => get('/admin/assets/stats'), - - getAdminAssets: (query: string) => get(`/admin/assets/?${query}`), - - deleteAdminAsset: (id: string) => del(`/admin/assets/${id}`), - - getAssetActivity: () => get('/admin/assets/activity'), - - getAdminStorageStats: () => get('/admin/storage/stats/overview'), - - getAdminStorageConfigs: (params?: any) => { - - const query = params ? '?' + Object.entries(params).filter(([_, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`).join('&') : ''; - - return get(`/admin/storage/${query}`); - - }, - - getAdminStorageConfig: (id: string) => get(`/admin/storage/${id}`), - - createAdminStorageConfig: (data: any) => post('/admin/storage/', data), - - updateAdminStorageConfig: (id: string, data: any) => put(`/admin/storage/${id}`, data), - - deleteAdminStorageConfig: (id: string) => del(`/admin/storage/${id}`), - - toggleAdminStorageConfig: (id: string) => put(`/admin/storage/${id}/toggle`), - - setDefaultAdminStorageConfig: (id: string) => put(`/admin/storage/${id}/set-default`), - - testAdminStorageConfig: (data: any) => post('/admin/storage/test', data), - - getAdminStorageUsage: (id: string) => get(`/admin/storage/${id}/usage`), - - // Storage Quota APIs (Phase C5) - - getStorageQuotaOverview: () => get('/admin/storage/quota/overview'), - - getStorageQuotaUsers: (params?: any) => { - - if (params) { - - const query = Object.entries(params).map(([k, v]) => v !== undefined && v !== '' && v !== null ? `${k}=${encodeURIComponent(String(v))}` : '').filter(Boolean).join('&'); - - return get(`/admin/storage/quota/users${query ? '?' + query : ''}`); - - } - - return get('/admin/storage/quota/users'); - - }, - - getStorageQuotaUser: (userId: string) => get(`/admin/storage/quota/users/${userId}`), - - adjustStorageQuota: (userId: string, data: any) => put(`/admin/storage/quota/users/${userId}`, data), - - recalcStorageQuota: (userId: string) => post(`/admin/storage/quota/recalculate/${userId}`, {}), - - initAllStorageQuotas: () => post('/admin/storage/quota/init-all', {}), - - - - // 自动化任务调度 (Phase 3.2) - - getAdminAutomationStats: () => get('/admin/automation/stats/overview'), - - getAdminAutomationTasks: (params?: any) => { - - const query = params ? '?' + Object.entries(params).filter(([_, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`).join('&') : ''; - - return get(`/admin/automation/${query}`); - - }, - - getAdminAutomationTask: (id: string) => get(`/admin/automation/${id}`), - - createAdminAutomationTask: (data: any) => post('/admin/automation/', data), - - updateAdminAutomationTask: (id: string, data: any) => put(`/admin/automation/${id}`, data), - - deleteAdminAutomationTask: (id: string) => del(`/admin/automation/${id}`), - - toggleAdminAutomationTask: (id: string) => put(`/admin/automation/${id}/toggle`), - - executeAdminAutomationTask: (id: string) => post(`/admin/automation/${id}/execute`), - - getAdminAutomationTaskHistory: (id: string, params?: any) => { - - const query = params ? '?' + Object.entries(params).filter(([_, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`).join('&') : ''; - - return get(`/admin/automation/${id}/history${query}`); - - }, - - // MCP Server Management - - getAdminMcpStats: () => get('/admin/mcp/stats/overview'), - - getAdminMcpServers: (params?: any) => { - - const query = new URLSearchParams(params).toString(); - - return get(`/admin/mcp/${query ? '?' + query : ''}`); - - }, - - getAdminMcpServer: (id: string) => get(`/admin/mcp/${id}`), - - createAdminMcpServer: (data: any) => post('/admin/mcp/', data), - - updateAdminMcpServer: (id: string, data: any) => put(`/admin/mcp/${id}`, data), - - deleteAdminMcpServer: (id: string) => del(`/admin/mcp/${id}`), - - toggleAdminMcpServer: (id: string) => put(`/admin/mcp/${id}/toggle`), - - testAdminMcpServer: (id: string, data?: any) => post(`/admin/mcp/${id}/test`, data || {}), - - getAdminMcpServerTools: (id: string) => get(`/admin/mcp/${id}/tools`), - - getAdminMcpServerResources: (id: string) => get(`/admin/mcp/${id}/resources`), - - // Developer API Management - - getAdminDeveloperStats: () => get('/admin/developer/stats/overview'), - - getAdminApiKeys: (params?: any) => { - - const sp = new URLSearchParams(); - - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - - return get(`/admin/developer/api-keys${sp.toString() ? '?' + sp.toString() : ''}`); - - }, - - createAdminApiKey: (data: any) => post('/admin/developer/api-keys/', data), - - updateAdminApiKey: (id: string, data: any) => put(`/admin/developer/api-keys/${id}`, data), - - revokeAdminApiKey: (id: string) => post(`/admin/developer/api-keys/${id}/revoke`), - - deleteAdminApiKey: (id: string) => del(`/admin/developer/api-keys/${id}`), - - getAdminOpenApiClients: (params?: any) => { - - const sp = new URLSearchParams(); - - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - - return get(`/admin/developer/openapi-clients${sp.toString() ? '?' + sp.toString() : ''}`); - - }, - - createAdminOpenApiClient: (data: any) => post('/admin/developer/openapi-clients/', data), - - updateAdminOpenApiClient: (id: string, data: any) => put(`/admin/developer/openapi-clients/${id}`, data), - - resetAdminOpenApiClientSecret: (id: string) => post(`/admin/developer/openapi-clients/${id}/reset-secret`), - - deleteAdminOpenApiClient: (id: string) => del(`/admin/developer/openapi-clients/${id}`), - - getAdminCallStats: (params?: any) => { - - const sp = new URLSearchParams(); - - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - - return get(`/admin/developer/call-stats${sp.toString() ? '?' + sp.toString() : ''}`); - - }, - - getAdminCallStatsOverview: () => get('/admin/developer/call-stats/overview'), - - getAdminSlowLogs: (params?: any) => { - - const sp = new URLSearchParams(); - - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - - return get(`/admin/developer/slow-logs${sp.toString() ? '?' + sp.toString() : ''}`); - - }, - - clearAdminSlowLogs: () => del('/admin/developer/slow-logs'), - - getAdminSlowLogsStats: () => get('/admin/developer/slow-logs/stats'), - - // AI App Management - - getAIAppStats: () => get('/admin/apps/stats/overview'), - - getAIApps: (params?: string) => get('/admin/apps/apps' + (params ? '?' + params : '')), - - getAIApp: (id: string) => get('/admin/apps/apps/' + id), - - createAIApp: (data: any) => post('/admin/apps/apps', data), - - updateAIApp: (id: string, data: any) => put('/admin/apps/apps/' + id, data), - - deleteAIApp: (id: string) => del('/admin/apps/apps/' + id), - - aiAppLifecycle: (id: string, action: string) => post('/admin/apps/apps/' + id + '/' + action, {}), - - getAIAppVersions: (id: string) => get('/admin/apps/apps/' + id + '/versions'), - - getAIAppAnalytics: (id: string) => get('/admin/apps/apps/' + id + '/analytics'), - - getAIAppTemplates: () => get('/admin/apps/templates'), - - createAIAppTemplate: (data: any) => post('/admin/apps/templates', data), - - createAIAppFromTemplate: (templateId: string, data: any) => post('/admin/apps/apps/from-template/' + templateId, data), - - getAIAppCategories: () => get('/admin/apps/categories'), - - // Logo 上传 - - uploadLogo: (formData: FormData) => { - - const token = localStorage.getItem('access_token'); - - return fetch(`/app/api/admin/settings/logo/upload`, { - - method: 'POST', - - headers: { 'Authorization': `Bearer ${token}` }, - - body: formData, - - }).then(res => res.json()); - - }, - - - - // MCP Market (Phase G1) - - getMcpMarketStats: () => - - get('/admin/mcp-market/stats'), - - getMcpTools: (params?: Record) => - - get('/admin/mcp-market/?' + new URLSearchParams(params || {}).toString()), - - getMcpToolDetail: (id: string) => - - get('/admin/mcp-market/' + id), - - createMcpTool: (data: any) => - - post('/admin/mcp-market/', data), - - updateMcpTool: (id: string, data: any) => - - put('/admin/mcp-market/' + id, data), - - deleteMcpTool: (id: string) => - - del('/admin/mcp-market/' + id), - - scanMcpTool: (id: string) => - - post('/admin/mcp-market/' + id + '/security-scan'), - - updateMcpToolSecurity: (id: string, data: any) => - - put('/admin/mcp-market/' + id + '/security-status', data), - - regenMcpDeepLink: (id: string) => - - post('/admin/mcp-market/' + id + '/deep-link'), - - getMcpManagedServers: () => - - get('/admin/mcp-market/managed/servers'), - - seedMcpTools: () => - - post('/admin/mcp-market/seed'), - - - - // Code Execution Sandbox (Phase G2) - - getCodeExecStats: () => get('/admin/code-exec/stats'), - - getCodeExecRuntimes: () => get('/admin/code-exec/runtimes'), - - executeCode: (data: { language: string; code: string; stdin?: string; timeout?: number }) => - - post('/admin/code-exec/execute', data), - - getCodeExecTasks: (params?: any) => { - - const sp = new URLSearchParams(); - - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - - return get(`/admin/code-exec/tasks${sp.toString() ? '?' + sp.toString() : ''}`); - - }, - - getCodeExecTask: (id: string) => get(`/admin/code-exec/tasks/${id}`), - - deleteCodeExecTask: (id: string) => del(`/admin/code-exec/tasks/${id}`), - - - - - - // Distribution Builder (Phase G4) - - getDistributionStats: () => get('/admin/distributions/stats'), - - getDistributions: (params?: any) => { - - const sp = new URLSearchParams(); - - if (params) Object.entries(params).forEach(([k, v]: [string, any]) => { if (v) sp.set(k, String(v)); }); - - return get('/admin/distributions' + (sp.toString() ? '?' + sp.toString() : '')); - - }, - - getDistributionTemplates: () => get('/admin/distributions/templates'), - - createDistribution: (data: any) => post('/admin/distributions', data), - - getDistribution: (id: string) => get('/admin/distributions/' + id), - - updateDistribution: (id: string, data: any) => put('/admin/distributions/' + id, data), - - deleteDistribution: (id: string) => del('/admin/distributions/' + id), - - getDistributionLicenses: (params?: any) => { - - const sp = new URLSearchParams(); - - if (params) Object.entries(params).forEach(([k, v]: [string, any]) => { if (v) sp.set(k, String(v)); }); - - return get('/admin/distributions/licenses' + (sp.toString() ? '?' + sp.toString() : '')); - - }, - - createDistributionLicense: (distroId: string, data: any) => post('/admin/distributions/' + distroId + '/licenses', data), - - getDistributionLicense: (id: string) => get('/admin/distributions/licenses/' + id), - - revokeDistributionLicense: (id: string) => post('/admin/distributions/licenses/' + id + '/revoke'), - - // ===== Ads Management ===== - - getAds: () => get('/admin/ads/'), - - addAd: (data: any) => post('/admin/ads/', data), - - updateAd: (id: string, data: any) => put(`/admin/ads/${id}`, data), - - deleteAd: (id: string) => del(`/admin/ads/${id}`), - - - - // ===== Market Admin (extended) ===== - - getMarketOverview: () => get('/admin/market/overview'), - - getTopApps: (metric?: string, limit?: number) => get(`/admin/market/top-apps${metric ? '?metric=' + metric : ''}${limit ? (metric ? '&' : '?') + 'limit=' + limit : ''}`), - - getAdminMarketApps: (params?: string) => get(`/admin/market/apps${params ? '?' + params : ''}`), - - getPendingApps: () => get('/admin/market/pending'), - - getCoupons: () => get('/admin/market/coupons'), - - getRecentReviews: () => get('/admin/market/reviews'), - - approveApp: (id: string) => post(`/admin/market/apps/${id}/approve`), - - rejectApp: (id: string) => post(`/admin/market/apps/${id}/reject`), - - setFeaturedApp: (id: string, featured: boolean, sortOrder?: number) => put(`/admin/market/apps/${id}/featured`, { featured, sort_order: sortOrder }), - - deleteMarketApp: (id: string) => del(`/admin/market/apps/${id}`), - - deleteCoupon: (id: string) => del(`/admin/market/coupons/${id}`), - - updateMarketApp: (id: string, data: any) => put(`/admin/market/apps/${id}`, data), - - updateCoupon: (id: string, data: any) => put(`/admin/market/coupons/${id}`, data), - - addCoupon: (data: any) => post('/admin/market/coupons', data), - - - - // ===== Membership (extended) ===== - - getMembershipStats: () => get('/admin/membership/stats'), - - revokeMembership: (userId: string) => post(`/admin/membership/users/${userId}/revoke`), - - - - // ===== Logs (extended) ===== - - getOperationLogs: (params?: any) => { - - const sp = new URLSearchParams(); - - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - - return get(`/admin/logs/operation-logs${sp.toString() ? '?' + sp.toString() : ''}`); - - }, - - getTokenLogs: (params?: any) => { - - const sp = new URLSearchParams(); - - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - - return get(`/admin/logs/token-logs${sp.toString() ? '?' + sp.toString() : ''}`); - - }, - - - - // ===== Analytics ===== - - getAnalyticsOverview: (days?: number) => get(`/admin/analytics/overview${days ? '?days=' + days : ''}`), - - getAnalyticsApiTrends: (days?: number) => get(`/admin/analytics/trends/api-calls${days ? '?days=' + days : ''}`), - - getAnalyticsTokenTrends: (days?: number) => get(`/admin/analytics/trends/tokens${days ? '?days=' + days : ''}`), - - getAnalyticsUserTrends: (days?: number) => get(`/admin/analytics/trends/users${days ? '?days=' + days : ''}`), - - getAnalyticsRevenueTrends: (days?: number) => get(`/admin/analytics/trends/revenue${days ? '?days=' + days : ''}`), - - getAnalyticsModelUsage: (days?: number) => get(`/admin/analytics/models/usage${days ? '?days=' + days : ''}`), - - - - // ===== Billing ===== - - getBillingRules: () => get('/admin/billing/rules'), - - getBillingPackages: () => get('/admin/billing/packages'), - - getBillingTransactions: (page?: number) => get(`/admin/billing/transactions${page ? '?page=' + page : ''}`), - - getBillingStats: () => get('/admin/billing/stats/overview'), - - - - // ===== Tenant ===== - - getTenants: (page?: number) => get(`/admin/tenant/list${page ? '?page=' + page : ''}`), - - getTenantStats: () => get('/admin/tenant/stats/overview'), - - getTenantMembers: (tenantId: string) => get(`/admin/tenant/${tenantId}/members`), - - createTenant: (params: string) => post(`/admin/tenant/create${params}`), - - - -}; - -// ===== AI自进化API ===== - -export const evolutionAPI = { - - // 驾驶舱面板 - - getSkillTree: () => get('/evolution/dashboard/skill-tree'), - - getTimeline: (eventType?: string) => - - get('/evolution/dashboard/timeline' + (eventType ? `?event_type=${eventType}` : '')), - - getStatistics: () => get('/evolution/dashboard/statistics'), - - getProfile: () => get('/evolution/dashboard/profile'), - - - - // 技能管理 - - getSkills: (params?: { state?: string; keyword?: string; sort_by?: string; order?: string }) => { - - const query = new URLSearchParams(); - - if (params?.state) query.append('state', params.state); - - if (params?.keyword) query.append('keyword', params.keyword); - - if (params?.sort_by) query.append('sort_by', params.sort_by); - - if (params?.order) query.append('order', params.order); - - const qs = query.toString(); - - return get('/evolution/skills' + (qs ? `?${qs}` : '')); - - }, - - getSkillDetail: (name: string) => get(`/evolution/skills/${encodeURIComponent(name)}`), - - updateSkill: (name: string, data: { description?: string; tags?: string[]; category?: string }) => - - put(`/evolution/skills/${encodeURIComponent(name)}`, data), - - deleteSkill: (name: string) => del(`/evolution/skills/${encodeURIComponent(name)}`), - - freezeSkill: (name: string) => post(`/evolution/skills/${encodeURIComponent(name)}/freeze`), - - unfreezeSkill: (name: string) => post(`/evolution/skills/${encodeURIComponent(name)}/unfreeze`), - - getSkillHistory: (name: string) => get(`/evolution/skills/${encodeURIComponent(name)}/history`), - - getSkillTrajectories: (name: string) => get(`/evolution/skills/${encodeURIComponent(name)}/trajectories`), - - - - // 事件 - - getEvents: (params?: { event_type?: string; skill_name?: string; limit?: number }) => { - - const query = new URLSearchParams(); - - if (params?.event_type) query.append('event_type', params.event_type); - - if (params?.skill_name) query.append('skill_name', params.skill_name); - - if (params?.limit) query.append('limit', String(params.limit)); - - const qs = query.toString(); - - return get('/evolution/events' + (qs ? `?${qs}` : '')); - - }, - - - - // 轨迹提交 - - submitTrajectory: (data: { conversation_id: string; messages: any[] }) => - - post('/evolution/trajectory/submit', data), - - - - // v2: 技能回滚 - - rollbackSkill: (skillName: string, version: number) => - - post(`/evolution/skills/${encodeURIComponent(skillName)}/rollback/${version}`), - - - - // v2: 发布到市场 - - publishSkill: (skillName: string, data: { price: number; market_description: string }) => - - post(`/evolution/skills/${encodeURIComponent(skillName)}/publish`, data), - - - - // v2: 团队共享 - - shareToTeam: (skillName: string, teamId: string) => - - post(`/evolution/skills/${encodeURIComponent(skillName)}/share-to-team`, { team_id: teamId }), - - - - // v2: 流水线状态 - - getPipelineStatus: () => get('/evolution/pipeline/status'), - - - - // v2: 手动触发流水线 - - triggerPipeline: () => post('/evolution/pipeline/trigger'), - - - - // v2: Watcher状态 - - getWatcherStatus: () => get('/evolution/watcher/status'), - - - - // v2: 进化报告 - - getReports: (reportType?: string) => { - - const qs = reportType ? `?report_type=${reportType}` : ''; - - return get(`/evolution/reports${qs}`); - - }, - - getReportDetail: (reportId: string) => get(`/evolution/reports/${reportId}`), - - - - - - // Workflow (Phase 3.6) - - getWorkflows: (params?: Record) => - - get('/admin/workflows/?' + new URLSearchParams(params || {}).toString()), - - getWorkflow: (id: string) => - - get(`/admin/workflows/${id}`), - - getWorkflowStats: () => - - get('/admin/workflows/stats'), - - getWorkflowTemplates: () => - - get('/admin/workflows/templates'), - - createWorkflow: (data: any) => - - post('/admin/workflows/', data), - - updateWorkflow: (id: string, data: any) => - - put(`/admin/workflows/${id}`, data), - - deleteWorkflow: (id: string) => - - del(`/admin/workflows/${id}`), - - toggleWorkflow: (id: string, isActive: boolean) => - - post(`/admin/workflows/${id}/toggle?is_active=${isActive}`), - - executeWorkflow: (id: string, inputText: string) => - - post(`/admin/workflows/${id}/execute`, { input_text: inputText }), - - - - - - // Privacy & Local Agent (Phase G5) - - getPrivacyStats: () => get('/admin/privacy/stats'), - - getPrivacyConfig: () => get('/admin/privacy/config'), - - updatePrivacyConfig: (params: any) => { - - const sp = new URLSearchParams(); - - Object.entries(params).forEach(([k, v]) => { if (v != null) sp.append(k, String(v)); }); - - return put('/admin/privacy/config?' + sp.toString()); - - }, - - switchPrivacyMode: (mode: string) => post('/admin/privacy/switch-mode?mode=' + encodeURIComponent(mode)), - - getPrivacyLicenses: (params?: any) => { - - const sp = new URLSearchParams(); - - if (params?.status) sp.append('status', params.status); - - return get('/admin/privacy/licenses' + (sp.toString() ? '?' + sp.toString() : '')); - - }, - - generateOfflineLicense: (data: any) => { - - const sp = new URLSearchParams(); - - if (data.agent_id) sp.append('agent_id', data.agent_id); - - sp.append('max_offline_days', String(data.max_offline_days || 7)); - - return post('/admin/privacy/licenses/generate?' + sp.toString()); - - }, - - revokePrivacyLicense: (id: number) => post('/admin/privacy/licenses/' + id + '/revoke'), - - validatePrivacyLicense: (licenseKey: string) => { - - const sp = new URLSearchParams(); - - sp.append('license_key', licenseKey); - - return post('/admin/privacy/licenses/validate?' + sp.toString()); - - }, - - getKbSyncStates: () => get('/admin/privacy/kb-sync'), - - triggerKbSync: (kbId: number, direction: string) => { - - const sp = new URLSearchParams(); - - sp.append('knowledge_base_id', String(kbId)); - - sp.append('sync_direction', direction); - - return post('/admin/privacy/kb-sync/trigger?' + sp.toString()); - - }, - - getLocalAgents: () => get('/admin/privacy/local-agents'), - - getPrivacyUsageStats: () => get('/admin/privacy/usage/stats'), - - getBridgeConfig: () => get('/admin/privacy/bridge-config'), - - - - // IM Bot Management (Phase 4) - - getImBotStats: () => get('/admin/im-bot/stats'), - - getImBotDashboard: () => get('/admin/im-bot/dashboard'), - - getImBotChannels: (params?: string) => get(`/admin/im-bot/channels${params || ''}`), - - getImBotChannelDetail: (id: string) => get(`/admin/im-bot/channels/${id}`), - - testImBotChannel: (id: string) => post(`/admin/im-bot/channels/${id}/test`), - - activateImBotChannel: (id: string) => post(`/admin/im-bot/channels/${id}/activate`), - - deactivateImBotChannel: (id: string) => post(`/admin/im-bot/channels/${id}/deactivate`), - - getImBotRoutes: (params?: string) => get(`/admin/im-bot/routes${params || ''}`), - - createImBotRoute: (params: string) => post(`/admin/im-bot/routes${params}`), - - deleteImBotRoute: (id: string) => del(`/admin/im-bot/routes/${id}`), - - // 个人微信(iLink 协议)原生适配器 - - getWechatConfig: () => get('/admin/im-bot/wechat'), - - wechatQrcode: () => post('/admin/im-bot/wechat/qrcode'), - - wechatQrcodeStatus: (params: string) => post(`/admin/im-bot/wechat/qrcode/status${params}`), - - getWechatStatus: () => get('/admin/im-bot/wechat/status'), - - disconnectWechat: () => post('/admin/im-bot/wechat/disconnect'), - - getImBotPlatforms: () => get('/admin/im-bot/platforms'), - - getImBotWebhookUrl: (platform: string) => get(`/admin/im-bot/webhook-url/${platform}`), - - getImBotMessages: (params?: string) => get(`/admin/im-bot/messages${params || ''}`), - - imBotQuickSetup: (params: string) => post(`/admin/im-bot/quick-setup${params}`), - - - - // 推广系统 - - getPromotionStats: () => get('/admin/promotion/stats'), - - getPromotions: (params?: any) => { - - const sp = new URLSearchParams(); - - if (params) Object.entries(params).forEach(([k, v]) => { if (v !== undefined && v !== null && v !== '') sp.set(k, String(v)); }); - - return get(`/admin/promotion/promotions${sp.toString() ? `?${sp.toString()}` : ''}`); - - }, - - createPromotion: (params: any) => { - - const sp = new URLSearchParams(); - - Object.entries(params).forEach(([k, v]) => { if (v !== undefined && v !== null && v !== '') sp.set(k, String(v)); }); - - return post(`/admin/promotion/promotions${sp.toString() ? `?${sp.toString()}` : ''}`); - - }, - - updatePromotion: (id: string, params: any) => { - - const sp = new URLSearchParams(); - - Object.entries(params).forEach(([k, v]) => { if (v !== undefined && v !== null && v !== '') sp.set(k, String(v)); }); - - return put(`/admin/promotion/promotions/${id}${sp.toString() ? `?${sp.toString()}` : ''}`); - - }, - - deletePromotion: (id: string) => del(`/admin/promotion/promotions/${id}`), - - togglePromotion: (id: string) => post(`/admin/promotion/promotions/${id}/toggle`), - - getReferralLinks: (params?: any) => { - - const sp = new URLSearchParams(); - - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - - return get(`/admin/promotion/referral-links${sp.toString() ? `?${sp.toString()}` : ''}`); - - }, - - generateReferralLink: (params: any) => { - - const sp = new URLSearchParams(); - - Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - - return post(`/admin/promotion/referral-links/generate${sp.toString() ? `?${sp.toString()}` : ''}`); - - }, - - getReferralRewards: (params?: any) => { - - const sp = new URLSearchParams(); - - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - - return get(`/admin/promotion/rewards${sp.toString() ? `?${sp.toString()}` : ''}`); - - }, - - approveReward: (id: string) => post(`/admin/promotion/rewards/${id}/approve`), - - markRewardPaid: (id: string, params: any) => { - - const sp = new URLSearchParams(); - - Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - - return post(`/admin/promotion/rewards/${id}/pay${sp.toString() ? `?${sp.toString()}` : ''}`); - - }, - - getPromotionLeaderboard: (params?: any) => { - - const sp = new URLSearchParams(); - - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - - return get(`/admin/promotion/leaderboard${sp.toString() ? `?${sp.toString()}` : ''}`); - - }, - - regenerateReferralCode: (id: string) => post(`/admin/promotion/referral-links/${id}/regenerate`), - - getMyReferrals: () => get('/admin/promotion/my-referrals'), - - getMyRewards: () => get('/admin/promotion/my-rewards'), - - - - // 成就系统 - - getAchievements: (params?: string) => get(`/admin/achievement/achievements${params || ''}`), - - getAchievementDetail: (id: string) => get(`/admin/achievement/achievements/${id}`), - - createAchievement: (params: string) => post(`/admin/achievement/achievements${params}`), - - updateAchievement: (id: string, params: string) => put(`/admin/achievement/achievements/${id}${params}`), - - deleteAchievement: (id: string) => del(`/admin/achievement/achievements/${id}`), - - getUserAchievements: () => get('/admin/achievement/user/achievements'), - - claimAchievementReward: (id: string) => post(`/admin/achievement/user/achievements/${id}/claim`), - - checkAchievementProgress: () => post('/admin/achievement/check-progress'), - - getAchievementLeaderboard: (params?: string) => get(`/admin/achievement/leaderboard${params || ''}`), - - - - // 多租户管理 - - getTenants: (page?: number, keyword?: string, status?: string) => { - - const sp = new URLSearchParams(); - - if (page) sp.set('page', String(page)); - - if (keyword) sp.set('keyword', keyword); - - if (status) sp.set('status', status); - - sp.set('page_size', '20'); - - return get(`/admin/tenant/list${sp.toString() ? `?${sp.toString()}` : ''}`); - - }, - - createTenant: (params: string) => post(`/admin/tenant/create${params}`), - - getTenantDetail: (id: string) => get(`/admin/tenant/${id}`), - - updateTenant: (id: string, params: string) => put(`/admin/tenant/${id}${params}`), - - deleteTenant: (id: string) => del(`/admin/tenant/${id}`), - - getTenantMembers: (tenantId: string, page?: number) => { - - const sp = new URLSearchParams(); - - sp.set('page', String(page || 1)); - - sp.set('page_size', '100'); - - return get(`/admin/tenant/${tenantId}/members?${sp.toString()}`); - - }, - - addTenantMember: (tenantId: string, params: string) => post(`/admin/tenant/${tenantId}/members/add${params}`), - - updateTenantMember: (tenantId: string, memberId: string, params: string) => put(`/admin/tenant/${tenantId}/members/${memberId}${params}`), - - removeTenantMember: (tenantId: string, memberId: string) => del(`/admin/tenant/${tenantId}/members/${memberId}`), - - getTenantQuotas: (tenantId: string) => get(`/admin/tenant/${tenantId}/quotas`), - - updateTenantQuota: (tenantId: string, params: string) => put(`/admin/tenant/${tenantId}/quotas${params}`), - - getTenantInvitations: (tenantId: string) => get(`/admin/tenant/${tenantId}/invitations`), - - createTenantInvitation: (tenantId: string, params: string) => post(`/admin/tenant/${tenantId}/invitations${params}`), - - getTenantStats: () => get('/admin/tenant/stats/overview'), - - - - // 计费与支付 - - getBillingRules: (page?: number) => get(`/admin/billing/rules?page=${page || 1}&page_size=100`), - - createBillingRule: (params: string) => post(`/admin/billing/rules/create${params}`), - - updateBillingRule: (id: string, params: string) => put(`/admin/billing/rules/${id}${params}`), - - deleteBillingRule: (id: string) => del(`/admin/billing/rules/${id}`), - - getBillingPackages: () => get('/admin/billing/packages'), - - createBillingPackage: (params: string) => post(`/admin/billing/packages/create${params}`), - - updateBillingPackage: (id: string, params: string) => put(`/admin/billing/packages/${id}${params}`), - - deleteBillingPackage: (id: string) => del(`/admin/billing/packages/${id}`), - - getBillingTransactions: (page?: number) => get(`/admin/billing/transactions?page=${page || 1}&page_size=20`), - - getInvoices: (page?: number) => get(`/admin/billing/invoices?page=${page || 1}&page_size=20`), - - generateInvoice: (params: string) => post(`/admin/billing/invoices/generate${params}`), - - getPaymentConfigs: () => get('/admin/billing/payment-configs'), - - updatePaymentConfig: (id: string, params: string) => put(`/admin/billing/payment-configs/${id}${params}`), - - getBillingStats: () => get('/admin/billing/stats/overview'), - - - - // 数据分析 - - getAnalyticsOverview: (days?: number) => get(`/admin/analytics/overview?days=${days || 30}`), - - getAnalyticsApiTrends: (days?: number) => get(`/admin/analytics/trends/api-calls?days=${days || 7}`), - - getAnalyticsTokenTrends: (days?: number) => get(`/admin/analytics/trends/tokens?days=${days || 7}`), - - getAnalyticsUserTrends: (days?: number) => get(`/admin/analytics/trends/users?days=${days || 30}`), - - getAnalyticsRevenueTrends: (days?: number) => get(`/admin/analytics/trends/revenue?days=${days || 30}`), - - getAnalyticsModelUsage: (days?: number) => get(`/admin/analytics/models/usage?days=${days || 30}`), - - getAnalyticsDashboards: () => get('/admin/analytics/dashboards'), - - createAnalyticsDashboard: (params: string) => post(`/admin/analytics/dashboards/create${params}`), - - deleteAnalyticsDashboard: (id: string) => del(`/admin/analytics/dashboards/${id}`), - - getAnalyticsEvents: (params?: any) => { - - const sp = new URLSearchParams(); - - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - - return get(`/admin/analytics/events${sp.toString() ? `?${sp.toString()}` : ''}`); - - }, - - - -}; - - - - - -// ===== 前台用户个人微信 (iLink 协议 / 微信 ClawBot 插件) ===== - -export const imUserAPI = { - - getStatus: () => get('/im/wechat/status'), - - qrcode: () => post('/im/wechat/qrcode'), - - qrcodeStatus: (qrcode: string) => post(`/im/wechat/qrcode/status?qrcode=${encodeURIComponent(qrcode)}`), - - disconnect: () => post('/im/wechat/disconnect'), - - history: () => get('/im/wechat/history'), - -}; - +/** + + * AIFlow API 客户端 + + * 统一管理所有前后端API请求 + + */ + +const API_BASE = '/app/api'; + + + +// 从localStorage获取token + +function getToken(): string | null { + + return localStorage.getItem('aiflow_token'); + +} + + + +// 通用请求方法 + +export async function request( + + endpoint: string, + + options: RequestInit = {} + +): Promise<{ code: number; data: T; message: string }> { + + const token = getToken(); + + const headers: Record = { + + 'Content-Type': 'application/json', + + ...(options.headers as Record || {}), + + }; + + if (token) { + + headers['Authorization'] = `Bearer ${token}`; + + } + + const res = await fetch(`${API_BASE}${endpoint}`, { + + ...options, + + headers, + + }); + + if (res.status === 401) { + + // 登录接口的401需要精确提示(用户名不存在 / 密码错误) + + if (endpoint === '/auth/login') { + + const err = await res.json().catch(() => ({ detail: '登录失败' })); + + const error: any = new Error(err.detail || '登录失败'); + + error.status = 401; + + error.detail = err.detail; + + throw error; + + } + + // 其他接口的401 = token过期/未登录,清除状态并跳转 + + localStorage.removeItem('aiflow_token'); + + localStorage.removeItem('aiflow_user'); + + window.location.href = '/login'; + + throw new Error('未登录'); + + } + + if (!res.ok) { + + const err = await res.json().catch(() => ({ detail: '请求失败' })); + + const error: any = new Error(err.detail || err.message || '请求失败'); + + error.status = res.status; + + error.response = { status: res.status, data: err }; + + error.detail = err.detail; + + throw error; + + } + + return res.json(); + +} + + + +// GET请求 + +function get(endpoint: string): Promise<{ code: number; data: T; message: string }> { + + return request(endpoint); + +} + + + +// POST请求 + +function post(endpoint: string, body?: any): Promise<{ code: number; data: T; message: string }> { + + return request(endpoint, { + + method: 'POST', + + body: body ? JSON.stringify(body) : undefined, + + }); + +} + + + +// PUT请求 + +function put(endpoint: string, body?: any): Promise<{ code: number; data: T; message: string }> { + + return request(endpoint, { + + method: 'PUT', + + body: body ? JSON.stringify(body) : undefined, + + }); + +} + + + +// DELETE请求 + +function del(endpoint: string): Promise<{ code: number; data: T; message: string }> { + + return request(endpoint, { method: 'DELETE' }); + +} + + + +// ============================================================ + +// 认证相关API + +// ============================================================ + +export const authAPI = { + + login: (username: string, password: string) => + + post('/auth/login', { username, password }), + + register: (data: { username: string; password: string; email?: string; phone?: string }) => + + post('/auth/register', data), + + getMe: () => get('/auth/me'), + + logout: () => post('/auth/logout'), + + getSettings: () => get('/auth/settings'), + +}; + + + +// ============================================================ + +// 聊天相关API + +// ============================================================ + +export const chatAPI = { + + getConversations: (status?: string) => + + get(`/chat/conversations${status ? `?status=${status}` : ''}`), + + createConversation: (data: { title: string; model_id?: string }) => + + post('/chat/conversations', data), + + updateConversation: (id: string, data: any) => + + put(`/chat/conversations/${id}`, data), + + deleteConversation: (id: string) => + + del(`/chat/conversations/${id}`), + + getMessages: (convId: string) => + + get(`/chat/conversations/${convId}/messages`), + + sendMessage: (convId: string, data: { content: string; model_id?: string; agent_ids?: string[]; skill_ids?: string[]; knowledge_base_ids?: string[]; use_tools?: boolean }) => + + post(`/chat/conversations/${convId}/messages`, data), + + clearContext: (convId: string) => + + post(`/chat/conversations/${convId}/clear`), + + convertToKnowledgeGraph: (convId: string) => + + post(`/chat/conversations/${convId}/to-knowledge-graph/`), + + getModels: () => get('/chat/models'), + +}; + + + + + + +// ============================================================ + +// 知识库相关API + +// ============================================================ + +export const knowledgeAPI = { + + getBases: () => get('/knowledge/bases'), + + createBase: (data: any) => post('/knowledge/bases', data), + + getBase: (id: string) => get(`/knowledge/bases/${id}`), + + updateBase: (id: string, data: any) => put(`/knowledge/bases/${id}`, data), + + deleteBase: (id: string) => del(`/knowledge/bases/${id}`), + + getDocuments: (baseId: string) => get(`/knowledge/bases/${baseId}/documents`), + + getDocument: (baseId: string, docId: string) => get(`/knowledge/bases/${baseId}/documents/${docId}`), + + uploadDocument: (baseId: string, data: any) => post(`/knowledge/bases/${baseId}/documents/`, data), + + deleteDocument: (baseId: string, docId: string) => del(`/knowledge/bases/${baseId}/documents/${docId}`), + + getDirectories: () => get('/knowledge/directories'), + + createDirectory: (data: any) => post('/knowledge/directories', data), + + buildGraph: (baseId: string) => post(`/knowledge/bases/${baseId}/build-graph`), + + getGraph: (baseId: string) => get(`/knowledge/bases/${baseId}/graph`), + + searchQA: (baseId: string, question: string) => post('/knowledge/qa', { base_id: baseId, question }), + + searchDocuments: (baseId: string, keyword: string) => get(`/knowledge/bases/${baseId}/search?keyword=${encodeURIComponent(keyword)}`), + +}; + + + +// ============================================================ + +// 应用市场相关API + +// ============================================================ + +export const marketAPI = { + + getApps: (params?: { type?: string; source?: string; search?: string; page?: number; page_size?: number }) => { + + const searchParams = new URLSearchParams(); + + if (params?.type) searchParams.set('type', params.type); + + if (params?.source) searchParams.set('source', params.source); + + if (params?.search) searchParams.set('search', params.search); + + if (params?.page) searchParams.set('page', String(params.page)); + + if (params?.page_size) searchParams.set('page_size', String(params.page_size)); + + const qs = searchParams.toString(); + + return get(`/market/apps${qs ? `?${qs}` : ''}`); + + }, + + getApp: (id: string) => get(`/market/apps/${id}`), + + createApp: (data: any) => post('/market/apps', data), + + purchaseApp: (id: string) => post(`/market/apps/${id}/purchase`), + + installApp: (id: string) => post(`/market/apps/${id}/install`), + + uninstallApp: (id: string) => del(`/market/apps/${id}/uninstall`), + + reviewApp: (id: string, data: { rating: number; content?: string }) => + + post(`/market/apps/${id}/review`, data), + + getMyApps: () => get('/market/my-apps'), + + getReviews: (appId: string) => get(`/market/reviews/${appId}`), + +}; + + + +// ============================================================ + +// Token相关API + +// ============================================================ + +export const tokenAPI = { + + getOverview: () => get('/token/overview'), + + getRankings: () => get('/token/rankings'), + + getLogs: (params?: any) => { + + const sp = new URLSearchParams(); + + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + + const qs = sp.toString(); + + return get(`/token/logs${qs ? `?${qs}` : ''}`); + + }, + + getBalance: () => get('/token/balance'), + + recharge: (amount: number) => post('/token/recharge', { amount }), + +}; + + + +// ============================================================ + +// 付费相关API + +// ============================================================ + +export const paymentAPI = { + + getPlans: () => get('/payment/plans'), + + createOrder: (data: any) => post('/payment/orders', data), + + getOrders: (status?: string) => get(`/payment/orders${status ? `?status=${status}` : ''}`), + + getOrder: (id: string) => get(`/payment/orders/${id}`), + + payOrder: (id: string, method?: string) => post(`/payment/orders/${id}/pay`, { payment_method: method }), + + getCoupons: () => get('/payment/coupons'), + + useCoupon: (code: string) => post('/payment/coupons/use', { code }), + + getServices: () => get('/payment/services'), + + createCustomOrder: (data: any) => post('/payment/custom-services', data), + + createServiceRequest: (data: any) => post('/payment/service-requests', data), + +}; + + + +// ============================================================ + +// 个人中心相关API + +// ============================================================ + +export const profileAPI = { + + getProfile: () => get('/profile/'), + + updateProfile: (data: any) => put('/profile/', data), + + updatePassword: (data: { old_password: string; new_password: string }) => + + put('/profile/password', data), + + getLoginLogs: () => get('/profile/login-logs'), + + getApiKeys: () => get('/profile/api-keys'), + + createApiKey: (name?: string) => post('/profile/api-keys', { name }), + + updateApiKey: (id: string, status: string) => put(`/profile/api-keys/${id}`, { status }), + + getMembership: () => get('/profile/membership'), + + getPurchasedApps: () => get('/profile/purchased-apps'), + +}; + + + +// ============================================================ + +// 通知相关API + +// ============================================================ + +export const notificationAPI = { + + getNotifications: (params?: { type?: string; page?: number }) => { + + const sp = new URLSearchParams(); + + if (params?.type) sp.set('type', params.type); + + if (params?.page) sp.set('page', String(params.page)); + + const qs = sp.toString(); + + return get(`/notifications/${qs ? `?${qs}` : ''}`); + + }, + + markRead: (id: string) => put(`/notifications/${id}/read`), + + markAllRead: () => put('/notifications/read-all'), + + getUnreadCount: () => get('/notifications/unread-count'), + +}; + + + +// ============================================================ + +// 资产管理相关API + +// ============================================================ + +export const assetsAPI = { + + getAssets: (category?: string) => + + get(`/assets/${category ? `?category=${category}` : ''}`), + + createAsset: (data: any) => post('/assets/', data), + + getAsset: (id: string) => get(`/assets/${id}`), + + updateAsset: (id: string, data: any) => put(`/assets/${id}`, data), + + deleteAsset: (id: string) => del(`/assets/${id}`), + + getCategories: () => get('/assets/categories'), + + getNovelChapters: (novelId: string) => get(`/assets/novels/${novelId}/chapters`), + + addChapter: (novelId: string, data: any) => post(`/assets/novels/${novelId}/chapters`, data), + +}; + + + +// ============================================================ + +// 后台管理API + +// ============================================================ + +export const adminAPI = { + + // 仪表盘 + + getStats: () => get('/admin/dashboard/stats'), + + getRecentLogs: () => get('/admin/dashboard/recent-logs'), + + getRecentAudit: () => get('/admin/dashboard/recent-audit'), + + getDailyStats: (days?: number) => get<{ days: number; data: Array<{ date: string; tokens: number }> }>(`/admin/dashboard/daily-stats${days ? `?days=${days}` : ''}`), + + // 用户管理 + + getUsers: (params?: any) => { + + const sp = new URLSearchParams(); + + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + + return get(`/admin/users/${sp.toString() ? `?${sp.toString()}` : ''}`); + + }, + + getUser: (id: string) => get(`/admin/users/${id}`), + + banUser: (id: string) => put(`/admin/users/${id}/ban`), + + unbanUser: (id: string) => put(`/admin/users/${id}/unban`), + + getRegisterSettings: () => get('/admin/users/register-settings'), + + updateRegisterSettings: (data: any) => put('/admin/users/register-settings', data), + + // 模型管理 + + getProviders: () => get('/admin/models/providers'), + + addProvider: (data: any) => post('/admin/models/providers', data), + + updateProvider: (id: string, data: any) => put(`/admin/models/providers/${id}`, data), + + deleteProvider: (id: string) => del(`/admin/models/providers/${id}`), + + fetchModels: (id: string) => post(`/admin/models/providers/${id}/fetch-models`), + + addModels: (id: string, modelIds: string[]) => post(`/admin/models/providers/${id}/add-models`, { model_ids: modelIds }), + + getProtocols: () => get('/admin/models/protocols'), + + getAdminModels: (params?: any) => { + + const sp = new URLSearchParams(); + + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + + return get(`/admin/models/${sp.toString() ? `?${sp.toString()}` : ''}`); + + }, + + addModel: (data: any) => post('/admin/models/', data), + + updateModel: (id: string, data: any) => put(`/admin/models/${id}`, data), + + deleteModel: (id: string) => del(`/admin/models/${id}`), + + toggleModel: (id: string, isEnabled: boolean) => put(`/admin/models/${id}/toggle?is_enabled=${isEnabled}`), + + setDefaultModel: (id: string) => put(`/admin/models/${id}/set-default`), + + // 按能力默认模型(Auto 自动模式使用) + getCapabilities: () => get('/admin/models/capabilities'), + getCapabilityDefaults: () => get('/admin/models/capability-defaults'), + setCapabilityDefault: (capability: string, modelId: string | null) => + put('/admin/models/capability-defaults', { capability, model_id: modelId }), + + // Phase 2: Enhanced model management + + testModelConnection: (id: string) => post(`/admin/models/${id}/test-connection`), + + testAllModels: () => post('/admin/models/test-all'), + + fetchProviderModels: (id: string) => post(`/admin/models/providers/${id}/fetch-models`), + + createAdminModel: (data: any) => post('/admin/models/', data), + + updateAdminModel: (id: string, data: any) => put(`/admin/models/${id}`, data), + + deleteAdminModel: (id: string) => del(`/admin/models/${id}`), + + getModelStats: () => get('/admin/models/stats'), + + // 网站设置 + + getSiteSettings: () => get('/admin/settings/site'), + + updateSiteSettings: (data: any) => put('/admin/settings/site', data), + + getEmailConfig: () => get('/admin/settings/email'), + + updateEmailConfig: (data: any) => put('/admin/settings/email', data), + + testEmail: (params?: { test_to?: string; test_subject?: string; test_body?: string }) => { + + const qs = new URLSearchParams(); + + if (params?.test_to) qs.set('test_to', params.test_to); + + if (params?.test_subject) qs.set('test_subject', params.test_subject); + + if (params?.test_body) qs.set('test_body', params.test_body); + + const qstr = qs.toString(); + + return post(`/admin/settings/email/test${qstr ? `?${qstr}` : ''}`); + + }, + + generateEmailContent: (topic: string) => post(`/admin/settings/email/generate-content?topic=${encodeURIComponent(topic)}`), + + getSmsConfig: () => get('/admin/settings/sms'), + + updateSmsConfig: (data: any) => put('/admin/settings/sms', data), + + testSms: () => post('/admin/settings/sms/test'), + + getSmsBalance: () => post('/admin/settings/sms/balance'), + + // 支付配置 + + getPaymentConfigs: () => get('/admin/payment/configs'), + + updatePaymentConfig: (id: string, data: any) => put(`/admin/payment/configs/${id}`, data), + + getAdminOrders: (params?: any) => { + + const sp = new URLSearchParams(); + + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + + return get(`/admin/payment/orders${sp.toString() ? `?${sp.toString()}` : ''}`); + + }, + + refundOrder: (id: string) => put(`/admin/payment/orders/${id}/refund`), + + // 增值服务 + + getServices: (params?: any) => { + + const sp = new URLSearchParams(); + + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + + return get(`/admin/services/${sp.toString() ? `?${sp.toString()}` : ''}`); + + }, + + addService: (data: any) => post('/admin/services/', data), + + updateService: (id: string, data: any) => put(`/admin/services/${id}`, data), + + deleteService: (id: string) => del(`/admin/services/${id}`), + + getCustomOrders: (status?: string) => get(`/admin/services/custom-orders${status ? `?status=${status}` : ''}`), + + updateCustomOrder: (id: string, data: any) => put(`/admin/services/custom-orders/${id}`, data), + + getCustomRequests: (params?: any) => { + + const sp = new URLSearchParams(); + + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + + return get(`/admin/services/custom-requests${sp.toString() ? `?${sp.toString()}` : ''}`); + + }, + + updateCustomRequest: (id: string, data: any) => put(`/admin/services/custom-requests/${id}`, data), + + getAvailableAgents: () => get('/admin/services/agents'), + + assignAgentToService: (id: string, agentId: string) => put(`/admin/services/${id}/agent?agent_id=${agentId}`), + + // 会员管理(增强版) + + getPlans: () => get('/admin/membership/plans'), + + addPlan: (params: string) => post(`/admin/membership/plans${params}`), + + updatePlan: (id: string, params: string) => put(`/admin/membership/plans/${id}${params}`), + + deletePlan: (id: string) => del(`/admin/membership/plans/${id}`), + + getSubscriptions: (page?: number) => get(`/admin/membership/subscriptions${page ? `?page=${page}` : ''}`), + + getAutomationRules: () => get('/admin/membership/automation-rules'), + + getBenefitsComparison: () => get('/admin/membership/benefits-comparison'), + + getMembershipConfigs: () => get('/admin/membership/global-configs'), + + updateMembershipConfig: (key: string, params: string) => put(`/admin/membership/global-configs/${key}${params}`), + + getUserMemberships: (page?: number, planId?: string, status?: string) => { + + const p = new URLSearchParams(); + + if (page) p.append('page', String(page)); + + if (planId) p.append('plan_id', planId); + + if (status) p.append('status', status); + + const qs = p.toString(); + + return get(`/admin/membership/user-memberships${qs ? `?${qs}` : ''}`); + + }, + + assignPlan: (userId: string, planId: string, days: number) => put(`/admin/membership/users/${userId}/assign-plan?plan_id=${planId}&duration_days=${days}`), + + getPaymentLogs: (params?: any) => { + + const sp = new URLSearchParams(); + + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + + return get(`/admin/logs/payment-logs${sp.toString() ? `?${sp.toString()}` : ''}`); + + }, + + getAuditLogs: (params?: any) => { + + const sp = new URLSearchParams(); + + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + + return get(`/admin/logs/audit-logs${sp.toString() ? `?${sp.toString()}` : ''}`); + + }, + + // 新增:日志统计 + + getLogStats: () => get('/admin/logs/stats'), + + // 新增:API调用统计 + + getApiStats: (params?: any) => { + + const sp = new URLSearchParams(); + + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + + return get(`/admin/logs/api-stats${sp.toString() ? `?${sp.toString()}` : ''}`); + + }, + + // 新增:慢请求记录 + + getSlowRequests: (params?: any) => { + + const sp = new URLSearchParams(); + + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + + return get(`/admin/logs/slow-requests${sp.toString() ? `?${sp.toString()}` : ''}`); + + }, + + // 新增:操作类型枚举 + + getActionTypes: () => get('/admin/logs/action-types'), + + // 新增:审计日志导出 + + exportAuditLogs: (params?: any) => { + + const sp = new URLSearchParams(); + + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + + return get(`/admin/logs/export/audit-logs${sp.toString() ? `?${sp.toString()}` : ''}`); + + }, + + exportOperationLogs: (params?: any) => { + + const sp = new URLSearchParams(); + + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + + return get(`/admin/logs/export/operations${sp.toString() ? `?${sp.toString()}` : ''}`); + + }, + + exportTokenLogs: (params?: any) => { + + const sp = new URLSearchParams(); + + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + + return get(`/admin/logs/export/token-logs${sp.toString() ? `?${sp.toString()}` : ''}`); + + }, + + getRiskRules: () => get('/admin/logs/risk-rules'), + + addRiskRule: (data: any) => post('/admin/logs/risk-rules', data), + + updateRiskRule: (id: string, data: any) => put(`/admin/logs/risk-rules/${id}`, data), + + deleteRiskRule: (id: string) => del(`/admin/logs/risk-rules/${id}`), + + // 技能管理 (Phase 2.2) + + getAdminSkills: (params?: any) => { + + const sp = new URLSearchParams(); + + if (params) Object.entries(params).forEach(([k, v]) => { if (v !== undefined && v !== null && v !== '') sp.set(k, String(v)); }); + + return get(`/admin/skills/${sp.toString() ? `?${sp.toString()}` : ''}`); + + }, + + getAdminSkill: (id: string) => get(`/admin/skills/${id}`), + + createAdminSkill: (data: any) => post('/admin/skills/', data), + + updateAdminSkill: (id: string, data: any) => put(`/admin/skills/${id}`, data), + + deleteAdminSkill: (id: string) => del(`/admin/skills/${id}`), + + changeAdminSkillState: (id: string, data: any) => put(`/admin/skills/${id}/state`, data), + + getAdminSkillVersions: (id: string) => get(`/admin/skills/${id}/versions`), + + rollbackAdminSkill: (id: string, data: any) => post(`/admin/skills/${id}/rollback`, data), + + publishAdminSkill: (id: string, data: any) => post(`/admin/skills/${id}/publish`, data), + + unpublishAdminSkill: (id: string) => post(`/admin/skills/${id}/unpublish`), + + getAdminSkillsStats: () => get('/admin/skills/stats/overview'), + +// 智能体管理 (Phase 2.3) + + getAdminAgents: (params?: any) => { + + const query = params ? '?' + Object.entries(params).filter(([_, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`).join('&') : ''; + + return get(`/admin/agents/${query}`); + + }, + + getAdminAgent: (id: string) => get(`/admin/agents/${id}`), + + createAdminAgent: (data: any) => post('/admin/agents/', data), + + updateAdminAgent: (id: string, data: any) => put(`/admin/agents/${id}`, data), + + deleteAdminAgent: (id: string) => del(`/admin/agents/${id}`), + + toggleAdminAgent: (id: string, isActive: boolean) => put(`/admin/agents/${id}/toggle`, { is_active: isActive }), + + getAdminAgentsStats: () => get('/admin/agents/stats/overview'), + + getAdminAgentReviews: (id: string) => get(`/admin/agents/${id}/reviews`), + + getAdminAvailableSkills: (params?: any) => { + + const query = params ? '?' + Object.entries(params).filter(([_, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`).join('&') : ''; + + return get(`/admin/agents/available/skills${query}`); + + }, + + getAdminAvailableModels: (params?: any) => { + + const query = params ? '?' + Object.entries(params).filter(([_, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`).join('&') : ''; + + return get(`/admin/agents/available/models${query}`); + + }, + + // 知识库管理 (Phase 2.4) + + getAdminKnowledgeStats: () => get('/admin/knowledge/stats/overview'), + + getAdminKnowledge: (params?: any) => { + + const query = params ? '?' + Object.entries(params).filter(([_, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`).join('&') : ''; + + return get(`/admin/knowledge/${query}`); + + }, + + getAdminKnowledgeDetail: (id: string) => get(`/admin/knowledge/${id}`), + + createAdminKnowledge: (data: any) => post('/admin/knowledge/', data), + + updateAdminKnowledge: (id: string, data: any) => put(`/admin/knowledge/${id}`, data), + + deleteAdminKnowledge: (id: string) => del(`/admin/knowledge/${id}`), + + toggleAdminKnowledge: (id: string, isActive: boolean) => put(`/admin/knowledge/${id}/toggle`, { is_active: isActive }), + + getAdminKnowledgeDocs: (kbId: string, params?: any) => { + + const query = params ? '?' + Object.entries(params).filter(([_, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`).join('&') : ''; + + return get(`/admin/knowledge/${kbId}/documents${query}`); + + }, + + deleteAdminKnowledgeDoc: (kbId: string, docId: string) => del(`/admin/knowledge/${kbId}/documents/${docId}`), + + getAdminKnowledgeExtServices: () => get('/admin/knowledge/external/services'), + + createAdminKnowledgeExtService: (data: any) => post('/admin/knowledge/external/services', data), + + updateAdminKnowledgeExtService: (id: string, data: any) => put(`/admin/knowledge/external/services/${id}`, data), + + deleteAdminKnowledgeExtService: (id: string) => del(`/admin/knowledge/external/services/${id}`), + + checkAdminKnowledgeExtStatus: (id: string) => get(`/admin/knowledge/external/services/${id}/status`), + + syncAdminKnowledge: (kbId: string, data: any) => post(`/admin/knowledge/external/sync/${kbId}`, data), + + batchDeleteAdminKnowledge: (ids: string[]) => post('/admin/knowledge/batch/delete', ids), + + // OSS存储配置 (Phase 3.1) + + getAssetStats: () => get('/admin/assets/stats'), + + getAdminAssets: (query: string) => get(`/admin/assets/?${query}`), + + deleteAdminAsset: (id: string) => del(`/admin/assets/${id}`), + + getAssetActivity: () => get('/admin/assets/activity'), + + getAdminStorageStats: () => get('/admin/storage/stats/overview'), + + getAdminStorageConfigs: (params?: any) => { + + const query = params ? '?' + Object.entries(params).filter(([_, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`).join('&') : ''; + + return get(`/admin/storage/${query}`); + + }, + + getAdminStorageConfig: (id: string) => get(`/admin/storage/${id}`), + + createAdminStorageConfig: (data: any) => post('/admin/storage/', data), + + updateAdminStorageConfig: (id: string, data: any) => put(`/admin/storage/${id}`, data), + + deleteAdminStorageConfig: (id: string) => del(`/admin/storage/${id}`), + + toggleAdminStorageConfig: (id: string) => put(`/admin/storage/${id}/toggle`), + + setDefaultAdminStorageConfig: (id: string) => put(`/admin/storage/${id}/set-default`), + + testAdminStorageConfig: (data: any) => post('/admin/storage/test', data), + + getAdminStorageUsage: (id: string) => get(`/admin/storage/${id}/usage`), + + // Storage Quota APIs (Phase C5) + + getStorageQuotaOverview: () => get('/admin/storage/quota/overview'), + + getStorageQuotaUsers: (params?: any) => { + + if (params) { + + const query = Object.entries(params).map(([k, v]) => v !== undefined && v !== '' && v !== null ? `${k}=${encodeURIComponent(String(v))}` : '').filter(Boolean).join('&'); + + return get(`/admin/storage/quota/users${query ? '?' + query : ''}`); + + } + + return get('/admin/storage/quota/users'); + + }, + + getStorageQuotaUser: (userId: string) => get(`/admin/storage/quota/users/${userId}`), + + adjustStorageQuota: (userId: string, data: any) => put(`/admin/storage/quota/users/${userId}`, data), + + recalcStorageQuota: (userId: string) => post(`/admin/storage/quota/recalculate/${userId}`, {}), + + initAllStorageQuotas: () => post('/admin/storage/quota/init-all', {}), + + + + // 自动化任务调度 (Phase 3.2) + + getAdminAutomationStats: () => get('/admin/automation/stats/overview'), + + getAdminAutomationTasks: (params?: any) => { + + const query = params ? '?' + Object.entries(params).filter(([_, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`).join('&') : ''; + + return get(`/admin/automation/${query}`); + + }, + + getAdminAutomationTask: (id: string) => get(`/admin/automation/${id}`), + + createAdminAutomationTask: (data: any) => post('/admin/automation/', data), + + updateAdminAutomationTask: (id: string, data: any) => put(`/admin/automation/${id}`, data), + + deleteAdminAutomationTask: (id: string) => del(`/admin/automation/${id}`), + + toggleAdminAutomationTask: (id: string) => put(`/admin/automation/${id}/toggle`), + + executeAdminAutomationTask: (id: string) => post(`/admin/automation/${id}/execute`), + + getAdminAutomationTaskHistory: (id: string, params?: any) => { + + const query = params ? '?' + Object.entries(params).filter(([_, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`).join('&') : ''; + + return get(`/admin/automation/${id}/history${query}`); + + }, + + // MCP Server Management + + getAdminMcpStats: () => get('/admin/mcp/stats/overview'), + + getAdminMcpServers: (params?: any) => { + + const query = new URLSearchParams(params).toString(); + + return get(`/admin/mcp/${query ? '?' + query : ''}`); + + }, + + getAdminMcpServer: (id: string) => get(`/admin/mcp/${id}`), + + createAdminMcpServer: (data: any) => post('/admin/mcp/', data), + + updateAdminMcpServer: (id: string, data: any) => put(`/admin/mcp/${id}`, data), + + deleteAdminMcpServer: (id: string) => del(`/admin/mcp/${id}`), + + toggleAdminMcpServer: (id: string) => put(`/admin/mcp/${id}/toggle`), + + testAdminMcpServer: (id: string, data?: any) => post(`/admin/mcp/${id}/test`, data || {}), + + getAdminMcpServerTools: (id: string) => get(`/admin/mcp/${id}/tools`), + + getAdminMcpServerResources: (id: string) => get(`/admin/mcp/${id}/resources`), + + // Developer API Management + + getAdminDeveloperStats: () => get('/admin/developer/stats/overview'), + + getAdminApiKeys: (params?: any) => { + + const sp = new URLSearchParams(); + + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + + return get(`/admin/developer/api-keys${sp.toString() ? '?' + sp.toString() : ''}`); + + }, + + createAdminApiKey: (data: any) => post('/admin/developer/api-keys/', data), + + updateAdminApiKey: (id: string, data: any) => put(`/admin/developer/api-keys/${id}`, data), + + revokeAdminApiKey: (id: string) => post(`/admin/developer/api-keys/${id}/revoke`), + + deleteAdminApiKey: (id: string) => del(`/admin/developer/api-keys/${id}`), + + getAdminOpenApiClients: (params?: any) => { + + const sp = new URLSearchParams(); + + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + + return get(`/admin/developer/openapi-clients${sp.toString() ? '?' + sp.toString() : ''}`); + + }, + + createAdminOpenApiClient: (data: any) => post('/admin/developer/openapi-clients/', data), + + updateAdminOpenApiClient: (id: string, data: any) => put(`/admin/developer/openapi-clients/${id}`, data), + + resetAdminOpenApiClientSecret: (id: string) => post(`/admin/developer/openapi-clients/${id}/reset-secret`), + + deleteAdminOpenApiClient: (id: string) => del(`/admin/developer/openapi-clients/${id}`), + + getAdminCallStats: (params?: any) => { + + const sp = new URLSearchParams(); + + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + + return get(`/admin/developer/call-stats${sp.toString() ? '?' + sp.toString() : ''}`); + + }, + + getAdminCallStatsOverview: () => get('/admin/developer/call-stats/overview'), + + getAdminSlowLogs: (params?: any) => { + + const sp = new URLSearchParams(); + + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + + return get(`/admin/developer/slow-logs${sp.toString() ? '?' + sp.toString() : ''}`); + + }, + + clearAdminSlowLogs: () => del('/admin/developer/slow-logs'), + + getAdminSlowLogsStats: () => get('/admin/developer/slow-logs/stats'), + + // AI App Management + + getAIAppStats: () => get('/admin/apps/stats/overview'), + + getAIApps: (params?: string) => get('/admin/apps/apps' + (params ? '?' + params : '')), + + getAIApp: (id: string) => get('/admin/apps/apps/' + id), + + createAIApp: (data: any) => post('/admin/apps/apps', data), + + updateAIApp: (id: string, data: any) => put('/admin/apps/apps/' + id, data), + + deleteAIApp: (id: string) => del('/admin/apps/apps/' + id), + + aiAppLifecycle: (id: string, action: string) => post('/admin/apps/apps/' + id + '/' + action, {}), + + getAIAppVersions: (id: string) => get('/admin/apps/apps/' + id + '/versions'), + + getAIAppAnalytics: (id: string) => get('/admin/apps/apps/' + id + '/analytics'), + + getAIAppTemplates: () => get('/admin/apps/templates'), + + createAIAppTemplate: (data: any) => post('/admin/apps/templates', data), + + createAIAppFromTemplate: (templateId: string, data: any) => post('/admin/apps/apps/from-template/' + templateId, data), + + getAIAppCategories: () => get('/admin/apps/categories'), + + // Logo 上传 + + uploadLogo: (formData: FormData) => { + + const token = localStorage.getItem('access_token'); + + return fetch(`/app/api/admin/settings/logo/upload`, { + + method: 'POST', + + headers: { 'Authorization': `Bearer ${token}` }, + + body: formData, + + }).then(res => res.json()); + + }, + + + + // MCP Market (Phase G1) + + getMcpMarketStats: () => + + get('/admin/mcp-market/stats'), + + getMcpTools: (params?: Record) => + + get('/admin/mcp-market/?' + new URLSearchParams(params || {}).toString()), + + getMcpToolDetail: (id: string) => + + get('/admin/mcp-market/' + id), + + createMcpTool: (data: any) => + + post('/admin/mcp-market/', data), + + updateMcpTool: (id: string, data: any) => + + put('/admin/mcp-market/' + id, data), + + deleteMcpTool: (id: string) => + + del('/admin/mcp-market/' + id), + + scanMcpTool: (id: string) => + + post('/admin/mcp-market/' + id + '/security-scan'), + + updateMcpToolSecurity: (id: string, data: any) => + + put('/admin/mcp-market/' + id + '/security-status', data), + + regenMcpDeepLink: (id: string) => + + post('/admin/mcp-market/' + id + '/deep-link'), + + getMcpManagedServers: () => + + get('/admin/mcp-market/managed/servers'), + + seedMcpTools: () => + + post('/admin/mcp-market/seed'), + + + + // Code Execution Sandbox (Phase G2) + + getCodeExecStats: () => get('/admin/code-exec/stats'), + + getCodeExecRuntimes: () => get('/admin/code-exec/runtimes'), + + executeCode: (data: { language: string; code: string; stdin?: string; timeout?: number }) => + + post('/admin/code-exec/execute', data), + + getCodeExecTasks: (params?: any) => { + + const sp = new URLSearchParams(); + + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + + return get(`/admin/code-exec/tasks${sp.toString() ? '?' + sp.toString() : ''}`); + + }, + + getCodeExecTask: (id: string) => get(`/admin/code-exec/tasks/${id}`), + + deleteCodeExecTask: (id: string) => del(`/admin/code-exec/tasks/${id}`), + + + + + + // Distribution Builder (Phase G4) + + getDistributionStats: () => get('/admin/distributions/stats'), + + getDistributions: (params?: any) => { + + const sp = new URLSearchParams(); + + if (params) Object.entries(params).forEach(([k, v]: [string, any]) => { if (v) sp.set(k, String(v)); }); + + return get('/admin/distributions' + (sp.toString() ? '?' + sp.toString() : '')); + + }, + + getDistributionTemplates: () => get('/admin/distributions/templates'), + + createDistribution: (data: any) => post('/admin/distributions', data), + + getDistribution: (id: string) => get('/admin/distributions/' + id), + + updateDistribution: (id: string, data: any) => put('/admin/distributions/' + id, data), + + deleteDistribution: (id: string) => del('/admin/distributions/' + id), + + getDistributionLicenses: (params?: any) => { + + const sp = new URLSearchParams(); + + if (params) Object.entries(params).forEach(([k, v]: [string, any]) => { if (v) sp.set(k, String(v)); }); + + return get('/admin/distributions/licenses' + (sp.toString() ? '?' + sp.toString() : '')); + + }, + + createDistributionLicense: (distroId: string, data: any) => post('/admin/distributions/' + distroId + '/licenses', data), + + getDistributionLicense: (id: string) => get('/admin/distributions/licenses/' + id), + + revokeDistributionLicense: (id: string) => post('/admin/distributions/licenses/' + id + '/revoke'), + + // ===== Ads Management ===== + + getAds: () => get('/admin/ads/'), + + addAd: (data: any) => post('/admin/ads/', data), + + updateAd: (id: string, data: any) => put(`/admin/ads/${id}`, data), + + deleteAd: (id: string) => del(`/admin/ads/${id}`), + + + + // ===== Market Admin (extended) ===== + + getMarketOverview: () => get('/admin/market/overview'), + + getTopApps: (metric?: string, limit?: number) => get(`/admin/market/top-apps${metric ? '?metric=' + metric : ''}${limit ? (metric ? '&' : '?') + 'limit=' + limit : ''}`), + + getAdminMarketApps: (params?: string) => get(`/admin/market/apps${params ? '?' + params : ''}`), + + getPendingApps: () => get('/admin/market/pending'), + + getCoupons: () => get('/admin/market/coupons'), + + getRecentReviews: () => get('/admin/market/reviews'), + + approveApp: (id: string) => post(`/admin/market/apps/${id}/approve`), + + rejectApp: (id: string) => post(`/admin/market/apps/${id}/reject`), + + setFeaturedApp: (id: string, featured: boolean, sortOrder?: number) => put(`/admin/market/apps/${id}/featured`, { featured, sort_order: sortOrder }), + + deleteMarketApp: (id: string) => del(`/admin/market/apps/${id}`), + + deleteCoupon: (id: string) => del(`/admin/market/coupons/${id}`), + + updateMarketApp: (id: string, data: any) => put(`/admin/market/apps/${id}`, data), + + updateCoupon: (id: string, data: any) => put(`/admin/market/coupons/${id}`, data), + + addCoupon: (data: any) => post('/admin/market/coupons', data), + + + + // ===== Membership (extended) ===== + + getMembershipStats: () => get('/admin/membership/stats'), + + revokeMembership: (userId: string) => post(`/admin/membership/users/${userId}/revoke`), + + + + // ===== Logs (extended) ===== + + getOperationLogs: (params?: any) => { + + const sp = new URLSearchParams(); + + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + + return get(`/admin/logs/operation-logs${sp.toString() ? '?' + sp.toString() : ''}`); + + }, + + getTokenLogs: (params?: any) => { + + const sp = new URLSearchParams(); + + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + + return get(`/admin/logs/token-logs${sp.toString() ? '?' + sp.toString() : ''}`); + + }, + + + + // ===== Analytics ===== + + getAnalyticsOverview: (days?: number) => get(`/admin/analytics/overview${days ? '?days=' + days : ''}`), + + getAnalyticsApiTrends: (days?: number) => get(`/admin/analytics/trends/api-calls${days ? '?days=' + days : ''}`), + + getAnalyticsTokenTrends: (days?: number) => get(`/admin/analytics/trends/tokens${days ? '?days=' + days : ''}`), + + getAnalyticsUserTrends: (days?: number) => get(`/admin/analytics/trends/users${days ? '?days=' + days : ''}`), + + getAnalyticsRevenueTrends: (days?: number) => get(`/admin/analytics/trends/revenue${days ? '?days=' + days : ''}`), + + getAnalyticsModelUsage: (days?: number) => get(`/admin/analytics/models/usage${days ? '?days=' + days : ''}`), + + + + // ===== Billing ===== + + getBillingRules: () => get('/admin/billing/rules'), + + getBillingPackages: () => get('/admin/billing/packages'), + + getBillingTransactions: (page?: number) => get(`/admin/billing/transactions${page ? '?page=' + page : ''}`), + + getBillingStats: () => get('/admin/billing/stats/overview'), + + + + // ===== Tenant ===== + + getTenants: (page?: number) => get(`/admin/tenant/list${page ? '?page=' + page : ''}`), + + getTenantStats: () => get('/admin/tenant/stats/overview'), + + getTenantMembers: (tenantId: string) => get(`/admin/tenant/${tenantId}/members`), + + createTenant: (params: string) => post(`/admin/tenant/create${params}`), + + + +}; + +// ===== AI自进化API ===== + +export const evolutionAPI = { + + // 驾驶舱面板 + + getSkillTree: () => get('/evolution/dashboard/skill-tree'), + + getTimeline: (eventType?: string) => + + get('/evolution/dashboard/timeline' + (eventType ? `?event_type=${eventType}` : '')), + + getStatistics: () => get('/evolution/dashboard/statistics'), + + getProfile: () => get('/evolution/dashboard/profile'), + + + + // 技能管理 + + getSkills: (params?: { state?: string; keyword?: string; sort_by?: string; order?: string }) => { + + const query = new URLSearchParams(); + + if (params?.state) query.append('state', params.state); + + if (params?.keyword) query.append('keyword', params.keyword); + + if (params?.sort_by) query.append('sort_by', params.sort_by); + + if (params?.order) query.append('order', params.order); + + const qs = query.toString(); + + return get('/evolution/skills' + (qs ? `?${qs}` : '')); + + }, + + getSkillDetail: (name: string) => get(`/evolution/skills/${encodeURIComponent(name)}`), + + updateSkill: (name: string, data: { description?: string; tags?: string[]; category?: string }) => + + put(`/evolution/skills/${encodeURIComponent(name)}`, data), + + deleteSkill: (name: string) => del(`/evolution/skills/${encodeURIComponent(name)}`), + + freezeSkill: (name: string) => post(`/evolution/skills/${encodeURIComponent(name)}/freeze`), + + unfreezeSkill: (name: string) => post(`/evolution/skills/${encodeURIComponent(name)}/unfreeze`), + + getSkillHistory: (name: string) => get(`/evolution/skills/${encodeURIComponent(name)}/history`), + + getSkillTrajectories: (name: string) => get(`/evolution/skills/${encodeURIComponent(name)}/trajectories`), + + + + // 事件 + + getEvents: (params?: { event_type?: string; skill_name?: string; limit?: number }) => { + + const query = new URLSearchParams(); + + if (params?.event_type) query.append('event_type', params.event_type); + + if (params?.skill_name) query.append('skill_name', params.skill_name); + + if (params?.limit) query.append('limit', String(params.limit)); + + const qs = query.toString(); + + return get('/evolution/events' + (qs ? `?${qs}` : '')); + + }, + + + + // 轨迹提交 + + submitTrajectory: (data: { conversation_id: string; messages: any[] }) => + + post('/evolution/trajectory/submit', data), + + + + // v2: 技能回滚 + + rollbackSkill: (skillName: string, version: number) => + + post(`/evolution/skills/${encodeURIComponent(skillName)}/rollback/${version}`), + + + + // v2: 发布到市场 + + publishSkill: (skillName: string, data: { price: number; market_description: string }) => + + post(`/evolution/skills/${encodeURIComponent(skillName)}/publish`, data), + + + + // v2: 团队共享 + + shareToTeam: (skillName: string, teamId: string) => + + post(`/evolution/skills/${encodeURIComponent(skillName)}/share-to-team`, { team_id: teamId }), + + + + // v2: 流水线状态 + + getPipelineStatus: () => get('/evolution/pipeline/status'), + + + + // v2: 手动触发流水线 + + triggerPipeline: () => post('/evolution/pipeline/trigger'), + + + + // v2: Watcher状态 + + getWatcherStatus: () => get('/evolution/watcher/status'), + + + + // v2: 进化报告 + + getReports: (reportType?: string) => { + + const qs = reportType ? `?report_type=${reportType}` : ''; + + return get(`/evolution/reports${qs}`); + + }, + + getReportDetail: (reportId: string) => get(`/evolution/reports/${reportId}`), + + + + + + // Workflow (Phase 3.6) + + getWorkflows: (params?: Record) => + + get('/admin/workflows/?' + new URLSearchParams(params || {}).toString()), + + getWorkflow: (id: string) => + + get(`/admin/workflows/${id}`), + + getWorkflowStats: () => + + get('/admin/workflows/stats'), + + getWorkflowTemplates: () => + + get('/admin/workflows/templates'), + + createWorkflow: (data: any) => + + post('/admin/workflows/', data), + + updateWorkflow: (id: string, data: any) => + + put(`/admin/workflows/${id}`, data), + + deleteWorkflow: (id: string) => + + del(`/admin/workflows/${id}`), + + toggleWorkflow: (id: string, isActive: boolean) => + + post(`/admin/workflows/${id}/toggle?is_active=${isActive}`), + + executeWorkflow: (id: string, inputText: string) => + + post(`/admin/workflows/${id}/execute`, { input_text: inputText }), + + + + + + // Privacy & Local Agent (Phase G5) + + getPrivacyStats: () => get('/admin/privacy/stats'), + + getPrivacyConfig: () => get('/admin/privacy/config'), + + updatePrivacyConfig: (params: any) => { + + const sp = new URLSearchParams(); + + Object.entries(params).forEach(([k, v]) => { if (v != null) sp.append(k, String(v)); }); + + return put('/admin/privacy/config?' + sp.toString()); + + }, + + switchPrivacyMode: (mode: string) => post('/admin/privacy/switch-mode?mode=' + encodeURIComponent(mode)), + + getPrivacyLicenses: (params?: any) => { + + const sp = new URLSearchParams(); + + if (params?.status) sp.append('status', params.status); + + return get('/admin/privacy/licenses' + (sp.toString() ? '?' + sp.toString() : '')); + + }, + + generateOfflineLicense: (data: any) => { + + const sp = new URLSearchParams(); + + if (data.agent_id) sp.append('agent_id', data.agent_id); + + sp.append('max_offline_days', String(data.max_offline_days || 7)); + + return post('/admin/privacy/licenses/generate?' + sp.toString()); + + }, + + revokePrivacyLicense: (id: number) => post('/admin/privacy/licenses/' + id + '/revoke'), + + validatePrivacyLicense: (licenseKey: string) => { + + const sp = new URLSearchParams(); + + sp.append('license_key', licenseKey); + + return post('/admin/privacy/licenses/validate?' + sp.toString()); + + }, + + getKbSyncStates: () => get('/admin/privacy/kb-sync'), + + triggerKbSync: (kbId: number, direction: string) => { + + const sp = new URLSearchParams(); + + sp.append('knowledge_base_id', String(kbId)); + + sp.append('sync_direction', direction); + + return post('/admin/privacy/kb-sync/trigger?' + sp.toString()); + + }, + + getLocalAgents: () => get('/admin/privacy/local-agents'), + + getPrivacyUsageStats: () => get('/admin/privacy/usage/stats'), + + getBridgeConfig: () => get('/admin/privacy/bridge-config'), + + + + // IM Bot Management (Phase 4) + + getImBotStats: () => get('/admin/im-bot/stats'), + + getImBotDashboard: () => get('/admin/im-bot/dashboard'), + + getImBotChannels: (params?: string) => get(`/admin/im-bot/channels${params || ''}`), + + getImBotChannelDetail: (id: string) => get(`/admin/im-bot/channels/${id}`), + + testImBotChannel: (id: string) => post(`/admin/im-bot/channels/${id}/test`), + + activateImBotChannel: (id: string) => post(`/admin/im-bot/channels/${id}/activate`), + + deactivateImBotChannel: (id: string) => post(`/admin/im-bot/channels/${id}/deactivate`), + + getImBotRoutes: (params?: string) => get(`/admin/im-bot/routes${params || ''}`), + + createImBotRoute: (params: string) => post(`/admin/im-bot/routes${params}`), + + deleteImBotRoute: (id: string) => del(`/admin/im-bot/routes/${id}`), + + // 个人微信(iLink 协议)原生适配器 + + getWechatConfig: () => get('/admin/im-bot/wechat'), + + wechatQrcode: () => post('/admin/im-bot/wechat/qrcode'), + + wechatQrcodeStatus: (params: string) => post(`/admin/im-bot/wechat/qrcode/status${params}`), + + getWechatStatus: () => get('/admin/im-bot/wechat/status'), + + disconnectWechat: () => post('/admin/im-bot/wechat/disconnect'), + + getImBotPlatforms: () => get('/admin/im-bot/platforms'), + + getImBotWebhookUrl: (platform: string) => get(`/admin/im-bot/webhook-url/${platform}`), + + getImBotMessages: (params?: string) => get(`/admin/im-bot/messages${params || ''}`), + + imBotQuickSetup: (params: string) => post(`/admin/im-bot/quick-setup${params}`), + + + + // 推广系统 + + getPromotionStats: () => get('/admin/promotion/stats'), + + getPromotions: (params?: any) => { + + const sp = new URLSearchParams(); + + if (params) Object.entries(params).forEach(([k, v]) => { if (v !== undefined && v !== null && v !== '') sp.set(k, String(v)); }); + + return get(`/admin/promotion/promotions${sp.toString() ? `?${sp.toString()}` : ''}`); + + }, + + createPromotion: (params: any) => { + + const sp = new URLSearchParams(); + + Object.entries(params).forEach(([k, v]) => { if (v !== undefined && v !== null && v !== '') sp.set(k, String(v)); }); + + return post(`/admin/promotion/promotions${sp.toString() ? `?${sp.toString()}` : ''}`); + + }, + + updatePromotion: (id: string, params: any) => { + + const sp = new URLSearchParams(); + + Object.entries(params).forEach(([k, v]) => { if (v !== undefined && v !== null && v !== '') sp.set(k, String(v)); }); + + return put(`/admin/promotion/promotions/${id}${sp.toString() ? `?${sp.toString()}` : ''}`); + + }, + + deletePromotion: (id: string) => del(`/admin/promotion/promotions/${id}`), + + togglePromotion: (id: string) => post(`/admin/promotion/promotions/${id}/toggle`), + + getReferralLinks: (params?: any) => { + + const sp = new URLSearchParams(); + + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + + return get(`/admin/promotion/referral-links${sp.toString() ? `?${sp.toString()}` : ''}`); + + }, + + generateReferralLink: (params: any) => { + + const sp = new URLSearchParams(); + + Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + + return post(`/admin/promotion/referral-links/generate${sp.toString() ? `?${sp.toString()}` : ''}`); + + }, + + getReferralRewards: (params?: any) => { + + const sp = new URLSearchParams(); + + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + + return get(`/admin/promotion/rewards${sp.toString() ? `?${sp.toString()}` : ''}`); + + }, + + approveReward: (id: string) => post(`/admin/promotion/rewards/${id}/approve`), + + markRewardPaid: (id: string, params: any) => { + + const sp = new URLSearchParams(); + + Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + + return post(`/admin/promotion/rewards/${id}/pay${sp.toString() ? `?${sp.toString()}` : ''}`); + + }, + + getPromotionLeaderboard: (params?: any) => { + + const sp = new URLSearchParams(); + + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + + return get(`/admin/promotion/leaderboard${sp.toString() ? `?${sp.toString()}` : ''}`); + + }, + + regenerateReferralCode: (id: string) => post(`/admin/promotion/referral-links/${id}/regenerate`), + + getMyReferrals: () => get('/admin/promotion/my-referrals'), + + getMyRewards: () => get('/admin/promotion/my-rewards'), + + + + // 成就系统 + + getAchievements: (params?: string) => get(`/admin/achievement/achievements${params || ''}`), + + getAchievementDetail: (id: string) => get(`/admin/achievement/achievements/${id}`), + + createAchievement: (params: string) => post(`/admin/achievement/achievements${params}`), + + updateAchievement: (id: string, params: string) => put(`/admin/achievement/achievements/${id}${params}`), + + deleteAchievement: (id: string) => del(`/admin/achievement/achievements/${id}`), + + getUserAchievements: () => get('/admin/achievement/user/achievements'), + + claimAchievementReward: (id: string) => post(`/admin/achievement/user/achievements/${id}/claim`), + + checkAchievementProgress: () => post('/admin/achievement/check-progress'), + + getAchievementLeaderboard: (params?: string) => get(`/admin/achievement/leaderboard${params || ''}`), + + + + // 多租户管理 + + getTenants: (page?: number, keyword?: string, status?: string) => { + + const sp = new URLSearchParams(); + + if (page) sp.set('page', String(page)); + + if (keyword) sp.set('keyword', keyword); + + if (status) sp.set('status', status); + + sp.set('page_size', '20'); + + return get(`/admin/tenant/list${sp.toString() ? `?${sp.toString()}` : ''}`); + + }, + + createTenant: (params: string) => post(`/admin/tenant/create${params}`), + + getTenantDetail: (id: string) => get(`/admin/tenant/${id}`), + + updateTenant: (id: string, params: string) => put(`/admin/tenant/${id}${params}`), + + deleteTenant: (id: string) => del(`/admin/tenant/${id}`), + + getTenantMembers: (tenantId: string, page?: number) => { + + const sp = new URLSearchParams(); + + sp.set('page', String(page || 1)); + + sp.set('page_size', '100'); + + return get(`/admin/tenant/${tenantId}/members?${sp.toString()}`); + + }, + + addTenantMember: (tenantId: string, params: string) => post(`/admin/tenant/${tenantId}/members/add${params}`), + + updateTenantMember: (tenantId: string, memberId: string, params: string) => put(`/admin/tenant/${tenantId}/members/${memberId}${params}`), + + removeTenantMember: (tenantId: string, memberId: string) => del(`/admin/tenant/${tenantId}/members/${memberId}`), + + getTenantQuotas: (tenantId: string) => get(`/admin/tenant/${tenantId}/quotas`), + + updateTenantQuota: (tenantId: string, params: string) => put(`/admin/tenant/${tenantId}/quotas${params}`), + + getTenantInvitations: (tenantId: string) => get(`/admin/tenant/${tenantId}/invitations`), + + createTenantInvitation: (tenantId: string, params: string) => post(`/admin/tenant/${tenantId}/invitations${params}`), + + getTenantStats: () => get('/admin/tenant/stats/overview'), + + + + // 计费与支付 + + getBillingRules: (page?: number) => get(`/admin/billing/rules?page=${page || 1}&page_size=100`), + + createBillingRule: (params: string) => post(`/admin/billing/rules/create${params}`), + + updateBillingRule: (id: string, params: string) => put(`/admin/billing/rules/${id}${params}`), + + deleteBillingRule: (id: string) => del(`/admin/billing/rules/${id}`), + + getBillingPackages: () => get('/admin/billing/packages'), + + createBillingPackage: (params: string) => post(`/admin/billing/packages/create${params}`), + + updateBillingPackage: (id: string, params: string) => put(`/admin/billing/packages/${id}${params}`), + + deleteBillingPackage: (id: string) => del(`/admin/billing/packages/${id}`), + + getBillingTransactions: (page?: number) => get(`/admin/billing/transactions?page=${page || 1}&page_size=20`), + + getInvoices: (page?: number) => get(`/admin/billing/invoices?page=${page || 1}&page_size=20`), + + generateInvoice: (params: string) => post(`/admin/billing/invoices/generate${params}`), + + getPaymentConfigs: () => get('/admin/billing/payment-configs'), + + updatePaymentConfig: (id: string, params: string) => put(`/admin/billing/payment-configs/${id}${params}`), + + getBillingStats: () => get('/admin/billing/stats/overview'), + + + + // 数据分析 + + getAnalyticsOverview: (days?: number) => get(`/admin/analytics/overview?days=${days || 30}`), + + getAnalyticsApiTrends: (days?: number) => get(`/admin/analytics/trends/api-calls?days=${days || 7}`), + + getAnalyticsTokenTrends: (days?: number) => get(`/admin/analytics/trends/tokens?days=${days || 7}`), + + getAnalyticsUserTrends: (days?: number) => get(`/admin/analytics/trends/users?days=${days || 30}`), + + getAnalyticsRevenueTrends: (days?: number) => get(`/admin/analytics/trends/revenue?days=${days || 30}`), + + getAnalyticsModelUsage: (days?: number) => get(`/admin/analytics/models/usage?days=${days || 30}`), + + getAnalyticsDashboards: () => get('/admin/analytics/dashboards'), + + createAnalyticsDashboard: (params: string) => post(`/admin/analytics/dashboards/create${params}`), + + deleteAnalyticsDashboard: (id: string) => del(`/admin/analytics/dashboards/${id}`), + + getAnalyticsEvents: (params?: any) => { + + const sp = new URLSearchParams(); + + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + + return get(`/admin/analytics/events${sp.toString() ? `?${sp.toString()}` : ''}`); + + }, + + + +}; + + + + + +// ===== 前台用户个人微信 (iLink 协议 / 微信 ClawBot 插件) ===== + +export const imUserAPI = { + + getStatus: () => get('/im/wechat/status'), + + qrcode: () => post('/im/wechat/qrcode'), + + qrcodeStatus: (qrcode: string) => post(`/im/wechat/qrcode/status?qrcode=${encodeURIComponent(qrcode)}`), + + disconnect: () => post('/im/wechat/disconnect'), + + history: () => get('/im/wechat/history'), + +}; + diff --git a/frontend/src/components/layout/FrontLayout.tsx b/frontend/src/components/layout/FrontLayout.tsx index 5eb83a3..376891a 100644 --- a/frontend/src/components/layout/FrontLayout.tsx +++ b/frontend/src/components/layout/FrontLayout.tsx @@ -1,183 +1,182 @@ -import React, { useState } from 'react'; -import { Outlet, NavLink, useNavigate } from 'react-router-dom'; -import { useAuth } from '@/contexts/AuthContext'; -import './FrontLayout.css'; - -const navGroups = [ - { - title: '智能体', - items: [ - { path: '/chat', icon: 'chat', label: 'AI聊天' }, - { path: '/knowledge', icon: 'knowledge', label: '知识库' }, - { path: '/market', icon: 'market', label: '技能市场' }, - { path: '/evolution', icon: 'evolution', label: 'AI自进化' }, - ], - }, - { - title: 'IM Bot', - items: [ - { path: '/channels', icon: 'channel', label: '频道管理' }, - { path: '/wechat-bot', icon: 'wechat', label: '微信机器人' }, - ], - }, - { - title: '资源中心', - items: [ - { path: '/assets', icon: 'assets', label: '资产文件' }, - { path: '/token', icon: 'token', label: 'Token驾驶舱' }, - { path: '/pricing', icon: 'pricing', label: '付费中心' }, - ], - }, -]; - -const bottomItems = [ - { path: '/profile', icon: 'profile', label: '个人中心' }, - { path: '/notifications', icon: 'notify', label: '消息通知' }, -]; - -const iconPaths: Record = { - chat: , - channel: , - knowledge: , - market: , - apps: , - token: , - pricing: , - profile: , - notify: , - assets: , - evolution: , - wechat: , -}; - -const FrontLayout: React.FC = () => { - const { user, logout } = useAuth(); - const navigate = useNavigate(); - const [collapsed, setCollapsed] = useState(false); - - if (!user) { - navigate('/login'); - return null; - } - - return ( -
- {/* Sidebar */} - - - {/* Main Content */} -
- -
-
- ); -}; - -export default FrontLayout; +import React, { useState } from 'react'; +import { Outlet, NavLink, useNavigate } from 'react-router-dom'; +import { useAuth } from '@/contexts/AuthContext'; +import './FrontLayout.css'; + +const navGroups = [ + { + title: '智能体', + items: [ + { path: '/chat', icon: 'chat', label: 'AI聊天' }, + { path: '/knowledge', icon: 'knowledge', label: '知识库' }, + { path: '/market', icon: 'market', label: '技能市场' }, + { path: '/evolution', icon: 'evolution', label: 'AI自进化' }, + ], + }, + { + title: 'IM Bot', + items: [ + { path: '/wechat-bot', icon: 'wechat', label: '微信机器人' }, + ], + }, + { + title: '资源中心', + items: [ + { path: '/assets', icon: 'assets', label: '资产文件' }, + { path: '/token', icon: 'token', label: 'Token驾驶舱' }, + { path: '/pricing', icon: 'pricing', label: '付费中心' }, + ], + }, +]; + +const bottomItems = [ + { path: '/profile', icon: 'profile', label: '个人中心' }, + { path: '/notifications', icon: 'notify', label: '消息通知' }, +]; + +const iconPaths: Record = { + chat: , + channel: , + knowledge: , + market: , + apps: , + token: , + pricing: , + profile: , + notify: , + assets: , + evolution: , + wechat: , +}; + +const FrontLayout: React.FC = () => { + const { user, logout } = useAuth(); + const navigate = useNavigate(); + const [collapsed, setCollapsed] = useState(false); + + if (!user) { + navigate('/login'); + return null; + } + + return ( +
+ {/* Sidebar */} + + + {/* Main Content */} +
+ +
+
+ ); +}; + +export default FrontLayout; diff --git a/frontend/src/mock/data.ts b/frontend/src/mock/data.ts index 92111fa..3c01182 100644 --- a/frontend/src/mock/data.ts +++ b/frontend/src/mock/data.ts @@ -2,7 +2,7 @@ // 分离的Mock数据层,便于后续接入真实后端时替换 import type { - User, Conversation, Message, Channel, KnowledgeBase, + User, Conversation, Message, KnowledgeBase, KnowledgeDocument, MarketApp, AppReview, TokenUsageLog, TokenBalance, Order, MembershipPlan, Coupon, UserCoupon, Notification, ModelProvider, AIModel, SiteSettings, @@ -97,13 +97,7 @@ export const mockMessages: Record = { ], }; -// ==================== 频道数据 (IM账户绑定) ==================== -export const mockChannels: Channel[] = [ - { id: 'ch1', userId: 'u1', platform: 'feishu', name: '飞书AI助手', accountName: 'zangsan@company.feishu.cn', agentId: 'a1', knowledgeBaseId: 'kb1', status: 'connected', lastActive: '2026-05-26T09:30:00Z', boundModel: 'DeepSeek-V3', features: ['AI对话', '文档问答', '任务提醒', '会议纪要'], createdAt: '2026-04-01T08:00:00Z' }, - { id: 'ch2', userId: 'u1', platform: 'dingtalk', name: '钉钉AI助理', accountName: '张经理', agentId: 'a2', knowledgeBaseId: 'kb2', status: 'connected', lastActive: '2026-05-26T08:15:00Z', boundModel: 'GPT-4o', features: ['AI对话', '审批辅助', '日程管理'], createdAt: '2026-04-15T10:00:00Z' }, - { id: 'ch3', userId: 'u1', platform: 'wechat', name: '微信AI伙伴', accountName: 'wxid_abc123', agentId: undefined, knowledgeBaseId: undefined, status: 'pending', lastActive: undefined, boundModel: 'DeepSeek-V3', features: ['AI对话', '内容创作'], createdAt: '2026-05-01T09:00:00Z' }, - { id: 'ch4', userId: 'u1', platform: 'telegram', name: 'Telegram Bot', accountName: '@aiflow_bot', agentId: undefined, knowledgeBaseId: undefined, status: 'disconnected', lastActive: '2026-05-10T14:00:00Z', boundModel: 'Qwen-Max', features: ['AI对话'], createdAt: '2026-05-10T14:00:00Z' }, -]; + // ==================== 知识库数据 ==================== export const mockKnowledgeBases: KnowledgeBase[] = [ diff --git a/frontend/src/pages/admin/ImBots.tsx b/frontend/src/pages/admin/ImBots.tsx index 3d34d55..03b7451 100644 --- a/frontend/src/pages/admin/ImBots.tsx +++ b/frontend/src/pages/admin/ImBots.tsx @@ -33,7 +33,7 @@ const statusColors: Record = { disconnected: '#6b7280', }; -type Tab = 'dashboard' | 'channels' | 'routes' | 'langbot' | 'messages' | 'setup'; +type Tab = 'dashboard' | 'channels' | 'routes' | 'messages' | 'setup'; const ImBots: React.FC = () => { const [activeTab, setActiveTab] = useState('dashboard'); @@ -46,7 +46,6 @@ const ImBots: React.FC = () => { const [filterPlatform, setFilterPlatform] = useState(''); const [filterStatus, setFilterStatus] = useState(''); const [routes, setRoutes] = useState([]); - const [langbotConfig, setLangbotConfig] = useState([]); const [messages, setMessages] = useState([]); const [messagesTotal, setMessagesTotal] = useState(0); const [messagesPage, setMessagesPage] = useState(1); @@ -77,18 +76,6 @@ const ImBots: React.FC = () => { credentials: '', }); - // LangBot form state - const [langbotForm, setLangbotForm] = useState({ - id: 1, - gateway_url: '', - api_key: '', - webhook_secret: '', - bot_uuid: '', - callback_url: '', - sync_channels: 0, - status: 'inactive', - }); - const showToast = (msg: string, type: 'success' | 'error' = 'success') => { setToast({ msg, type }); setTimeout(() => setToast(null), 3000); @@ -155,22 +142,6 @@ const ImBots: React.FC = () => { } }, []); - // Load LangBot config - const loadLangbot = useCallback(async () => { - try { - const res = await adminAPI.getLangbotConfig(); - if ((res as any)?.success) { - const configs = res.data || []; - setLangbotConfig(configs); - if (configs.length > 0) { - setLangbotForm({ ...langbotForm, ...configs[0] }); - } - } - } catch (e) { - showToast('Failed to load LangBot config', 'error'); - } - }, []); - // Load messages const loadMessages = useCallback(async () => { try { @@ -211,8 +182,6 @@ const ImBots: React.FC = () => { } else if (activeTab === 'routes') { loadRoutes(); loadChannels(); - } else if (activeTab === 'langbot') { - loadLangbot(); } else if (activeTab === 'messages') { loadMessages(); } @@ -348,51 +317,7 @@ const ImBots: React.FC = () => { } }; - // Handle LangBot config save - const handleSaveLangbot = async () => { - try { - const params = new URLSearchParams({ - config_id: String(langbotForm.id || 1), - gateway_url: langbotForm.gateway_url, - status: langbotForm.status, - sync_channels: String(langbotForm.sync_channels), - }); - if (langbotForm.api_key) params.append('api_key', langbotForm.api_key); - if (langbotForm.webhook_secret) params.append('webhook_secret', langbotForm.webhook_secret); - if (langbotForm.bot_uuid) params.append('bot_uuid', langbotForm.bot_uuid); - if (langbotForm.callback_url) params.append('callback_url', langbotForm.callback_url); - await adminAPI.updateLangbotConfig('?' + params.toString()); - showToast('LangBot config saved'); - loadLangbot(); - } catch (e) { - showToast('Failed to save LangBot config', 'error'); - } - }; - // Handle LangBot test - const handleTestLangbot = async () => { - if (!langbotForm.gateway_url) { - showToast('Please enter gateway URL', 'error'); - return; - } - try { - const params = new URLSearchParams({ - gateway_url: langbotForm.gateway_url, - }); - if (langbotForm.api_key) params.append('api_key', langbotForm.api_key); - const res = await adminAPI.testLangbot('?' + params.toString()); - if ((res as any)?.success) { - const data = res.data; - if (data.reachable) { - showToast(`LangBot reachable! Status: ${data.status_code}`); - } else { - showToast(`LangBot unreachable: ${data.error}`, 'error'); - } - } - } catch (e) { - showToast('Test failed', 'error'); - } - }; // Copy to clipboard const copyToClipboard = (text: string) => { @@ -411,7 +336,6 @@ const ImBots: React.FC = () => { { key: 'dashboard', label: '仪表盘', icon: 'M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z' }, { key: 'channels', label: '频道管理', icon: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z' }, { key: 'routes', label: '消息路由', icon: 'M3.5 18.5l6-6.01 4 4L22 6.92l-1.41-1.41-7.09 7.97-4-4L2 17l1.5 1.5z' }, - { key: 'langbot', label: 'LangBot 网关', icon: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z' }, { key: 'messages', label: '消息日志', icon: 'M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z' }, { key: 'setup', label: '快速接入', icon: 'M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z' }, ]; @@ -422,7 +346,7 @@ const ImBots: React.FC = () => {

IM 机器人管理

-

管理即时通讯平台接入、消息路由和 LangBot 网关

+

管理即时通讯平台接入与消息路由

{stats && ( @@ -430,7 +354,6 @@ const ImBots: React.FC = () => { -
)}
@@ -744,105 +667,6 @@ const ImBots: React.FC = () => { )} - {/* LangBot Tab */} - {activeTab === 'langbot' && ( -
-
-
-

LangBot 网关配置

- - {langbotForm.status === 'active' ? '已启用' : '未启用'} - -
- -
- - setLangbotForm({ ...langbotForm, gateway_url: e.target.value })} - placeholder="http://localhost:5300" - style={inputStyle} - /> - - - setLangbotForm({ ...langbotForm, bot_uuid: e.target.value })} - placeholder="LangBot HTTP Bot UUID" - style={inputStyle} - /> - - - setLangbotForm({ ...langbotForm, api_key: e.target.value })} - placeholder="LangBot REST API Key" - style={inputStyle} - /> - - - setLangbotForm({ ...langbotForm, webhook_secret: e.target.value })} - placeholder="HMAC-SHA256 签名密钥" - style={inputStyle} - /> - - - setLangbotForm({ ...langbotForm, callback_url: e.target.value })} - placeholder="http://210.16.161.218:12888/app/api/channels/webhook/langbot" - style={inputStyle} - /> - - - - - - - -
- -
- - -
-
- - {/* LangBot Info */} -
-

LangBot 集成说明

-
    -
  • LangBot 是开源的 LLM 原生 IM 机器人平台,支持飞书、微信、钉钉等
  • -
  • 配置 LangBot 作为外部网关后,IM 消息将通过 LangBot 转发到本平台
  • -
  • 在 LangBot 中创建 HTTP Bot 适配器,将 Webhook URL 指向本平台
  • -
  • API Key 在 LangBot 的 config.yaml 中通过 api.global_api_key 配置
  • -
  • Webhook Secret 用于 HMAC-SHA256 双向签名验证
  • -
-
-
- )} {/* Messages Tab */} {activeTab === 'messages' && ( diff --git a/frontend/src/pages/front/ChannelPage.css b/frontend/src/pages/front/ChannelPage.css deleted file mode 100644 index 7224938..0000000 --- a/frontend/src/pages/front/ChannelPage.css +++ /dev/null @@ -1,1780 +0,0 @@ -/* ===== Channel Page (IM Channel Management) ===== */ -.channel-page { - height: 100%; - overflow: hidden; - display: flex; - flex-direction: column; - animation: fadeIn var(--transition-normal); -} - -.channel-list-view { - height: 100%; - overflow-y: auto; - padding: var(--space-3xl); - display: flex; - flex-direction: column; -} - -/* ===== Header ===== */ -.channel-page-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: var(--space-2xl); - flex-shrink: 0; -} - -.channel-header-left { - display: flex; - align-items: baseline; - gap: var(--space-md); -} - -.channel-page-title { - font-size: var(--text-xl); - font-weight: 700; - color: var(--color-text-primary); - letter-spacing: -0.02em; -} - -.channel-count { - font-size: var(--text-sm); - color: var(--color-text-tertiary); -} - -.channel-create-btn { - display: inline-flex; - align-items: center; - gap: var(--space-sm); - padding: var(--space-md) var(--space-xl); - background: var(--color-accent); - color: white; - border-radius: var(--radius-lg); - font-size: var(--text-sm); - font-weight: 600; - transition: all var(--transition-fast); -} - -.channel-create-btn:hover { - background: var(--color-accent-hover); - transform: translateY(-1px); - box-shadow: var(--shadow-md); -} - -/* ===== Intro Banner ===== */ -.channel-intro-banner { - display: flex; - align-items: center; - gap: var(--space-xl); - padding: var(--space-2xl); - background: linear-gradient(135deg, #F6F5FF 0%, #EEEDFF 100%); - border-radius: var(--radius-xl); - border: 1px solid #EEEDFF; - margin-bottom: var(--space-2xl); - color: var(--color-accent); -} - -.channel-intro-text h3 { - font-size: var(--text-md); - font-weight: 600; - color: var(--color-text-primary); - margin-bottom: var(--space-xs); -} - -.channel-intro-text p { - font-size: var(--text-sm); - color: var(--color-text-secondary); - line-height: 1.6; -} - -/* ===== Card Grid ===== */ -.channel-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: var(--space-xl); -} - -@media (max-width: 900px) { - .channel-grid { grid-template-columns: 1fr; } -} - -/* ===== Channel Card ===== */ -.channel-card { - background: var(--color-bg-secondary); - border: 1px solid var(--color-border); - border-radius: var(--radius-xl); - padding: var(--space-2xl); - cursor: pointer; - transition: all var(--transition-normal); - display: flex; - flex-direction: column; - gap: var(--space-lg); -} - -.channel-card:hover { - border-color: var(--color-accent); - box-shadow: var(--shadow-md); - transform: translateY(-2px); -} - -.channel-card-top { - display: flex; - align-items: flex-start; - gap: var(--space-lg); -} - -.channel-platform-icon { - width: 48px; - height: 48px; - border-radius: var(--radius-lg); - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; -} - -.channel-card-info { - flex: 1; - min-width: 0; -} - -.channel-card-name-row { - display: flex; - align-items: center; - gap: var(--space-sm); - margin-bottom: 3px; -} - -.channel-card-name { - font-size: var(--text-md); - font-weight: 600; - color: var(--color-text-primary); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.channel-status-sm { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 1px 8px; - border-radius: var(--radius-full); - font-size: 10px; - font-weight: 600; - flex-shrink: 0; -} - -.status-dot { - width: 6px; - height: 6px; - border-radius: 50%; - background: currentColor; -} - -.status-connected { background: var(--color-success-light); color: var(--color-success); } -.status-disconnected { background: var(--color-bg-tertiary); color: var(--color-text-tertiary); } -.status-pending { background: var(--color-warning-light); color: var(--color-warning); } - -.channel-card-account { - font-size: 11px; - color: var(--color-text-tertiary); -} - -.channel-card-meta-row { - display: flex; - align-items: center; - gap: var(--space-xl); -} - -.channel-meta-item { - display: flex; - align-items: center; - gap: 5px; - font-size: var(--text-xs); - color: var(--color-text-tertiary); -} - -.channel-card-footer { - padding-top: var(--space-md); - border-top: 1px solid var(--color-border-light); - display: flex; - align-items: center; - justify-content: space-between; -} - -.channel-last-active { - display: flex; - align-items: center; - gap: 5px; - font-size: var(--text-xs); - color: var(--color-text-tertiary); -} - -.channel-online-indicator { - font-size: 10px; - font-weight: 600; - color: var(--color-success); - background: var(--color-success-light); - padding: 1px 8px; - border-radius: var(--radius-full); -} - -/* ===== Detail View ===== */ -.channel-detail { - height: 100%; - display: flex; - flex-direction: column; - overflow: hidden; -} - -.channel-detail-header { - display: flex; - align-items: center; - gap: var(--space-lg); - padding: var(--space-lg) var(--space-2xl); - border-bottom: 1px solid var(--color-border); - background: var(--color-bg-secondary); - flex-shrink: 0; -} - -.channel-back-btn { - display: flex; - align-items: center; - justify-content: center; - width: 36px; - height: 36px; - border-radius: var(--radius-md); - color: var(--color-text-secondary); - transition: all var(--transition-fast); -} - -.channel-back-btn:hover { - background: var(--color-bg-hover); - color: var(--color-text-primary); -} - -.channel-detail-title { - font-size: var(--text-lg); - font-weight: 600; - color: var(--color-text-primary); - flex: 1; -} - -.detail-header-right { - display: flex; - align-items: center; - gap: var(--space-md); -} - -/* ===== Detail Tabs ===== */ -.detail-tabs { - display: flex; - gap: 0; - border-bottom: 1px solid var(--color-border); - background: var(--color-bg-secondary); - padding: 0 var(--space-2xl); - flex-shrink: 0; -} - -.detail-tab { - padding: var(--space-md) var(--space-xl); - font-size: var(--text-sm); - font-weight: 500; - color: var(--color-text-tertiary); - border-bottom: 2px solid transparent; - transition: all var(--transition-fast); - margin-bottom: -1px; -} - -.detail-tab:hover { - color: var(--color-text-primary); -} - -.detail-tab.active { - color: var(--color-accent); - border-bottom-color: var(--color-accent); - font-weight: 600; -} - -.channel-detail-body { - flex: 1; - overflow-y: auto; - padding: var(--space-2xl); - display: flex; - flex-direction: column; - gap: var(--space-xl); -} - -/* ===== Detail Cards ===== */ -.channel-detail-card { - background: var(--color-bg-secondary); - border: 1px solid var(--color-border); - border-radius: var(--radius-xl); - padding: var(--space-2xl); -} - -.detail-card-header { - font-size: var(--text-sm); - font-weight: 700; - color: var(--color-text-primary); - margin-bottom: var(--space-xl); - padding-bottom: var(--space-md); - border-bottom: 1px solid var(--color-border-light); - display: flex; - align-items: center; - justify-content: space-between; -} - -.detail-platform-row { - display: flex; - align-items: center; - gap: var(--space-lg); - margin-bottom: var(--space-lg); -} - -.detail-platform-icon { - width: 56px; - height: 56px; - border-radius: var(--radius-xl); - display: flex; - align-items: center; - justify-content: center; -} - -.detail-platform-info { - flex: 1; -} - -.detail-platform-name { - font-size: var(--text-lg); - font-weight: 700; - color: var(--color-text-primary); -} - -.detail-platform-account { - font-size: var(--text-sm); - color: var(--color-text-tertiary); - margin-top: 2px; -} - -.channel-status-tag { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 4px 12px; - border-radius: var(--radius-full); - font-size: var(--text-xs); - font-weight: 600; - flex-shrink: 0; -} - -.detail-meta-row { - display: flex; - align-items: center; - gap: var(--space-2xl); - font-size: var(--text-xs); - color: var(--color-text-tertiary); - margin-bottom: var(--space-lg); - flex-wrap: wrap; -} - -.detail-meta-row span { - display: flex; - align-items: center; - gap: 4px; -} - -.detail-actions { - display: flex; - gap: var(--space-md); - padding-top: var(--space-lg); - border-top: 1px solid var(--color-border-light); -} - -.detail-action-btn { - display: inline-flex; - align-items: center; - gap: var(--space-sm); - padding: var(--space-sm) var(--space-lg); - border-radius: var(--radius-md); - font-size: var(--text-sm); - font-weight: 500; - transition: all var(--transition-fast); -} - -.detail-action-btn.primary { - border: 1px solid var(--color-accent); - color: var(--color-accent); - background: transparent; -} -.detail-action-btn.primary:hover { - background: var(--color-accent-subtle); -} - -.detail-action-btn.warning { - border: 1px solid var(--color-warning); - color: var(--color-warning); - background: transparent; -} -.detail-action-btn.warning:hover { - background: var(--color-warning-light); -} - -.detail-action-btn.danger { - border: 1px solid var(--color-error); - color: var(--color-error); - background: transparent; -} -.detail-action-btn.danger:hover { - background: var(--color-error-light); -} - -/* ===== Credential Display ===== */ -.toggle-credential-btn { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 2px 10px; - border-radius: var(--radius-full); - font-size: 11px; - font-weight: 500; - color: var(--color-accent); - background: var(--color-accent-subtle); - transition: all var(--transition-fast); -} -.toggle-credential-btn:hover { - background: var(--color-accent-light); -} - -.credential-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: var(--space-lg); -} - -.credential-item { - display: flex; - flex-direction: column; - gap: 4px; -} - -.credential-item.webhook-item { - grid-column: 1 / -1; -} - -.credential-key { - font-size: 11px; - color: var(--color-text-tertiary); - text-transform: uppercase; - font-weight: 500; -} - -.credential-value-row { - display: flex; - align-items: center; - gap: var(--space-sm); -} - -.credential-value { - font-size: var(--text-sm); - color: var(--color-text-primary); - font-family: var(--font-mono); - word-break: break-all; - background: var(--color-bg-tertiary); - padding: 4px 10px; - border-radius: var(--radius-md); - flex: 1; -} - -.credential-eye-btn { - display: flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - border-radius: var(--radius-sm); - color: var(--color-text-tertiary); - transition: all var(--transition-fast); - flex-shrink: 0; -} -.credential-eye-btn:hover { - background: var(--color-bg-hover); - color: var(--color-text-primary); -} - -.no-credential { - text-align: center; - padding: var(--space-xl); - color: var(--color-text-tertiary); -} -.no-credential .hint { - font-size: var(--text-xs); - margin-top: var(--space-sm); -} - -/* ===== AI Settings Readonly View ===== */ -.ai-settings-readonly { - display: flex; - flex-direction: column; - gap: var(--space-lg); -} - -.ai-readonly-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: var(--space-lg); -} - -.ai-readonly-item { - display: flex; - flex-direction: column; - gap: 3px; -} - -.ai-readonly-label { - font-size: var(--text-xs); - color: var(--color-text-tertiary); - font-weight: 500; -} - -.ai-readonly-value { - font-size: var(--text-sm); - color: var(--color-text-primary); - font-weight: 500; -} - -.ai-readonly-value.text-success { - color: var(--color-success); -} - -.ai-readonly-value.text-muted { - color: var(--color-text-tertiary); -} - -/* ===== AI Settings Form (editable, shared with create modal) ===== */ -.ai-settings-form { - display: flex; - flex-direction: column; - gap: var(--space-xl); -} - -.ai-settings-form-scroll { - display: flex; - flex-direction: column; - gap: var(--space-lg); - max-height: 50vh; - overflow-y: auto; - padding-right: var(--space-sm); -} - -.ai-settings-footer { - display: flex; - justify-content: flex-end; - gap: var(--space-md); - padding-top: var(--space-lg); - border-top: 1px solid var(--color-border-light); -} - -.skill-checkbox-list { - display: flex; - flex-wrap: wrap; - gap: var(--space-sm); -} - -.skill-checkbox-item { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 4px 12px; - border-radius: var(--radius-full); - border: 1px solid var(--color-border); - font-size: var(--text-sm); - color: var(--color-text-secondary); - cursor: pointer; - transition: all var(--transition-fast); - user-select: none; -} - -.skill-checkbox-item input { - display: none; -} - -.skill-checkbox-item.checked { - border-color: var(--color-accent); - background: var(--color-accent-subtle); - color: var(--color-accent); -} - -.skill-checkbox-name { - font-weight: 500; -} - -.no-data-hint { - font-size: var(--text-xs); - color: var(--color-text-tertiary); -} - -.settings-result { - padding: var(--space-sm) var(--space-md); - border-radius: var(--radius-md); - font-size: var(--text-sm); - font-weight: 500; -} -.settings-result.success { - background: var(--color-success-light); - color: var(--color-success); -} -.settings-result.error { - background: var(--color-error-light); - color: var(--color-error); -} - -/* ===== Chat Card ===== */ -.chat-card { - flex: 1; - display: flex; - flex-direction: column; - min-height: 0; -} - -.chat-platform-badge { - display: inline-flex; - align-items: center; - padding: 2px 10px; - border-radius: var(--radius-full); - font-size: 11px; - font-weight: 600; -} - -.chat-not-connected { - display: flex; - align-items: center; - justify-content: space-between; - padding: var(--space-md) var(--space-lg); - background: var(--color-warning-light); - border-radius: var(--radius-md); - font-size: var(--text-sm); - color: var(--color-warning); - margin-bottom: var(--space-lg); -} - -.chat-container { - display: flex; - justify-content: center; - padding: var(--space-lg) 0; - flex: 1; -} - -.chat-empty-state { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: var(--space-md); - color: var(--color-text-tertiary); - height: 100%; -} -.chat-empty-state svg { - width: 32px; - height: 32px; - opacity: 0.5; -} -.chat-empty-state p { - font-size: var(--text-sm); -} - -/* ===== Phone Frame (Chat) ===== */ -.phone-frame { - width: 320px; - background: #FFFFFF; - border: 3px solid #101214; - border-radius: 28px; - overflow: hidden; - box-shadow: var(--shadow-lg); - display: flex; - flex-direction: column; - max-height: 500px; -} - -.phone-topbar { - background: #F1F3F7; - padding: 8px 16px; - display: flex; - flex-direction: column; - align-items: center; - border-bottom: 1px solid #E3E6EB; -} - -.phone-notch { - width: 60px; - height: 6px; - background: #101214; - border-radius: 3px; - margin-bottom: 6px; -} - -.phone-title { - font-size: 11px; - font-weight: 600; - color: var(--color-text-secondary); -} - -.phone-chat { - flex: 1; - height: 300px; - overflow-y: auto; - padding: 12px; - display: flex; - flex-direction: column; - gap: 10px; - background: #F8F9FB; -} - -.phone-msg { - display: flex; - flex-direction: column; - max-width: 85%; -} - -.phone-msg.bot { - align-self: flex-start; -} - -.phone-msg.user { - align-self: flex-end; -} - -.phone-msg-sender { - font-size: 9px; - color: var(--color-text-disabled); - margin-bottom: 2px; - padding: 0 4px; -} - -.phone-bubble { - padding: 8px 12px; - border-radius: 14px; - font-size: 11px; - line-height: 1.5; - word-break: break-word; -} - -.phone-msg.bot .phone-bubble { - background: #FFFFFF; - color: var(--color-text-primary); - border: 1px solid #E3E6EB; - border-bottom-left-radius: 4px; -} - -.phone-msg.user .phone-bubble { - background: var(--color-accent); - color: white; - border-bottom-right-radius: 4px; -} - -.phone-time { - font-size: 9px; - color: var(--color-text-disabled); - margin-top: 2px; - padding: 0 4px; -} - -.phone-input-bar { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 12px; - border-top: 1px solid #E3E6EB; - background: #FFFFFF; -} - -.phone-input-field { - flex: 1; - border: none; - outline: none; - font-size: 12px; - color: var(--color-text-primary); - background: transparent; - min-width: 0; -} - -.phone-input-field::placeholder { - color: var(--color-text-disabled); -} - -.phone-send-btn { - display: flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - border-radius: 50%; - background: var(--color-accent); - color: white; - transition: all var(--transition-fast); - flex-shrink: 0; -} - -.phone-send-btn:hover { - background: var(--color-accent-hover); -} - -.phone-send-btn:disabled { - background: var(--color-border); - color: var(--color-text-disabled); - cursor: not-allowed; -} - -/* ===== Modals ===== */ -.modal-overlay { - position: fixed; - inset: 0; - background: rgba(0,0,0,0.45); - display: flex; - align-items: center; - justify-content: center; - z-index: var(--z-modal); - animation: fadeIn var(--transition-fast); -} - -.bind-modal { - background: #FFFFFF; - border-radius: var(--radius-2xl); - width: 520px; - max-width: 90vw; - padding: var(--space-3xl); - box-shadow: var(--shadow-xl); - animation: slideUp var(--transition-normal); -} - -.platform-config-modal { - background: #FFFFFF; - border-radius: var(--radius-2xl); - width: 600px; - max-width: 90vw; - max-height: 85vh; - overflow-y: auto; - padding: var(--space-3xl); - box-shadow: var(--shadow-xl); - animation: slideUp var(--transition-normal); -} - -.confirm-modal { - background: #FFFFFF; - border-radius: var(--radius-2xl); - width: 420px; - max-width: 90vw; - padding: var(--space-2xl); - box-shadow: var(--shadow-xl); - animation: slideUp var(--transition-normal); -} - -.bind-modal-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: var(--space-sm); -} - -.bind-modal-header h2 { - font-size: var(--text-lg); - font-weight: 700; - color: var(--color-text-primary); -} - -.modal-close-btn { - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border-radius: var(--radius-md); - color: var(--color-text-tertiary); - transition: all var(--transition-fast); -} - -.modal-close-btn:hover { - background: var(--color-bg-hover); - color: var(--color-text-primary); -} - -.bind-modal-desc { - font-size: var(--text-sm); - color: var(--color-text-secondary); - margin-bottom: var(--space-2xl); - line-height: 1.6; -} - -.confirm-modal-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: var(--space-md); -} -.confirm-modal-header h2 { - font-size: var(--text-md); - font-weight: 700; - color: var(--color-text-primary); -} -.confirm-modal-message { - font-size: var(--text-sm); - color: var(--color-text-secondary); - line-height: 1.6; - margin-bottom: var(--space-xl); -} -.confirm-modal-footer { - display: flex; - justify-content: flex-end; - gap: var(--space-md); -} - -/* ===== Platform Selection ===== */ -.platform-select-label { - font-size: var(--text-sm); - font-weight: 600; - color: var(--color-text-primary); - margin-bottom: var(--space-lg); -} - -.bind-platform-list { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: var(--space-md); - margin-bottom: var(--space-2xl); -} - -.bind-platform-option { - display: flex; - flex-direction: column; - align-items: center; - gap: var(--space-sm); - padding: var(--space-xl) var(--space-md); - border-radius: var(--radius-xl); - border: 2px solid var(--color-border); - background: #FFFFFF; - transition: all var(--transition-fast); - cursor: pointer; - position: relative; -} - -.bind-platform-option:hover { - border-color: var(--color-accent); - background: var(--color-accent-subtle); -} - -.bind-platform-option.selected { - border-color: var(--color-accent); - background: var(--color-accent-light); -} - -.bpo-icon { - width: 44px; - height: 44px; - border-radius: var(--radius-xl); - display: flex; - align-items: center; - justify-content: center; -} - -.bpo-label { - font-size: var(--text-sm); - font-weight: 600; - color: var(--color-text-primary); -} - -.bpo-check { - position: absolute; - top: 8px; - right: 8px; - color: var(--color-accent); -} - -/* ===== Modal Footer ===== */ -.bind-modal-footer { - display: flex; - justify-content: flex-end; - gap: var(--space-md); - padding-top: var(--space-xl); - border-top: 1px solid var(--color-border-light); -} - -.bind-cancel-btn { - padding: var(--space-md) var(--space-2xl); - border-radius: var(--radius-lg); - border: 1px solid var(--color-border); - font-size: var(--text-sm); - font-weight: 600; - color: var(--color-text-secondary); - background: #FFFFFF; - transition: all var(--transition-fast); -} - -.bind-cancel-btn:hover { - background: var(--color-bg-hover); -} - -.bind-confirm-btn { - display: inline-flex; - align-items: center; - gap: var(--space-sm); - padding: var(--space-md) var(--space-2xl); - border-radius: var(--radius-lg); - background: var(--color-accent); - color: white; - font-size: var(--text-sm); - font-weight: 600; - transition: all var(--transition-fast); -} - -.bind-confirm-btn:hover { - background: var(--color-accent-hover); -} - -.bind-confirm-btn:disabled { - opacity: 0.6; - cursor: not-allowed; -} - -.bind-confirm-btn.danger { - background: var(--color-error); -} -.bind-confirm-btn.danger:hover { - background: var(--color-error-hover, #D9383A); -} - -/* ===== Step Indicator ===== */ -.step-indicator { - display: flex; - align-items: center; - gap: var(--space-sm); - margin-bottom: var(--space-lg); -} - -.step-dot { - width: 24px; - height: 24px; - border-radius: 50%; - background: var(--color-bg-tertiary); - color: var(--color-text-tertiary); - font-size: 11px; - font-weight: 600; - display: flex; - align-items: center; - justify-content: center; -} - -.step-dot.active { - background: var(--color-accent); - color: white; -} - -.step-arrow { - color: var(--color-text-disabled); - font-size: 12px; -} - -.step-dot-label { - font-size: var(--text-sm); - font-weight: 600; - color: var(--color-text-primary); - margin-left: var(--space-sm); -} - -/* ===== Platform Config Form ===== */ -.platform-config-form { - display: flex; - flex-direction: column; - gap: var(--space-lg); - margin-bottom: var(--space-xl); -} - -.config-field { - display: flex; - flex-direction: column; - gap: var(--space-sm); -} - -.config-field-row { - flex-direction: row; - align-items: center; - justify-content: space-between; -} - -.config-field-row .config-field-label { - gap: var(--space-xs); -} - -.config-field-label { - display: flex; - align-items: center; - gap: 5px; - font-size: var(--text-xs); - color: var(--color-text-secondary); - font-weight: 600; -} - -.required-mark { - color: var(--color-error); - font-weight: 700; -} - -.field-optional { - font-size: 10px; - color: var(--color-text-disabled); - font-weight: 400; -} - -.config-field-input { - padding: 10px 14px; - border: 1px solid var(--color-border); - border-radius: var(--radius-md); - font-size: var(--text-sm); - color: var(--color-text-primary); - background: #FFFFFF; - transition: all var(--transition-fast); - outline: none; - font-family: var(--font-mono); -} - -.config-field-input:focus { - border-color: var(--color-accent); - box-shadow: 0 0 0 3px var(--color-accent-subtle); -} - -.config-field-input::placeholder { - color: var(--color-text-disabled); - font-family: inherit; -} - -.config-field-select { - padding: 10px 14px; - border: 1px solid var(--color-border); - border-radius: var(--radius-md); - font-size: var(--text-sm); - color: var(--color-text-primary); - background: #FFFFFF; - transition: all var(--transition-fast); - outline: none; - cursor: pointer; -} -.config-field-select:focus { - border-color: var(--color-accent); - box-shadow: 0 0 0 3px var(--color-accent-subtle); -} - -.config-field-textarea { - padding: 10px 14px; - border: 1px solid var(--color-border); - border-radius: var(--radius-md); - font-size: var(--text-sm); - color: var(--color-text-primary); - background: #FFFFFF; - transition: all var(--transition-fast); - outline: none; - resize: vertical; - font-family: inherit; -} -.config-field-textarea:focus { - border-color: var(--color-accent); - box-shadow: 0 0 0 3px var(--color-accent-subtle); -} - -/* Password input with eye toggle */ -.password-input-wrapper { - position: relative; - display: flex; - align-items: center; -} - -.password-input-wrapper .config-field-input { - width: 100%; - padding-right: 40px; -} - -.password-toggle-btn { - position: absolute; - right: 8px; - display: flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - border-radius: var(--radius-sm); - color: var(--color-text-tertiary); - transition: all var(--transition-fast); -} -.password-toggle-btn:hover { - background: var(--color-bg-hover); - color: var(--color-text-primary); -} - -.config-doc-link { - display: inline-flex; - align-items: center; - gap: var(--space-xs); - font-size: var(--text-sm); - color: var(--color-accent); - text-decoration: none; - font-weight: 500; -} -.config-doc-link:hover { - text-decoration: underline; -} - -/* ===== Platform Tips ===== */ -.platform-tips-container { - display: flex; - flex-direction: column; - gap: var(--space-sm); - margin-bottom: var(--space-xl); -} - -.platform-tip { - display: flex; - align-items: flex-start; - gap: var(--space-sm); - padding: var(--space-md) var(--space-lg); - border-radius: var(--radius-md); - font-size: var(--text-sm); - line-height: 1.5; -} - -.platform-tip.tip-info { - background: var(--color-info-light); - color: var(--color-info); - border: 1px solid rgba(59, 130, 246, 0.2); -} - -.platform-tip.tip-warning { - background: var(--color-warning-light); - color: var(--color-warning); - border: 1px solid rgba(232, 146, 44, 0.2); -} - -.tip-icon { - flex-shrink: 0; - margin-top: 1px; -} - -.tip-content { - flex: 1; -} - -/* ===== Connect Result ===== */ -.connect-result { - display: flex; - align-items: center; - gap: var(--space-lg); - padding: var(--space-xl); - border-radius: var(--radius-xl); - margin-bottom: var(--space-lg); -} - -.connect-result.success { - background: var(--color-success-light); - border: 1px solid var(--color-success); -} - -.connect-result.error { - background: var(--color-error-light); - border: 1px solid var(--color-error); -} - -.connect-result-icon { - width: 40px; - height: 40px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-weight: 700; - flex-shrink: 0; -} - -.connect-result.success .connect-result-icon { - background: var(--color-success); - color: white; -} - -.connect-result.error .connect-result-icon { - background: var(--color-error); - color: white; -} - -.connect-result-text { - display: flex; - flex-direction: column; - gap: 2px; -} - -.connect-result-text strong { - font-size: var(--text-sm); - color: var(--color-text-primary); -} - -.connect-result-text span { - font-size: var(--text-xs); - color: var(--color-text-secondary); -} - -/* ===== Error & Loading ===== */ -.channel-error { - display: flex; - align-items: center; - justify-content: space-between; - padding: var(--space-md) var(--space-lg); - background: var(--color-error-light); - color: var(--color-error); - border-radius: var(--radius-md); - margin-bottom: var(--space-lg); - font-size: var(--text-sm); -} - -.channel-error button { - padding: 4px var(--space-md); - background: var(--color-error); - color: white; - border-radius: var(--radius-md); - font-size: var(--text-xs); - font-weight: 600; -} - -.channel-loading { - text-align: center; - padding: 60px 20px; - color: var(--color-text-tertiary); - font-size: var(--text-md); -} - -.channel-empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 60px 20px; - color: var(--color-text-tertiary); -} - -.channel-empty svg { - width: 48px; - height: 48px; - margin-bottom: 16px; - opacity: 0.5; -} - -.channel-empty h3 { - font-size: var(--text-lg); - font-weight: 600; - color: var(--color-text-secondary); - margin-bottom: 8px; -} - -.channel-empty p { - font-size: var(--text-sm); -} - -/* ===== QR Config Section ===== */ -.qr-config-section { - padding: var(--space-lg); - margin-bottom: var(--space-xl); - border: 1px solid var(--color-border); - border-radius: var(--radius-lg); - background: var(--color-bg-tertiary); -} - -.qr-config-header { - display: flex; - align-items: center; - gap: var(--space-sm); - margin-bottom: var(--space-sm); -} - -.qr-icon { - display: flex; - align-items: center; - color: var(--color-accent); -} - -.qr-config-title { - font-size: var(--text-sm); - font-weight: 600; - color: var(--color-text-primary); -} - -.qr-config-desc { - font-size: var(--text-xs); - color: var(--color-text-secondary); - line-height: 1.5; - margin-bottom: var(--space-md); -} - -.qr-code-container { - display: flex; - align-items: center; - justify-content: center; - gap: var(--space-xl); -} - -.qr-code-box { - background: #FFFFFF; - border: 1px solid var(--color-border); - border-radius: var(--radius-md); - padding: var(--space-sm); - display: flex; - align-items: center; - position: relative; - justify-content: center; - flex-shrink: 0; -} - -.qr-code-actions { - display: flex; - flex-direction: column; - gap: var(--space-sm); -} - -.qr-action-link { - display: inline-flex; - align-items: center; - gap: var(--space-xs); - padding: var(--space-sm) var(--space-lg); - background: var(--color-accent); - color: white; - border-radius: var(--radius-md); - font-size: var(--text-sm); - font-weight: 500; - text-decoration: none; - transition: all var(--transition-fast); -} - -.qr-action-link:hover { - background: var(--color-accent-hover); - text-decoration: none; -} - -.qr-flow-hint { - font-size: var(--text-xs); - color: var(--color-text-tertiary); - line-height: 1.4; -} - -.qr-flow-hint.scan-error { - color: #D9383A; - font-weight: 500; - padding: 6px 12px; - background: #FDEBEB; - border-radius: var(--radius-sm); - border: 1px solid #FECACA; -} - -.qr-flow-hint.scan-success { - color: #219B5E; - font-weight: 500; - padding: 6px 12px; - background: #E6F7EE; - border-radius: var(--radius-sm); - border: 1px solid #A7F3D0; -} - -.qr-placeholder { - width: 160px; - height: 160px; - display: flex; - align-items: center; - justify-content: center; - font-size: var(--text-sm); - color: var(--color-text-tertiary); -} - -.qr-placeholder-error { - color: #D9383A; - background: #FDEBEB; - border: 1px dashed #FECACA; - border-radius: var(--radius-md); - font-weight: 500; - padding: var(--space-md); - text-align: center; -} - -/* ===== WeChat Config Guide ===== */ -.wechat-config-guide { - display: flex; - flex-direction: column; - gap: var(--space-md); - padding: var(--space-md); - width: 100%; -} - -.wechat-guide-steps { - display: flex; - flex-direction: column; - gap: var(--space-sm); -} - -.wechat-guide-step { - display: flex; - align-items: center; - gap: var(--space-sm); - font-size: var(--text-sm); - color: var(--color-text-secondary); - line-height: 1.5; -} - -.wechat-guide-step .step-number { - display: inline-flex; - align-items: center; - justify-content: center; - width: 22px; - height: 22px; - border-radius: 50%; - background: var(--color-accent); - color: white; - font-size: 12px; - font-weight: 600; - flex-shrink: 0; -} - -.wechat-open-platform-link { - display: inline-flex; - align-items: center; - gap: var(--space-xs); - padding: var(--space-sm) var(--space-lg); - background: var(--color-accent); - color: white; - border-radius: var(--radius-md); - font-size: var(--text-sm); - font-weight: 500; - text-decoration: none; - transition: all var(--transition-fast); - align-self: flex-start; -} - -.wechat-open-platform-link:hover { - background: var(--color-accent-hover); - text-decoration: none; - color: white; -} - -/* ===== QwenPaw Tooltip ===== */ -.qwp-tooltip-wrapper { - position: relative; - display: inline-flex; - align-items: center; -} - -.qwp-info-icon { - display: inline-flex; - align-items: center; - justify-content: center; - width: 16px; - height: 16px; - border-radius: 50%; - color: var(--color-text-tertiary); - cursor: help; - transition: color var(--transition-fast); -} -.qwp-info-icon:hover { - color: var(--color-accent); -} - -.qwp-tooltip-popup { - display: none; - position: absolute; - bottom: calc(100% + 8px); - left: 50%; - transform: translateX(-50%); - background: #101214; - color: #fff; - padding: 6px 12px; - border-radius: var(--radius-md); - font-size: 11px; - font-weight: 400; - line-height: 1.5; - white-space: nowrap; - z-index: var(--z-overlay); - pointer-events: none; - max-width: 260px; - white-space: normal; - word-break: break-word; -} - -.qwp-tooltip-popup::after { - content: ''; - position: absolute; - top: 100%; - left: 50%; - transform: translateX(-50%); - border: 5px solid transparent; - border-top-color: #101214; -} - -.qwp-tooltip-wrapper:hover .qwp-tooltip-popup { - display: block; -} - -/* ===== QwenPaw Toggle Switch ===== */ -.qwp-toggle { - position: relative; - width: 40px; - height: 22px; - border-radius: var(--radius-full); - background: var(--color-border); - border: none; - cursor: pointer; - transition: background var(--transition-fast); - flex-shrink: 0; - padding: 0; -} - -.qwp-toggle.active { - background: var(--color-accent); -} - -.qwp-toggle:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.qwp-toggle-knob { - position: absolute; - top: 2px; - left: 2px; - width: 18px; - height: 18px; - border-radius: 50%; - background: white; - box-shadow: var(--shadow-sm); - transition: transform var(--transition-fast); -} - -.qwp-toggle.active .qwp-toggle-knob { - transform: translateX(18px); -} - -/* ===== QwenPaw Tag Input ===== */ -.qwp-tag-input { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: var(--space-xs); - padding: 6px 10px; - border: 1px solid var(--color-border); - border-radius: var(--radius-md); - background: #FFFFFF; - min-height: 38px; - transition: border-color var(--transition-fast); -} -.qwp-tag-input:focus-within { - border-color: var(--color-accent); - box-shadow: 0 0 0 3px var(--color-accent-subtle); -} - -.qwp-tag { - display: inline-flex; - align-items: center; - gap: 3px; - padding: 2px 8px; - background: var(--color-accent-subtle); - color: var(--color-accent); - border-radius: var(--radius-full); - font-size: var(--text-xs); - font-weight: 500; -} - -.qwp-tag-remove { - display: inline-flex; - align-items: center; - justify-content: center; - width: 14px; - height: 14px; - border-radius: 50%; - font-size: 10px; - color: var(--color-accent); - opacity: 0.7; - transition: opacity var(--transition-fast); - line-height: 1; -} -.qwp-tag-remove:hover { - opacity: 1; - background: rgba(91, 95, 237, 0.15); -} - -.qwp-tag-input-field { - border: none; - outline: none; - font-size: var(--text-sm); - color: var(--color-text-primary); - background: transparent; - flex: 1; - min-width: 100px; - padding: 0; - font-family: inherit; -} -.qwp-tag-input-field::placeholder { - color: var(--color-text-disabled); -} - -/* ===== Extra Settings Section ===== */ -.extra-settings-section { - display: flex; - flex-direction: column; - gap: var(--space-lg); - padding: var(--space-lg); - background: var(--color-bg-tertiary); - border-radius: var(--radius-lg); - border: 1px solid var(--color-border-light); -} - -.extra-settings-title { - font-size: var(--text-xs); - font-weight: 700; - color: var(--color-text-secondary); - text-transform: uppercase; - letter-spacing: 0.5px; - margin-bottom: -4px; -} - -/* ===== Animations ===== */ -@keyframes spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } -} - -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } -} - -@keyframes slideUp { - from { opacity: 0; transform: translateY(20px); } - to { opacity: 1; transform: translateY(0); } -} - -/* ===== Responsive ===== */ -@media (max-width: 768px) { - .channel-list-view { padding: var(--space-lg); } - .channel-detail-body { padding: var(--space-lg); } - .bind-platform-list { grid-template-columns: repeat(2, 1fr); } - .platform-config-modal { padding: var(--space-xl); } - .channel-intro-banner { flex-direction: column; text-align: center; } - .credential-grid { grid-template-columns: 1fr; } - .detail-meta-row { gap: var(--space-md); } - .ai-readonly-grid { grid-template-columns: 1fr; } - .ai-settings-form-scroll { max-height: 40vh; } -} - -/* ===== QR Mock Badge ===== */ -.qr-mock-badge { - position: absolute; - bottom: var(--space-sm); - left: 50%; - transform: translateX(-50%); - background: #E8922C; - color: white; - font-size: 11px; - padding: 2px 8px; - border-radius: var(--radius-sm); - font-weight: 500; - white-space: nowrap; -} diff --git a/frontend/src/pages/front/ChannelPage.tsx b/frontend/src/pages/front/ChannelPage.tsx deleted file mode 100644 index 74a0a4d..0000000 --- a/frontend/src/pages/front/ChannelPage.tsx +++ /dev/null @@ -1,2012 +0,0 @@ -import React, { useState, useEffect, useRef, useCallback } from 'react'; -import type { IMPlatform } from '@/types'; -import { channelAPI, knowledgeAPI, marketAPI } from '@/api'; -import './ChannelPage.css'; - -// ===== Platform Config from API ===== -interface CredentialFieldDef { - key: string; - label: string; - type: 'text' | 'password'; - required: boolean; - placeholder: string; - tooltip?: string; -} - -interface AISettingsDef { - is_active: boolean; - bot_prefix: string; - show_tool_msg: boolean; - show_thinking: boolean; - stream_output: boolean; - private_chat_strategy: string; - group_chat_strategy: string; - require_mention: boolean; - whitelist_users: string[]; - media_dir: string; -} - -interface ExtraSettingsDef { - message_merge?: boolean; - merge_delay?: number; -} - -interface PlatformTip { - type: 'info' | 'warning'; - content: string; -} - -interface QRConfig { - title: string; - description: string; - qr_url: string; - action_label: string; - flow_hint: string; - config_guide?: boolean; -} - -interface PlatformConfig { - name: string; - description: string; - credential_fields: CredentialFieldDef[]; - ai_settings: AISettingsDef; - extra_settings?: ExtraSettingsDef; - platform_tips: PlatformTip[]; - enable_region: boolean; - regions: string[]; - doc_url: string; - qr_config?: QRConfig; -} - -// ===== Constants ===== -const platformLabels: Record = { - feishu: '飞书', wechat: '微信', dingtalk: '钉钉', - whatsapp: 'WhatsApp', telegram: 'Telegram', slack: 'Slack', -}; - -function PIcons(p: IMPlatform): JSX.Element { - switch (p) { - case 'feishu': return ; - case 'wechat': return ; - case 'dingtalk': return ; - case 'whatsapp': return ; - case 'telegram': return ; - case 'slack': return ; - } -} - -const platformColors: Record = { - feishu: { color: '#3370FF', bg: '#EDF2FF' }, - wechat: { color: '#07C160', bg: '#E8F8EE' }, - dingtalk: { color: '#0089FF', bg: '#E6F3FF' }, - whatsapp: { color: '#25D366', bg: '#E8F8EF' }, - telegram: { color: '#26A5E4', bg: '#EBF5FC' }, - slack: { color: '#4A154B', bg: '#F5EEF5' }, -}; - -const defaultAISettings: AISettingsDef = { - is_active: true, - bot_prefix: '', - show_tool_msg: true, - show_thinking: false, - stream_output: true, - private_chat_strategy: 'open', - group_chat_strategy: 'open', - require_mention: false, - whitelist_users: [], - media_dir: '', -}; - -// ===== SVG Icons ===== -const PlusIcon = () => ( - - - -); -const LinkIcon = () => ( - - - -); -const UnlinkIcon = () => ( - - - -); -const BotIcon = () => ( - - - -); -const BookIcon = () => ( - - - -); -const ChipIcon = () => ( - - - -); -const PhoneIcon = () => ( - - - -); -const MessageIcon = () => ( - - - -); -const CheckIcon = () => ( - - - -); -const ClockIcon = () => ( - - - -); -const CloseIcon = () => ( - - - -); -const SendIcon = () => ( - - - -); -const EyeIcon = () => ( - - - -); -const EyeOffIcon = () => ( - - - -); -const InfoCircleIcon = () => ( - - - -); -const AlertTriangleIcon = () => ( - - - -); -const DocIcon = () => ( - - - -); -const EditIcon = () => ( - - - -); - -// ===== Helpers ===== -function formatTime(dateStr?: string): string { - if (!dateStr) return '--'; - const d = new Date(dateStr); - const now = new Date(); - const diff = now.getTime() - d.getTime(); - const mins = Math.floor(diff / 60000); - if (mins < 1) return '刚刚'; - if (mins < 60) return mins + '分钟前'; - const hours = Math.floor(mins / 60); - if (hours < 24) return hours + '小时前'; - const days = Math.floor(hours / 24); - if (days < 7) return days + '天前'; - return d.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' }); -} - -function maskCredentialValue(value: string): string { - if (!value) return ''; - if (value.length <= 8) return value.substring(0, 2) + '****'; - return value.substring(0, 4) + '***...***' + value.substring(value.length - 4); -} - -const statusLabels: Record = { - connected: { label: '已连接', cls: 'status-connected' }, - disconnected: { label: '已断开', cls: 'status-disconnected' }, - pending: { label: '待连接', cls: 'status-pending' }, -}; - -// ===== Tooltip Component ===== -const Tooltip: React.FC<{ text: string; children?: React.ReactNode }> = ({ text, children }) => ( - - {children || } - {text} - -); - -// ===== Toggle Switch Component ===== -const ToggleSwitch: React.FC<{ - checked: boolean; - onChange: (v: boolean) => void; - disabled?: boolean; -}> = ({ checked, onChange, disabled }) => ( - -); - -// ===== Tag Input Component ===== -const TagInput: React.FC<{ - tags: string[]; - onChange: (tags: string[]) => void; - placeholder?: string; - disabled?: boolean; -}> = ({ tags, onChange, placeholder = '输入ID后按回车添加', disabled }) => { - const [inputVal, setInputVal] = useState(''); - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && inputVal.trim()) { - e.preventDefault(); - const val = inputVal.trim(); - if (!tags.includes(val)) { - onChange([...tags, val]); - } - setInputVal(''); - } - if (e.key === 'Backspace' && !inputVal && tags.length > 0) { - onChange(tags.slice(0, -1)); - } - }; - return ( -
- {tags.map(tag => ( - - {tag} - - - ))} - setInputVal(e.target.value)} - onKeyDown={handleKeyDown} - placeholder={tags.length === 0 ? placeholder : ''} - disabled={disabled} - /> -
- ); -}; - -// ===== Confirm Modal ===== -const ConfirmModal: React.FC<{ - title: string; - message: string; - onConfirm: () => void; - onCancel: () => void; - confirmText?: string; - cancelText?: string; - danger?: boolean; -}> = ({ title, message, onConfirm, onCancel, confirmText = '确定', cancelText = '取消', danger }) => ( -
-
e.stopPropagation()}> -
-

{title}

- -
-

{message}

-
- - -
-
-
-); - -// ===== Channel Data Types ===== -interface ChannelData { - id: string; - name: string; - description?: string; - platform: IMPlatform; - status: string; - is_active?: boolean; - webhook_url?: string; - credential?: Record; - credential_masked?: Record; - settings?: { - agent_id?: string; - knowledge_base_id?: string; - skill_ids?: string[]; - welcome_message?: string; - bot_prefix?: string; - show_tool_msg?: boolean; - show_thinking?: boolean; - stream_output?: boolean; - private_chat_strategy?: string; - group_chat_strategy?: string; - require_mention?: boolean; - whitelist_users?: string[]; - media_dir?: string; - message_merge?: boolean; - merge_delay?: number; - is_active?: boolean; - }; - role?: string; - member_count?: number; - message_count?: number; - default_agent_id?: string; - default_skill_id?: string; - knowledge_base_id?: string; - bound_model_id?: string; - last_active?: string; - created_at?: string; - updated_at?: string; - region?: string; -} - -interface ChatMessage { - id: string; - role: string; - content: string; - from_user?: string; - created_at: string; -} - -// ===== Create Channel Modal (3 Steps) ===== -const CreateChannelModal: React.FC<{ - onComplete: () => void; - onClose: () => void; -}> = ({ onComplete, onClose }) => { - const [step, setStep] = useState(1); - const [selectedPlatform, setSelectedPlatform] = useState('feishu'); - const [channelName, setChannelName] = useState(''); - const [channelDesc, setChannelDesc] = useState(''); - const [creds, setCreds] = useState>({}); - const [showPasswords, setShowPasswords] = useState>({}); - const [region, setRegion] = useState(''); - const [aiSettings, setAiSettings] = useState({ ...defaultAISettings }); - const [extraSettings, setExtraSettings] = useState({}); - const [platformCfgs, setPlatformCfgs] = useState>({}); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [connectResult, setConnectResult] = useState(null); - const [qrDataUrl, setQrDataUrl] = useState(null); - const [scanStatus, setScanStatus] = useState(''); - const [scanMessage, setScanMessage] = useState(''); - - // Handle generate QR code for scan authorization - const handleGenerateQR = async () => { - try { - setScanStatus('loading'); - setScanMessage('正在生成授权二维码...'); - const res = await channelAPI.scanAuthorize(selectedPlatform, creds); - const data = res.data || res; - if (data.qr_url) { - setPlatformCfgs((prev: any) => ({ - ...prev, - [selectedPlatform]: { - ...prev[selectedPlatform], - qr_config: { - ...(prev[selectedPlatform]?.qr_config || {}), - qr_url: data.qr_url, - }, - }, - })); - setScanStatus(data.status || 'waiting'); - setScanMessage(data.message || '请使用微信扫描二维码完成授权'); - - // Start polling for scan status (wechat always needs polling) - pollScanStatus(); - } else if (data.status === 'need_credentials') { - setScanStatus('need_credentials'); - setScanMessage(data.message || '请先在下方填写AppID和AppSecret'); - } else { - setScanStatus('error'); - setScanMessage(data.message || '生成二维码失败'); - } - } catch (err: any) { - setScanStatus('error'); - setScanMessage(err.message || '生成二维码失败'); - } - }; - - // Poll scan status for ClawBot authorization - const pollScanStatus = async () => { - const maxAttempts = 60; // Poll up to 5 minutes (5s intervals) - let attempts = 0; - const interval = setInterval(async () => { - attempts++; - if (attempts > maxAttempts) { - clearInterval(interval); - setScanStatus('expired'); - setScanMessage('二维码已过期,请重新生成'); - return; - } - try { - const res = await channelAPI.scanAuthorizeStatus(selectedPlatform); - const data = res.data || res; - if (data.status === 'authorized') { - clearInterval(interval); - setScanStatus('authorized'); - setScanMessage('授权成功!微信个人号已绑定'); - } else if (data.status === 'scaned') { - setScanMessage('二维码已扫描,请在微信中确认授权...'); - } else if (data.status === 'expired') { - clearInterval(interval); - setScanStatus('expired'); - setScanMessage('二维码已过期,请重新生成'); - } - } catch (err) { - // Silently ignore polling errors - } - }, 5000); - }; - - // Update qrDataUrl when qr_url changes (directly use the URL from ClawBot API) - useEffect(() => { - const cfg = platformCfgs[selectedPlatform]; - const qrUrl = cfg?.qr_config?.qr_url; - if (!qrUrl) { - setQrDataUrl(null); - return; - } - // Use the URL directly - it's a standard https URL from ClawBot API - setQrDataUrl(qrUrl); - }, [selectedPlatform, platformCfgs]); - - useEffect(() => { - const loadConfigs = async () => { - try { - const res = await channelAPI.getPlatformConfigs(); - if (res.code === 0 && res.data && typeof res.data === 'object') { - setPlatformCfgs(res.data as Record); - } - } catch { /* ignore */ } - }; - loadConfigs(); - }, []); - - // Reset ai settings when platform changes - useEffect(() => { - const cfg = platformCfgs[selectedPlatform]; - if (cfg && cfg.ai_settings) { - setAiSettings({ ...cfg.ai_settings }); - setExtraSettings(cfg.extra_settings || {}); - } else { - setAiSettings({ ...defaultAISettings }); - setExtraSettings({}); - } - setRegion(''); - setCreds({}); - setShowPasswords({}); - }, [selectedPlatform, platformCfgs]); - - const currentCfg = platformCfgs[selectedPlatform]; - const platformLabel = platformLabels[selectedPlatform]; - - const handleNext = () => { - if (!channelName.trim()) { - setError('请输入频道名称'); - return; - } - setError(null); - setStep(2); - }; - - const handleNextToAI = () => { - const cfgFields = currentCfg?.credential_fields || []; - for (const f of cfgFields) { - if (f.required && (!creds[f.key] || !creds[f.key].trim())) { - setError('请填写必填字段: ' + f.label); - return; - } - } - setError(null); - setStep(3); - }; - - const handleSave = async () => { - setLoading(true); - setError(null); - try { - const cfgFields = currentCfg?.credential_fields || []; - const credential: Record = {}; - for (const f of cfgFields) { - if (creds[f.key] && creds[f.key].trim()) { - credential[f.key] = creds[f.key].trim(); - } - } - if (region) { - credential['region'] = region; - } - - const settings: Record = { - ...aiSettings, - ...extraSettings, - }; - - const createRes = await channelAPI.createChannel({ - name: channelName.trim(), - description: channelDesc.trim() || ('通过' + platformLabel + '平台接入配置'), - platform: selectedPlatform, - credential, - settings, - }); - - if (createRes.code !== 0) { - setError(createRes.message || '创建频道失败'); - setLoading(false); - return; - } - - const channelId = createRes.data?.id; - if (!channelId) { - setError('创建频道失败:未获取到频道ID'); - setLoading(false); - return; - } - - try { - const connRes = await channelAPI.connectChannel(channelId); - if (connRes.code === 0) { - setConnectResult(connRes.data); - } else { - setConnectResult({ connected: false, message: connRes.message || '连接失败' }); - } - } catch (e) { - setConnectResult({ connected: false, message: (e as Error).message }); - } - - setLoading(false); - onComplete(); - } catch (e) { - setError('操作失败: ' + (e as Error).message); - setLoading(false); - } - }; - - // ===== Step renderers ===== - const renderStep1 = () => ( - <> -

选择你要接入的IM平台,配置官方接入凭证完成绑定

- -
-
- - setChannelName(e.target.value)} - /> -
-
- - setChannelDesc(e.target.value)} - /> -
-
- -
选择IM平台
-
- {(Object.keys(platformLabels) as IMPlatform[]).map(key => { - const pc = platformColors[key]; - return ( - - ); - })} -
- - {error &&
{error}
} - -
- - -
- - ); - - const renderStep2 = () => ( - <> -
- 1 - → - 2 - → - 3 - {platformLabel} 凭证配置 -
-

- 填写{platformLabel}官方的接入凭证信息,配置说明请参考官方文档 -

- - {/* Platform Tips */} - {currentCfg && Array.isArray(currentCfg.platform_tips) && currentCfg.platform_tips.length > 0 && ( -
- {currentCfg.platform_tips.map((tip, idx) => ( -
- {tip.type === 'warning' ? : } - {tip.content} -
- ))} -
- )} - - {/* QR Code Scan Section / WeChat Config Guide */} - {currentCfg?.qr_config && ( -
-
- - - - - - - {currentCfg.qr_config.title} -
-

{currentCfg.qr_config.description}

- - {/* QR Code / Scan Authorization - Unified for all platforms */} -
-
- {qrDataUrl ? ( - QR Code - ) : ( -
- {scanStatus === 'loading' ? ( - 正在生成二维码... - ) : scanStatus === 'error' || scanStatus === 'need_credentials' ? ( - {scanMessage || '生成失败,请检查配置'} - ) : ( - '点击下方按钮生成二维码' - )} -
- )} -
-
- - {scanMessage && ( - - {scanMessage} - - )} - {!scanMessage && ( - {currentCfg.qr_config.flow_hint} - )} -
-
-
- )} - - {/* Credential Fields */} - {currentCfg && Array.isArray(currentCfg.credential_fields) && ( -
- {currentCfg.credential_fields.map(field => ( -
- - {field.type === 'password' ? ( -
- setCreds(prev => ({ ...prev, [field.key]: e.target.value }))} - /> - -
- ) : ( - setCreds(prev => ({ ...prev, [field.key]: e.target.value }))} - /> - )} -
- ))} - - {/* Region selector */} - {currentCfg.enable_region && Array.isArray(currentCfg.regions) && currentCfg.regions.length > 0 && ( -
- - -
- )} - - {/* Doc link */} - {currentCfg.doc_url && ( - - 查看{platformLabel}官方接入文档 - - )} -
- )} - - {error &&
{error}
} - -
- - - -
- - ); - - const renderStep3 = () => ( - <> -
- 1 - → - 2 - → - 3 - AI 行为配置 -
-

配置 AI 机器人在 {platformLabel} 中的行为规则

- -
- {/* is_active */} -
- - setAiSettings(prev => ({ ...prev, is_active: v }))} - /> -
- - {/* bot_prefix */} -
- - setAiSettings(prev => ({ ...prev, bot_prefix: e.target.value }))} - /> -
- - {/* show_tool_msg */} -
- - setAiSettings(prev => ({ ...prev, show_tool_msg: v }))} - /> -
- - {/* show_thinking */} -
- - setAiSettings(prev => ({ ...prev, show_thinking: v }))} - /> -
- - {/* stream_output */} -
- - setAiSettings(prev => ({ ...prev, stream_output: v }))} - /> -
- - {/* private_chat_strategy */} -
- - -
- - {/* group_chat_strategy */} -
- - -
- - {/* require_mention */} -
- - setAiSettings(prev => ({ ...prev, require_mention: v }))} - /> -
- - {/* whitelist_users */} -
- - setAiSettings(prev => ({ ...prev, whitelist_users: tags }))} - placeholder="输入用户ID后按回车添加" - /> -
- - {/* media_dir */} -
- - setAiSettings(prev => ({ ...prev, media_dir: e.target.value }))} - /> -
- - {/* Wechat extra settings */} - {selectedPlatform === 'wechat' && ( -
-
微信额外设置
- -
- - setExtraSettings(prev => ({ ...prev, message_merge: v }))} - /> -
- -
- - setExtraSettings(prev => ({ ...prev, merge_delay: Number(e.target.value) }))} - /> -
-
- )} -
- - {error &&
{error}
} - - {connectResult && ( -
-
- {connectResult.connected ? : !} -
-
- {connectResult.connected ? '连接成功' : '连接失败'} - {connectResult.message} -
-
- )} - -
- - - -
- - ); - - return ( -
-
e.stopPropagation()}> -
-

接入新平台

- -
- - {step === 1 && renderStep1()} - {step === 2 && renderStep2()} - {step === 3 && renderStep3()} -
-
- ); -}; - -// ===== Channel Detail Page ===== -const ChannelDetail: React.FC<{ - channel: ChannelData; - onBack: () => void; - onDelete: (id: string) => void; - onRefresh: () => void; -}> = ({ channel, onBack, onDelete, onRefresh }) => { - const platform = channel.platform as IMPlatform; - const pc = platformColors[platform] || platformColors.feishu; - const [activeTab, setActiveTab] = useState<'info' | 'chat'>('info'); - - // Info tab - existing settings - const [settingAgentId, setSettingAgentId] = useState(channel.settings?.agent_id || channel.default_agent_id || ''); - const [settingKbId, setSettingKbId] = useState(channel.settings?.knowledge_base_id || channel.knowledge_base_id || ''); - const [settingSkillIds, setSettingSkillIds] = useState(channel.settings?.skill_ids || []); - const [settingWelcome, setSettingWelcome] = useState(channel.settings?.welcome_message || ''); - - // Info tab - QwenPaw AI settings - const [editingAI, setEditingAI] = useState(false); - const [aiSettings, setAiSettings] = useState({ - is_active: channel.settings?.is_active !== undefined ? channel.settings.is_active : true, - bot_prefix: channel.settings?.bot_prefix || '', - show_tool_msg: channel.settings?.show_tool_msg !== undefined ? channel.settings.show_tool_msg : true, - show_thinking: channel.settings?.show_thinking || false, - stream_output: channel.settings?.stream_output !== undefined ? channel.settings.stream_output : true, - private_chat_strategy: channel.settings?.private_chat_strategy || 'open', - group_chat_strategy: channel.settings?.group_chat_strategy || 'open', - require_mention: channel.settings?.require_mention || false, - whitelist_users: channel.settings?.whitelist_users || [], - media_dir: channel.settings?.media_dir || '', - }); - const [extraSettings, setExtraSettings] = useState({ - message_merge: channel.settings?.message_merge || false, - merge_delay: channel.settings?.merge_delay, - }); - - const [agents, setAgents] = useState([]); - const [knowledgeBases, setKnowledgeBases] = useState([]); - const [skills, setSkills] = useState([]); - const [savingSettings, setSavingSettings] = useState(false); - const [savingAI, setSavingAI] = useState(false); - const [settingsMsg, setSettingsMsg] = useState(null); - - // Credential display - const [showCredential, setShowCredential] = useState(false); - const [showCredValues, setShowCredValues] = useState>({}); - - // Chat tab states - const [messages, setMessages] = useState([]); - const [inputText, setInputText] = useState(''); - const [sending, setSending] = useState(false); - const chatEndRef = useRef(null); - - // Confirm modal - const [confirmModal, setConfirmModal] = useState<{ type: 'delete' | 'disconnect' } | null>(null); - - // Load options - useEffect(() => { - const loadOptions = async () => { - try { - const [agentRes, kbRes, skillRes] = await Promise.all([ - marketAPI.getApps({ type: 'agent' }), - knowledgeAPI.getBases(), - marketAPI.getApps({ type: 'skill' }), - ]); - if (agentRes.code === 0) { - const agentData: any = agentRes.data; - const rawAgents = agentData?.items || agentData || []; - setAgents(Array.isArray(rawAgents) ? rawAgents : []); - } - if (kbRes.code === 0) { - const kbData: any = kbRes.data; - const rawBases = kbData?.bases || kbData || []; - setKnowledgeBases(Array.isArray(rawBases) ? rawBases : []); - } - if (skillRes.code === 0) { - const skillData: any = skillRes.data; - const rawSkills = skillData?.items || skillData || []; - setSkills(Array.isArray(rawSkills) ? rawSkills : []); - } - } catch { /* ignore */ } - }; - loadOptions(); - }, []); - - // Load messages - const loadMessages = useCallback(async () => { - try { - const res = await channelAPI.getMessages(channel.id, { limit: 100 }); - if (res.code === 0) { - const rawMessages = res.data || []; - const msgArray = Array.isArray(rawMessages) ? [...rawMessages] : []; - setMessages(msgArray.reverse().map((m: any) => ({ - id: m.id, - role: m.role, - content: m.content, - from_user: m.username, - created_at: m.created_at, - }))); - } - } catch { /* ignore */ } - }, [channel.id]); - - useEffect(() => { - if (activeTab === 'chat') loadMessages(); - }, [activeTab, loadMessages]); - - useEffect(() => { - if (chatEndRef.current) { - chatEndRef.current.scrollIntoView({ behavior: 'smooth' }); - } - }, [messages]); - - useEffect(() => { - if (activeTab !== 'chat') return; - const interval = setInterval(loadMessages, 5000); - return () => clearInterval(interval); - }, [activeTab, loadMessages]); - - // Save existing settings (agent/kb/skills/welcome) - const handleSaveSettings = async () => { - setSavingSettings(true); - setSettingsMsg(null); - try { - const newSettings: Record = {}; - if (settingAgentId) newSettings.agent_id = settingAgentId; - if (settingKbId) newSettings.knowledge_base_id = settingKbId; - if (settingSkillIds.length > 0) newSettings.skill_ids = settingSkillIds; - if (settingWelcome) newSettings.welcome_message = settingWelcome; - - const res = await channelAPI.updateChannel(channel.id, { settings: newSettings }); - if (res.code === 0) { - setSettingsMsg('配置保存成功'); - onRefresh(); - } else { - setSettingsMsg('保存失败: ' + res.message); - } - } catch (e) { - setSettingsMsg('保存失败: ' + (e as Error).message); - } finally { - setSavingSettings(false); - } - }; - - // Save AI settings - const handleSaveAI = async () => { - setSavingAI(true); - setSettingsMsg(null); - try { - const newSettings: Record = { - ...aiSettings, - ...extraSettings, - }; - // Preserve existing non-AI settings - if (settingAgentId) newSettings.agent_id = settingAgentId; - if (settingKbId) newSettings.knowledge_base_id = settingKbId; - if (settingSkillIds.length > 0) newSettings.skill_ids = settingSkillIds; - if (settingWelcome) newSettings.welcome_message = settingWelcome; - - const res = await channelAPI.updateChannel(channel.id, { settings: newSettings }); - if (res.code === 0) { - setSettingsMsg('AI 配置保存成功'); - setEditingAI(false); - onRefresh(); - } else { - setSettingsMsg('保存失败: ' + res.message); - } - } catch (e) { - setSettingsMsg('保存失败: ' + (e as Error).message); - } finally { - setSavingAI(false); - } - }; - - const handleSendMessage = async () => { - if (!inputText.trim() || sending) return; - setSending(true); - try { - const res = await channelAPI.webhookMessage(channel.id, { - from_user: '手机用户', - content: inputText.trim(), - message_type: 'text', - }); - if (res.code === 0) { - setInputText(''); - if (res.data?.received) { - setMessages(prev => [...prev, { - id: res.data.received.id, - role: 'user', - content: res.data.received.content, - from_user: res.data.received.from_user, - created_at: res.data.received.created_at, - }]); - } - if (res.data?.reply) { - setMessages(prev => [...prev, { - id: res.data.reply.id, - role: 'assistant', - content: res.data.reply.content, - created_at: res.data.reply.created_at, - }]); - } - } - } catch (e) { - // ignore - } finally { - setSending(false); - } - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - handleSendMessage(); - } - }; - - const handleConnect = async () => { - try { - const res = await channelAPI.connectChannel(channel.id); - if (res.code === 0) { - onRefresh(); - } else { - alert(res.message || '连接失败'); - } - } catch (e) { - alert('连接失败: ' + (e as Error).message); - } - }; - - const handleDisconnect = async () => { - setConfirmModal(null); - try { - const res = await channelAPI.disconnectChannel(channel.id); - if (res.code === 0) { - onRefresh(); - } - } catch (e) { - alert('断开失败: ' + (e as Error).message); - } - }; - - const handleDelete = async () => { - setConfirmModal(null); - onDelete(channel.id); - }; - - const toggleSkill = (skillId: string) => { - setSettingSkillIds(prev => - prev.includes(skillId) - ? prev.filter(id => id !== skillId) - : [...prev, skillId] - ); - }; - - return ( -
-
-
- -

{channel.name}

-
- - - {statusLabels[channel.status]?.label || channel.status} - -
-
- - {/* Tabs */} -
- - -
- -
- {activeTab === 'info' && ( - <> - {/* Card 1: Platform Info */} -
-
平台信息
-
-
- {PIcons(platform)} -
-
-
{platformLabels[platform]}
-
- {channel.status === 'connected' ? 'AI机器人已在线' : '未连接'} -
-
- - - {statusLabels[channel.status]?.label || channel.status} - -
-
- 创建时间:{channel.created_at ? new Date(channel.created_at).toLocaleDateString('zh-CN') : '--'} - 最近活跃:{formatTime(channel.last_active || channel.updated_at)} - 成员数:{channel.member_count || 1} - 消息数:{channel.message_count || 0} -
-
- {channel.status === 'connected' ? ( - - ) : ( - - )} - -
-
- - {/* Card 2: Connection Config (Credentials) */} -
-
- 连接配置 - -
- {channel.credential_masked && Object.keys(channel.credential_masked).length > 0 ? ( - <> -
- {Object.entries(channel.credential_masked).map(([key, val]) => ( -
- {key} -
- - {showCredential - ? (showCredValues[key] - ? (channel.credential?.[key] || String(val)) - : maskCredentialValue(channel.credential?.[key] || String(val))) - : String(val)} - - {showCredential && channel.credential?.[key] && ( - - )} -
-
- ))} -
- {channel.webhook_url && ( -
- webhook_url - {channel.webhook_url} -
- )} - - ) : ( -
-

尚未配置平台接入凭证

-

请删除此频道后重新创建,并填写各平台所需的接入凭证信息

-
- )} -
- - {/* Card 3: AI Configuration */} -
-
- AI配置 - {!editingAI && ( - - )} -
- - {!editingAI ? ( -
-
-
- 已启用 - - {aiSettings.is_active ? '是' : '否'} - -
-
- Bot Prefix - {aiSettings.bot_prefix || '未设置'} -
-
- 显示工具消息 - {aiSettings.show_tool_msg ? '是' : '否'} -
-
- 显示思考过程 - {aiSettings.show_thinking ? '是' : '否'} -
-
- 流式输出 - {aiSettings.stream_output ? '是' : '否'} -
-
- 私聊策略 - - {aiSettings.private_chat_strategy === 'open' ? '开放' : - aiSettings.private_chat_strategy === 'whitelist' ? '白名单' : '关闭'} - -
-
- 群聊策略 - - {aiSettings.group_chat_strategy === 'open' ? '开放' : - aiSettings.group_chat_strategy === 'mention' ? '需要@提及' : - aiSettings.group_chat_strategy === 'whitelist' ? '白名单' : '关闭'} - -
-
- 需要@提及 - {aiSettings.require_mention ? '是' : '否'} -
-
- 白名单用户 - - {aiSettings.whitelist_users && aiSettings.whitelist_users.length > 0 - ? aiSettings.whitelist_users.join(', ') - : '未设置'} - -
-
- 媒体文件目录 - {aiSettings.media_dir || '未设置'} -
- {platform === 'wechat' && ( - <> -
- 消息合并 - {extraSettings.message_merge ? '是' : '否'} -
-
- 合并延时 - - {extraSettings.merge_delay !== undefined ? extraSettings.merge_delay + 'ms' : '未设置'} - -
- - )} -
-
- ) : ( -
- {/* is_active */} -
- - setAiSettings(prev => ({ ...prev, is_active: v }))} - /> -
- - {/* bot_prefix */} -
- - setAiSettings(prev => ({ ...prev, bot_prefix: e.target.value }))} - /> -
- - {/* show_tool_msg */} -
- - setAiSettings(prev => ({ ...prev, show_tool_msg: v }))} - /> -
- - {/* show_thinking */} -
- - setAiSettings(prev => ({ ...prev, show_thinking: v }))} - /> -
- - {/* stream_output */} -
- - setAiSettings(prev => ({ ...prev, stream_output: v }))} - /> -
- - {/* private_chat_strategy */} -
- - -
- - {/* group_chat_strategy */} -
- - -
- - {/* require_mention */} -
- - setAiSettings(prev => ({ ...prev, require_mention: v }))} - /> -
- - {/* whitelist_users */} -
- - setAiSettings(prev => ({ ...prev, whitelist_users: tags }))} - placeholder="输入用户ID后按回车添加" - /> -
- - {/* media_dir */} -
- - setAiSettings(prev => ({ ...prev, media_dir: e.target.value }))} - /> -
- - {/* Wechat extras */} - {platform === 'wechat' && ( -
-
微信额外设置
- -
- - setExtraSettings(prev => ({ ...prev, message_merge: v }))} - /> -
- -
- - setExtraSettings(prev => ({ ...prev, merge_delay: Number(e.target.value) }))} - /> -
- -
- )} - - {/* Bind Agent / KnowledgeBase / Skills */} -
-
绑定资源
- -
- - -
- -
- - -
- -
- -
- {skills.map((s: any) => ( - - ))} - {skills.length === 0 && 暂无可用技能} -
-
- -
- -