diff --git a/audio_ai_chat/.env b/audio_ai_chat/.env index 6eda825..4c0e46e 100644 --- a/audio_ai_chat/.env +++ b/audio_ai_chat/.env @@ -6,9 +6,10 @@ LOG_LEVEL=INFO LOG_FILE=logs/app.log # ASR服务配置 -ASR_SERVICE_URL=http://localhost:5000/asr -ASR_TIMEOUT=30 # 超时时间(秒) -ASR_RETRY_TIMES=2 # 重试次数 +ASR_HOST=10.10.10.202 +ASR_PORT=10096 +ASR_TIMEOUT=30 # 超时时间(秒) +ASR_RETRY_TIMES=2 # 重试次数 # LLM服务配置 LLM_SERVICE_URL=http://localhost:6000/chat diff --git a/audio_ai_chat/.idea/dictionaries/project.xml b/audio_ai_chat/.idea/dictionaries/project.xml new file mode 100644 index 0000000..cf44085 --- /dev/null +++ b/audio_ai_chat/.idea/dictionaries/project.xml @@ -0,0 +1,7 @@ + + + + tymas + + + \ No newline at end of file diff --git a/audio_ai_chat/audio_ai_chat/__pycache__/__init__.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/__pycache__/__init__.cpython-310.pyc index 1f30bf6..2d4294d 100644 Binary files a/audio_ai_chat/audio_ai_chat/__pycache__/__init__.cpython-310.pyc and b/audio_ai_chat/audio_ai_chat/__pycache__/__init__.cpython-310.pyc differ diff --git a/audio_ai_chat/audio_ai_chat/__pycache__/main.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/__pycache__/main.cpython-310.pyc index 8153a46..9486623 100644 Binary files a/audio_ai_chat/audio_ai_chat/__pycache__/main.cpython-310.pyc and b/audio_ai_chat/audio_ai_chat/__pycache__/main.cpython-310.pyc differ diff --git a/audio_ai_chat/audio_ai_chat/codec/__pycache__/ProtocolCodec.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/codec/__pycache__/ProtocolCodec.cpython-310.pyc index f30cb9d..653bf09 100644 Binary files a/audio_ai_chat/audio_ai_chat/codec/__pycache__/ProtocolCodec.cpython-310.pyc and b/audio_ai_chat/audio_ai_chat/codec/__pycache__/ProtocolCodec.cpython-310.pyc differ diff --git a/audio_ai_chat/audio_ai_chat/codec/__pycache__/__init__.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/codec/__pycache__/__init__.cpython-310.pyc index 8784a48..2e10ddf 100644 Binary files a/audio_ai_chat/audio_ai_chat/codec/__pycache__/__init__.cpython-310.pyc and b/audio_ai_chat/audio_ai_chat/codec/__pycache__/__init__.cpython-310.pyc differ diff --git a/audio_ai_chat/audio_ai_chat/config/__pycache__/__init__.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/config/__pycache__/__init__.cpython-310.pyc index cafaf39..4a3dac3 100644 Binary files a/audio_ai_chat/audio_ai_chat/config/__pycache__/__init__.cpython-310.pyc and b/audio_ai_chat/audio_ai_chat/config/__pycache__/__init__.cpython-310.pyc differ diff --git a/audio_ai_chat/audio_ai_chat/config/__pycache__/logger.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/config/__pycache__/logger.cpython-310.pyc index 6675f07..3874682 100644 Binary files a/audio_ai_chat/audio_ai_chat/config/__pycache__/logger.cpython-310.pyc and b/audio_ai_chat/audio_ai_chat/config/__pycache__/logger.cpython-310.pyc differ diff --git a/audio_ai_chat/audio_ai_chat/config/__pycache__/settings.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/config/__pycache__/settings.cpython-310.pyc index 7486ba9..1aacaa3 100644 Binary files a/audio_ai_chat/audio_ai_chat/config/__pycache__/settings.cpython-310.pyc and b/audio_ai_chat/audio_ai_chat/config/__pycache__/settings.cpython-310.pyc differ diff --git a/audio_ai_chat/audio_ai_chat/config/settings.py b/audio_ai_chat/audio_ai_chat/config/settings.py index b99fa65..52744ec 100644 --- a/audio_ai_chat/audio_ai_chat/config/settings.py +++ b/audio_ai_chat/audio_ai_chat/config/settings.py @@ -1,20 +1,17 @@ from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic import Field from pathlib import Path +from typing import List ROOT_DIR = Path(__file__).parent.parent.parent + class Settings(BaseSettings): # 应用配置 APP_PORT: int = Field(default=8000, description="服务端口") LOG_LEVEL: str = Field(default="INFO", description="日志级别") LOG_FILE: Path = Field(default=ROOT_DIR / "logs/app.log", description="日志文件路径") - # ASR服务配置 - ASR_SERVICE_URL: str = Field(..., description="ASR服务地址") - ASR_TIMEOUT: int = Field(default=30, description="ASR超时时间(秒)") - ASR_RETRY_TIMES: int = Field(default=2, description="ASR重试次数") - # LLM服务配置 LLM_SERVICE_URL: str = Field(..., description="LLM服务地址") LLM_TIMEOUT: int = Field(default=60, description="LLM超时时间(秒)") @@ -38,20 +35,62 @@ class Settings(BaseSettings): # -------------------------- 新增:服务版本配置 -------------------------- # ASR当前使用版本(对应ASR_REGISTRY中的key) - ASR_CURRENT_VERSION: str = Field(default="local_v1", description="ASR服务当前版本") - # 本地ASR专属配置(仅local_v1版本使用) - LOCAL_ASR_MODEL_PATH: Path = Field(default=ROOT_DIR / "models/asr/local_model", description="本地ASR模型路径") - # 百度云ASR专属配置(仅baidu_v2版本使用) - BAIDU_ASR_API_KEY: str = Field(default="", description="百度云ASR API Key") - BAIDU_ASR_SECRET_KEY: str = Field(default="", description="百度云ASR Secret Key") + ASR_CURRENT_VERSION: str = Field(default="FunASR", description="ASR服务当前版本") + + # ASR 公共配置 + ASR_TIMEOUT: int = Field(default=5, description="ASR连接超时时间(秒)") + ASR_RETRY_TIMES: int = Field(default=3, description="ASR公共重试次数") + + # ASR服务配置 + # FunASR 专属配置 + ASR_HOST: str = Field(..., description="FunASR服务地址") + ASR_PORT: int = Field(..., description="FunASR服务端口") + ASR_MODE: str = Field(default="2pass", description="FunASR识别模式") + ASR_CHUNK_SIZE: List[int] = Field(default=[5, 10, 5], description="FunASR分片大小配置") + ASR_CHUNK_INTERVAL: int = Field(default=10, description="FunASR分片间隔(毫秒)") + ASR_USE_ITN: int = Field(default=1, description="FunASR是否启用数字转换(1=启用,0=禁用)") + ASR_HOTWORDS: str = Field(default="", description="FunASR热词列表(逗号分隔)") + ASR_RECONNECT_MAX_TIMES: int = Field(default=3, description="FunASR连接重连最大次数") + ASR_POOL_SIZE: int = Field(default=2, description="FunASR连接池大小") + ASR_AUDIO_QUEUE_SIZE: int = Field(default=10000, description="FunASR音频队列最大长度") + + # 音频参数(FunASR要求) + ASR_SAMPLE_RATE: int = Field(default=16000, description="音频采样率(Hz)") + ASR_CHANNELS: int = Field(default=1, description="音频声道数(1=单声道)") + ASR_SAMPLE_WIDTH: int = Field(default=2, description="音频采样宽度(字节)") + ASR_FRAME_SIZE: int = Field(default=1024, description="音频帧大小") # LLM当前使用版本(对应LLM_REGISTRY中的key) LLM_CURRENT_VERSION: str = Field(default="local", description="LLM服务当前版本") - # OpenAI LLM专属配置(仅openai版本使用) - OPENAI_API_KEY: str = Field(default="", description="OpenAI API Key") - OPENAI_BASE_URL: str = Field(default="https://api.openai.com/v1", description="OpenAI接口地址") - # 本地LLM专属配置(仅local版本使用) - LOCAL_LLM_MODEL_PATH: Path = Field(default=ROOT_DIR / "models/llm/local_model", description="本地LLM模型路径") + # -------------------------- Dify API 配置(专属) -------------------------- + DIFY_BASE_URL: str = Field( + default="http://10.10.10.202:8088/v1", + description="Dify平台API基础URL(如http://xxx:8088/v1)" + ) + DIFY_API_KEY: str = Field( + default="app-m7HZNV1aGiheh3wr6wNVHFxX", + description="Dify平台API密钥(在Dify应用设置中获取,格式为app-xxx)" + ) + DIFY_TIMEOUT: int = Field( + default=30, + description="Dify API请求超时时间(秒)" + ) + DIFY_DEFAULT_SCENE: str = Field( + default="通用聊天场景", + description="Dify默认场景描述(传给inputs.scene_description参数)" + ) + DIFY_STREAM_CHUNK_SIZE: int = Field( + default=1024, + description="Dify流式响应读取块大小(字节)" + ) + + # Chat 服务配置(新增) + CHAT_CURRENT_VERSION: str = "DefaultChat" # 对应工厂类的注册名 + CHAT_BASE_URL: str = "http://10.10.10.202/v1" + CHAT_API_KEY: str = "app-m7HZNV1aGiheh3wr6wNVHFxX" # 替换为真实 API-Key + CHAT_TIMEOUT: int = 300 # 流式请求超时时间(秒) + CHAT_RETRY_TIMES: int = 3 # 重试次数 + CHAT_POOL_SIZE: int = 5 # 连接池大小 # TTS当前使用版本(对应TTS_REGISTRY中的key) TTS_CURRENT_VERSION: str = Field(default="pyttsx3", description="TTS服务当前版本") @@ -64,7 +103,8 @@ class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8") + settings = Settings() # 确保模型目录存在(本地版本需要) # settings.LOCAL_ASR_MODEL_PATH.parent.mkdir(parents=True, exist_ok=True) -# settings.LOCAL_LLM_MODEL_PATH.parent.mkdir(parents=True, exist_ok=True) \ No newline at end of file +# settings.LOCAL_LLM_MODEL_PATH.parent.mkdir(parents=True, exist_ok=True) diff --git a/audio_ai_chat/audio_ai_chat/core/__pycache__/__init__.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/core/__pycache__/__init__.cpython-310.pyc index 7afa194..f7ab12e 100644 Binary files a/audio_ai_chat/audio_ai_chat/core/__pycache__/__init__.cpython-310.pyc and b/audio_ai_chat/audio_ai_chat/core/__pycache__/__init__.cpython-310.pyc differ diff --git a/audio_ai_chat/audio_ai_chat/core/__pycache__/connection.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/core/__pycache__/connection.cpython-310.pyc index a4aa12a..b99da09 100644 Binary files a/audio_ai_chat/audio_ai_chat/core/__pycache__/connection.cpython-310.pyc and b/audio_ai_chat/audio_ai_chat/core/__pycache__/connection.cpython-310.pyc differ diff --git a/audio_ai_chat/audio_ai_chat/core/__pycache__/websocket_handler.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/core/__pycache__/websocket_handler.cpython-310.pyc index b4a1cf3..1c9b986 100644 Binary files a/audio_ai_chat/audio_ai_chat/core/__pycache__/websocket_handler.cpython-310.pyc and b/audio_ai_chat/audio_ai_chat/core/__pycache__/websocket_handler.cpython-310.pyc differ diff --git a/audio_ai_chat/audio_ai_chat/core/asr/__pycache__/asr_manager.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/core/asr/__pycache__/asr_manager.cpython-310.pyc new file mode 100644 index 0000000..8f3eae3 Binary files /dev/null and b/audio_ai_chat/audio_ai_chat/core/asr/__pycache__/asr_manager.cpython-310.pyc differ diff --git a/audio_ai_chat/audio_ai_chat/core/asr/__pycache__/base.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/core/asr/__pycache__/base.cpython-310.pyc index 3dce723..e61cd47 100644 Binary files a/audio_ai_chat/audio_ai_chat/core/asr/__pycache__/base.cpython-310.pyc and b/audio_ai_chat/audio_ai_chat/core/asr/__pycache__/base.cpython-310.pyc differ diff --git a/audio_ai_chat/audio_ai_chat/core/asr/__pycache__/factory.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/core/asr/__pycache__/factory.cpython-310.pyc index cd9c8db..d748afc 100644 Binary files a/audio_ai_chat/audio_ai_chat/core/asr/__pycache__/factory.cpython-310.pyc and b/audio_ai_chat/audio_ai_chat/core/asr/__pycache__/factory.cpython-310.pyc differ diff --git a/audio_ai_chat/audio_ai_chat/core/asr/asr_manager.py b/audio_ai_chat/audio_ai_chat/core/asr/asr_manager.py new file mode 100644 index 0000000..078cc73 --- /dev/null +++ b/audio_ai_chat/audio_ai_chat/core/asr/asr_manager.py @@ -0,0 +1,48 @@ +# audio_ai_chat/asr/asr_manager.py +from typing import Optional,Tuple +from .base import ASRBase +from .factory import ASRFactory +from audio_ai_chat.config.settings import settings + +class ASRManager: # 类名与文件名呼应 + """ASR 管理器:负责实例生命周期、连接池复用、资源管理""" + _instance: Optional[ASRBase] = None # 单例存储 + + @classmethod + async def initialize(cls) -> Tuple[bool, str]: + try: + # 1. 创建 ASR 实例 + cls._instance = ASRFactory.get_asr_client() + + # 2. 初始化连接池 + pool_init_success = await cls._instance.initialize() + if not pool_init_success: + return False, "ASR 连接池初始化失败" + + # 3. 异步调用获取有效连接数(添加 await) + valid_conn_count = await cls._instance.get_valid_connection_count() + if valid_conn_count == 0: + return False, f"有效连接数为 0(配置池大小:{settings.ASR_POOL_SIZE})" + + return True, f"初始化成功:有效连接数 {valid_conn_count}" + except Exception as e: + return False, f"初始化失败:{str(e)}" + + @classmethod + def get_instance(cls) -> Optional[ASRBase]: + """获取全局 ASR 实例(业务代码调用)""" + return cls._instance + + @classmethod + async def close(cls): + """关闭 ASR 实例和连接池(FastAPI 关闭时调用)""" + if cls._instance: + await cls._instance.close() + cls._instance = None + print("ASR 管理器:实例和连接池已关闭") + + # 未来可扩展的管理功能 + @classmethod + def is_healthy(cls) -> bool: + """检查 ASR 实例健康状态(管理功能扩展)""" + return cls._instance is not None \ No newline at end of file diff --git a/audio_ai_chat/audio_ai_chat/core/asr/base.py b/audio_ai_chat/audio_ai_chat/core/asr/base.py index 5f941cc..c92be08 100644 --- a/audio_ai_chat/audio_ai_chat/core/asr/base.py +++ b/audio_ai_chat/audio_ai_chat/core/asr/base.py @@ -1,7 +1,12 @@ +# audio_ai_chat/asr/base.py from abc import ABC, abstractmethod -from typing import Optional, Coroutine +from typing import Optional, Dict, Callable, Awaitable from audio_ai_chat.config.settings import settings +# 定义回调函数类型(异步函数,接收 ASR 结果字典) +ASRResultCallback = Callable[[Dict], Awaitable[None]] + + class ASRBase(ABC): """ASR服务统一抽象接口""" def __init__(self): @@ -10,16 +15,36 @@ class ASRBase(ABC): self.retry_times = settings.ASR_RETRY_TIMES @abstractmethod - async def recognize( - self, - voice_data: bytes, - user_id: Optional[str] = None, - **kwargs # 兼容不同版本的额外参数 - ) -> str: - """ - 语音识别核心方法(所有ASR版本必须实现) - :param voice_data: 语音二进制数据 - :param user_id: 用户ID(可选) - :return: 识别后的文本 - """ + async def initialize(self) -> bool: + """初始化ASR服务(如连接池初始化)""" + pass + + @abstractmethod + async def get_connection(self) -> Optional[object]: + """获取ASR连接对象""" + pass + + @abstractmethod + async def push_audio(self, conn: object, audio_data: bytes) -> bool: + """推送音频数据到ASR服务""" + pass + + @abstractmethod + async def start_communication(self, conn: object, callback: ASRResultCallback) -> None: + """启动ASR通信(发送音频+接收结果)""" + pass + + @abstractmethod + async def release_connection(self, conn: object) -> None: + """释放ASR连接""" + pass + + @abstractmethod + async def close(self) -> None: + """关闭ASR服务(释放所有连接)""" + pass + + @abstractmethod + async def get_valid_connection_count(self) -> int: + """获取有效连接数(异步方法,子类必须实现)""" pass \ No newline at end of file diff --git a/audio_ai_chat/audio_ai_chat/core/asr/factory.py b/audio_ai_chat/audio_ai_chat/core/asr/factory.py index 7e52290..0b9c1c5 100644 --- a/audio_ai_chat/audio_ai_chat/core/asr/factory.py +++ b/audio_ai_chat/audio_ai_chat/core/asr/factory.py @@ -1,26 +1,26 @@ +# audio_ai_chat/asr/factory.py from typing import Type from audio_ai_chat.config.settings import settings from audio_ai_chat.utils.exceptions import ServiceCallError from .base import ASRBase -# from .version1 import LocalOfflineASR -# from .version2 import BaiduASR -# -# # 注册所有ASR版本:key=配置中的版本名,value=对应的类 -# ASR_REGISTRY: dict[str, Type[ASRBase]] = { -# "local_v1": LocalOfflineASR, -# "baidu_v2": BaiduASR, -# # 新增版本时,只需在这里注册:"新版本名": 新类名 -# } +from .fun_asr import FunASR # 对应原asr_client.py的实现类 -# class ASRFactory: -# """ASR服务工厂类:根据配置创建对应版本的实例""" -# @staticmethod -# def get_asr_client() -> ASRBase: -# # 从配置中获取当前指定的ASR版本 -# current_version = settings.ASR_CURRENT_VERSION -# if current_version not in ASR_REGISTRY: -# raise ServiceCallError( -# f"不支持的ASR版本:{current_version},可选版本:{list(ASR_REGISTRY.keys())}" -# ) -# # 创建并返回对应版本的实例 -# return ASR_REGISTRY[current_version]() \ No newline at end of file +# 注册所有ASR版本:key=配置中的版本名,value=对应的类 +ASR_REGISTRY: dict[str, Type[ASRBase]] = { + "FunASR": FunASR, + # 新增版本时,只需在这里注册:"新版本名": 新类名 +} + + +class ASRFactory: + """ASR服务工厂类:根据配置创建对应版本的实例""" + @staticmethod + def get_asr_client() -> ASRBase: + # 从配置中获取当前指定的ASR版本 + current_version = settings.ASR_CURRENT_VERSION + if current_version not in ASR_REGISTRY: + raise ServiceCallError( + f"不支持的ASR版本:{current_version},可选版本:{list(ASR_REGISTRY.keys())}" + ) + # 创建并返回对应版本的实例 + return ASR_REGISTRY[current_version]() \ No newline at end of file diff --git a/audio_ai_chat/audio_ai_chat/core/asr/fun_asr/__init__.py b/audio_ai_chat/audio_ai_chat/core/asr/fun_asr/__init__.py new file mode 100644 index 0000000..4c7a111 --- /dev/null +++ b/audio_ai_chat/audio_ai_chat/core/asr/fun_asr/__init__.py @@ -0,0 +1 @@ +from .fun_asr import FunASR \ No newline at end of file diff --git a/audio_ai_chat/audio_ai_chat/core/asr/fun_asr/__pycache__/__init__.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/core/asr/fun_asr/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..063b1d2 Binary files /dev/null and b/audio_ai_chat/audio_ai_chat/core/asr/fun_asr/__pycache__/__init__.cpython-310.pyc differ diff --git a/audio_ai_chat/audio_ai_chat/core/asr/fun_asr/__pycache__/fun_asr.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/core/asr/fun_asr/__pycache__/fun_asr.cpython-310.pyc new file mode 100644 index 0000000..987b6bb Binary files /dev/null and b/audio_ai_chat/audio_ai_chat/core/asr/fun_asr/__pycache__/fun_asr.cpython-310.pyc differ diff --git a/audio_ai_chat/audio_ai_chat/core/asr/fun_asr/fun_asr.py b/audio_ai_chat/audio_ai_chat/core/asr/fun_asr/fun_asr.py new file mode 100644 index 0000000..04e344b --- /dev/null +++ b/audio_ai_chat/audio_ai_chat/core/asr/fun_asr/fun_asr.py @@ -0,0 +1,264 @@ +import asyncio +import json +import websockets +from typing import Optional, List, Dict, Callable, Awaitable, Any +from dataclasses import dataclass, field +from ..base import ASRBase, ASRResultCallback +from audio_ai_chat.config.settings import settings + +# 从配置读取参数(替换原硬编码配置) +AUDIO_PARAMS = { + "sample_rate": settings.ASR_SAMPLE_RATE, + "channels": settings.ASR_CHANNELS, + "sample_width": settings.ASR_SAMPLE_WIDTH, + "frame_size": settings.ASR_FRAME_SIZE +} + +ASR_CONFIG = { + "host": settings.ASR_HOST, + "port": settings.ASR_PORT, + "mode": settings.ASR_MODE, + "chunk_size": settings.ASR_CHUNK_SIZE, + "chunk_interval": settings.ASR_CHUNK_INTERVAL, + "use_itn": settings.ASR_USE_ITN, + "hotwords": settings.ASR_HOTWORDS, + "reconnect_max_times": settings.ASR_RECONNECT_MAX_TIMES, + "pool_size": settings.ASR_POOL_SIZE, + "audio_queue_size": settings.ASR_AUDIO_QUEUE_SIZE +} + + +@dataclass +class ASRConnection: + """ASR 连接对象(内置音频队列)""" + ws: Optional[websockets.WebSocketClientProtocol] = None + is_busy: bool = False + is_alive: bool = False + reconnect_count: int = 0 + audio_queue: asyncio.Queue = field(default_factory=lambda: asyncio.Queue(maxsize=ASR_CONFIG["audio_queue_size"])) + stop_event: asyncio.Event = field(default_factory=asyncio.Event) + + +class FunASR(ASRBase): + """FunASR实现类""" + def __init__(self): + super().__init__() + self._connection_pool: List[ASRConnection] = [] + self._pool_lock = asyncio.Lock() # 异步锁 + + async def initialize(self) -> bool: + """初始化ASR连接池""" + print(f"开始初始化 FunASR 连接池,大小:{ASR_CONFIG['pool_size']}") + tasks = [self._create_single_connection() for _ in range(ASR_CONFIG["pool_size"])] + connections = await asyncio.gather(*tasks) + self._connection_pool = [conn for conn in connections if conn.is_alive] + print(f"FunASR 连接池初始化完成,有效连接数:{len(self._connection_pool)}") + return len(self._connection_pool) > 0 + + async def get_connection(self) -> Optional[ASRConnection]: + """从连接池获取空闲连接(实现抽象方法)""" + async with self._pool_lock: + # 查找空闲连接 + idle_conns = [ + conn for conn in self._connection_pool + if not conn.is_busy and conn.is_alive + ] + if idle_conns: + conn = idle_conns[0] + conn.is_busy = True + conn.stop_event.clear() + return conn + + # 连接池未满时创建新连接 + if len(self._connection_pool) < ASR_CONFIG["pool_size"]: + new_conn = await self._create_single_connection() + if new_conn.is_alive: + new_conn.is_busy = True + self._connection_pool.append(new_conn) + return new_conn + + print("FunASR 连接池无空闲连接") + return None + + async def push_audio(self, conn: ASRConnection, audio_data: bytes) -> bool: + """推送音频数据到ASR连接队列(实现抽象方法)""" + if not isinstance(conn, ASRConnection): + print("无效的ASR连接对象") + return False + + if not conn or not conn.is_alive or conn.stop_event.is_set(): + return False + try: + conn.audio_queue.put_nowait(audio_data) + return True + except asyncio.QueueFull: + print("FunASR 音频队列已满,丢弃当前音频帧") + return False + + async def start_communication(self, conn: ASRConnection, callback: ASRResultCallback) -> None: + """启动ASR通信(发送音频+接收结果,实现抽象方法)""" + if not isinstance(conn, ASRConnection): + await callback({"error": "无效的ASR连接对象", "text": ""}) + return + + await self._handle_communication(conn, callback) + + async def release_connection(self, conn: ASRConnection) -> None: + """释放ASR连接(实现抽象方法)""" + if not isinstance(conn, ASRConnection): + print("无效的ASR连接对象,无法释放") + return + + async with self._pool_lock: + conn.is_busy = False + conn.stop_event.set() + # 清空队列 + while not conn.audio_queue.empty(): + try: + conn.audio_queue.get_nowait() + except asyncio.QueueEmpty: + break + # 重连逻辑 + if not conn.is_alive and conn.reconnect_count < ASR_CONFIG["reconnect_max_times"]: + print(f"尝试重连 FunASR 连接(次数:{conn.reconnect_count + 1})") + new_conn = await self._create_single_connection() + if new_conn.is_alive: + if conn in self._connection_pool: + idx = self._connection_pool.index(conn) + self._connection_pool[idx] = new_conn + else: + conn.reconnect_count += 1 + elif conn.reconnect_count >= ASR_CONFIG["reconnect_max_times"]: + if conn in self._connection_pool: + self._connection_pool.remove(conn) + print("FunASR 连接重连次数耗尽,已移除") + + async def close(self) -> None: + """关闭所有ASR连接(实现抽象方法)""" + async with self._pool_lock: + # for conn in self._connection_pool: + # conn.stop_event.set() + # if conn.ws and not conn.ws.closed: + # try: + # await conn.ws.close() + # print("FunASR 连接已关闭") + # except Exception as e: + # print(f"关闭 FunASR 连接失败:{e}") + self._connection_pool.clear() + print("FunASR 连接池已清空") + + async def _create_single_connection(self) -> ASRConnection: + """创建单个ASR连接(内部私有方法)""" + asr_conn = ASRConnection() + asr_uri = f"ws://{ASR_CONFIG['host']}:{ASR_CONFIG['port']}" + + try: + ws = await websockets.connect( + asr_uri, + subprotocols=["binary"], + ping_interval=None, + open_timeout=self.timeout # 使用基类的超时配置 + ) + asr_conn.ws = ws + asr_conn.is_alive = True + + # 发送初始化配置 + init_msg = json.dumps({ + "mode": ASR_CONFIG["mode"], + "chunk_size": ASR_CONFIG["chunk_size"], + "chunk_interval": ASR_CONFIG["chunk_interval"], + "wav_name": "pool_connection", + "is_speaking": True, + "hotwords": ASR_CONFIG["hotwords"], + "itn": bool(ASR_CONFIG["use_itn"]), + "audio_fs": AUDIO_PARAMS["sample_rate"] + }) + await ws.send(init_msg) + print("FunASR 连接初始化成功") + return asr_conn + + except Exception as e: + print(f"创建 FunASR 连接失败:{e}") + asr_conn.is_alive = False + return asr_conn + + async def _handle_communication( + self, + asr_conn: ASRConnection, + result_callback: ASRResultCallback + ): + """处理ASR通信细节(内部私有方法)""" + if not asr_conn or not asr_conn.ws: + await result_callback({"error": "无可用 FunASR 连接", "text": ""}) + return + + # 发送音频任务 + async def send_audio(): + while not asr_conn.stop_event.is_set() and asr_conn.is_alive: + try: + pcm_data = await asyncio.wait_for(asr_conn.audio_queue.get(), timeout=1.0) + if pcm_data and asr_conn.is_alive: + await asr_conn.ws.send(pcm_data) + await asyncio.sleep(0.005) + except asyncio.TimeoutError: + continue + except Exception as e: + print(f"发送音频到 FunASR 失败:{e}") + asr_conn.is_alive = False + await result_callback({"error": f"音频发送失败:{str(e)}", "text": ""}) + asr_conn.stop_event.set() + break + + # 接收结果任务 + async def recv_result(): + while not asr_conn.stop_event.is_set() and asr_conn.is_alive: + try: + asr_result = await asr_conn.ws.recv() + result_json = json.loads(asr_result) + # 打印asr实时结果 + # print(result_json.get("text", "")) + if result_json.get("timestamp", "") == '': + continue + result = { + "text": result_json.get("text", ""), + "mode": result_json.get("mode", ""), + "timestamp": result_json.get("timestamp", ""), + "is_final": result_json.get("is_final", True), + "error": "" + } + await result_callback(result) + except websockets.exceptions.ConnectionClosed: + print("FunASR 连接已关闭") + asr_conn.is_alive = False + await result_callback({"error": "FunASR 连接断开", "text": ""}) + asr_conn.stop_event.set() + break + except Exception as e: + print(f"接收 FunASR 结果失败:{e}") + asr_conn.is_alive = False + await result_callback({"error": f"接收结果失败:{str(e)}", "text": ""}) + asr_conn.stop_event.set() + break + + try: + send_task = asyncio.create_task(send_audio()) + recv_task = asyncio.create_task(recv_result()) + await asyncio.gather(send_task, recv_task) + finally: + # 确保任务被取消 + send_task.cancel() + recv_task.cancel() + try: + await send_task + await recv_task + except asyncio.CancelledError: + pass + # 自动释放连接 + await self.release_connection(asr_conn) + + async def get_valid_connection_count(self) -> int: + """获取有效连接数(异步方法,保证线程安全)""" + async with self._pool_lock: # 异步锁,自动 acquire/release + # 过滤出 "存活" 且 "在连接池内" 的连接 + valid_conns = [conn for conn in self._connection_pool if conn.is_alive] + return len(valid_conns) \ No newline at end of file diff --git a/audio_ai_chat/audio_ai_chat/core/connection.py b/audio_ai_chat/audio_ai_chat/core/connection.py index f2e0098..924c9da 100644 --- a/audio_ai_chat/audio_ai_chat/core/connection.py +++ b/audio_ai_chat/audio_ai_chat/core/connection.py @@ -1,9 +1,21 @@ -from typing import Optional, Dict, Any, List +from typing import Optional, Dict, Any, List, TypedDict import asyncio +from datetime import datetime from audio_ai_chat.config.logger import logger +from audio_ai_chat.core.llm.dify.dify import LLMConversation # from audio_ai_chat.core.llm.factory import LLMFactory -from audio_ai_chat.core.llm.base import LLMBase +# 定义对话历史条目类型(TypedDict 用于类型提示,更清晰) +class ChatHistoryItem(TypedDict): + """对话历史条目结构(强类型定义)""" + role: str # 发言人角色:"user"(用户)、"assistant"(助手)、"system"(系统) + content: str # 对话内容(ASR转写结果/大模型回复/系统提示) + timestamp: str # 对话时间(ISO 8601格式,如 "2024-05-20T14:30:00.123Z") + source: str # 内容来源:"asr"(语音转写)、"text"(纯文本输入)、"llm"(大模型生成)、"system"(系统配置) + +def get_current_iso_timestamp() -> str: + """获取当前时间的ISO 8601格式字符串(UTC时间)""" + return datetime.utcnow().isoformat(timespec="milliseconds") + "Z" class ConnectionContext: """ @@ -15,30 +27,43 @@ class ConnectionContext: """ 初始化连接上下文 :param client_id: WebSocket连接唯一标识(如id(websocket)) - :param user_id: 用户唯一标识(从前端请求中获取) """ - + self.MAX_CHAT_HISTORY = 100 # 单个连接最大对话历史条数 + self.MAX_QUEUE_SIZE = 50 # 单个连接消息队列最大长度 self.client_id = client_id # 连接唯一ID - self.created_at = asyncio.get_event_loop().time() # 连接创建时间 + self.created_at = asyncio.get_event_loop().time() # 连接创建时间(时间戳) + self.created_at_str = datetime.utcnow().isoformat() + "Z" # 连接创建时间(ISO格式) # 1. 大模型独立Session(每个连接创建一个新的LLM客户端实例) # self.llm_session: LLMBase = LLMFactory.get_llm_client() # 独立Session - self.chat_history: List[Dict[str, str]] = [] # 该连接的对话历史([(user: "...", assistant: "..."), ...]) + self.llm_session: Optional[LLMConversation] = None # 实际是DifyLLMClient实例 + # 优化后的对话历史:List[ChatHistoryItem] + self.chat_history: List[ChatHistoryItem] = [] # 该连接的完整对话历史 # 2. 异步消息队列(用于缓存TTS结果,有序推送给前端) + self.tts_client = None self.message_queue: asyncio.Queue[bytes] = asyncio.Queue() - # 3. 连接状态(可选:如是否正在处理请求、是否断开等) + # 3. 连接状态 self.is_active: bool = True self.is_processing: bool = False - self.name = None - self.user_id = None - self.token = None + self.name: Optional[str] = None # 用户名 + self.user_id: Optional[str] = None # 用户唯一标识 + self.token: Optional[str] = None # 用户令牌 + self.disconnect_time: Optional[datetime] = None # 断开时间(None 表示活跃) + + # 4. ASR临时缓存(处理流式结果,避免重复存储) + self._current_asr_text: str = "" # 当前正在拼接的ASR文本 + self._current_asr_metadata: Optional[Dict[str, Any]] = None # 当前ASR元数据 + + self.is_processing: bool = False + + def set_user_info(self, token: str, user_id: str, name: str = "匿名用户"): """ 二次设置用户信息(身份校验通过后调用) - :param token: + :param token: 用户令牌 :param user_id: 用户唯一标识(必填) :param name: 用户名(可选,默认匿名) """ @@ -49,6 +74,28 @@ class ConnectionContext: self.token = token logger.debug(f"客户端 {self.client_id} 设置用户信息:user_id={user_id}, name={name}") + def init_llm_session(self): + """初始化Dify客户端(每个连接一个实例,存入context)""" + # if not self.user_id: + # raise InitError(f"客户端 {self.client_id} 未设置用户信息,无法初始化Dify客户端") + if self.llm_session: + logger.warning(f"客户端 {self.client_id} Dify客户端已存在,无需重复初始化") + return + # 工厂类创建Dify客户端实例,存入当前context + self.llm_session = LLMFactory.get_llm_client(version="dify") + logger.debug(f"客户端 {self.client_id}(user_id={self.user_id})Dify客户端初始化完成") + + def update_dify_context(self, user_text: str, assistant_text: str, conversation_id: Optional[str]): + """更新Dify会话上下文(隔离存储)""" + if conversation_id: + self.dify_conversation_id = conversation_id + # 限制历史长度(最多50轮) + self.chat_history.append({"user": user_text, "assistant": assistant_text}) + if len(self.chat_history) > 50: + self.chat_history.pop(0) + logger.debug( + f"客户端 {self.client_id} Dify上下文更新:conversation_id={self.dify_conversation_id},历史长度={len(self.dify_history)}") + async def add_message_to_queue(self, message: bytes): """将TTS结果添加到消息队列(异步安全)""" if not self.is_active: @@ -56,6 +103,15 @@ class ConnectionContext: await self.message_queue.put(message) logger.debug(f"消息队列添加数据:client_id={self.client_id},队列长度={self.message_queue.qsize()}") + def complete_initialization(self): + """标记完成初始化(必须确保Dify客户端已创建)""" + # if not self.user_id: + # raise InitError(f"客户端 {self.client_id} 未设置用户信息") + # if not self.llm_session: + # raise InitError(f"客户端 {self.client_id} 未初始化Dify客户端") + self.is_initialized = True + logger.info(f"客户端 {self.client_id}(user_id={self.user_id})完整初始化完成") + async def get_message_from_queue(self) -> Optional[bytes]: """从消息队列获取消息(异步阻塞,直到有消息或连接断开)""" try: @@ -65,81 +121,334 @@ class ConnectionContext: logger.debug(f"消息队列超时:client_id={self.client_id},无新消息") return None - def update_chat_history(self, user_text: str, assistant_text: str): - """更新该连接的对话历史""" - self.chat_history.append({ - "user": user_text, - "assistant": assistant_text - }) - # 可选:限制历史长度(避免内存溢出) - if len(self.chat_history) > 50: - self.chat_history.pop(0) # 删除最早的历史 + def add_chat_history(self, item: ChatHistoryItem): + """ + 添加对话历史条目(统一接口,支持用户/助手/系统消息) + :param item: 符合 ChatHistoryItem 结构的对话条目 + """ + # 补全必填字段(防止遗漏) + if "timestamp" not in item: + item["timestamp"] = datetime.utcnow().isoformat() + "Z" + if "source" not in item: + item["source"] = "unknown" + + self.chat_history.append(item) + # 可选:限制历史长度(避免内存溢出,保留最近100条) + if len(self.chat_history) > 100: + removed_item = self.chat_history.pop(0) + logger.debug( + f"对话历史超出限制,删除最早条目:{removed_item['timestamp']} - {removed_item['role']}: {removed_item['content'][:20]}...") + + logger.debug( + f"添加对话历史:client_id={self.client_id}," + f"role={item['role']},content={item['content'][:30]}..." + ) + + def add_asr_result(self, asr_result: Dict[str, Any]): + """ + 处理ASR结果,拼接流式文本,最终结果存入对话历史 + :param asr_result: ASR返回的结果字典(含text、is_final、timestamp等) + """ + if asr_result.get("error"): + logger.error(f"ASR错误:client_id={self.client_id},error={asr_result['error']}") + return + + # 提取ASR核心信息 + asr_text = asr_result.get("text", "").strip() + is_final = asr_result.get("is_final", False) + asr_timestamp = asr_result.get("timestamp", "") + asr_mode = asr_result.get("mode", "") + + # 缓存ASR元数据(流式过程中更新) + self._current_asr_metadata = { + "timestamp": asr_timestamp, + "mode": asr_mode, + "is_final": is_final, + "source": "asr" + } + + # 拼接流式文本(处理部分结果) + if asr_text: + # 避免重复拼接(如果ASR返回重复文本) + if not self._current_asr_text.endswith(asr_text) and self._current_asr_text != asr_text: + self._current_asr_text += asr_text if not self._current_asr_text else f" {asr_text}" + + # 当ASR返回最终结果时,存入对话历史 + if is_final: + if self._current_asr_text: + # 构造对话历史条目 + chat_item: ChatHistoryItem = { + "role": "user", # ASR结果属于用户输入 + "content": self._current_asr_text, + "timestamp": datetime.utcnow().isoformat() + "Z", + "source": "asr", + "asr_metadata": self._current_asr_metadata + } + # 添加到对话历史 + self.add_chat_history(chat_item) + # 清空临时缓存 + self._current_asr_text = "" + self._current_asr_metadata = None + else: + logger.warning(f"ASR最终结果为空:client_id={self.client_id}") + + def add_llm_result(self, llm_text: str): + """ + 添加大模型回复到对话历史 + :param llm_text: 大模型生成的回复文本 + """ + if not llm_text.strip(): + logger.warning(f"大模型回复为空:client_id={self.client_id}") + return + + chat_item: ChatHistoryItem = { + "role": "assistant", # 大模型回复属于助手角色 + "content": llm_text.strip(), + "timestamp": datetime.utcnow().isoformat() + "Z", + "source": "llm", + "asr_metadata": None # 大模型回复无ASR元数据 + } + self.add_chat_history(chat_item) + + def add_system_message(self, system_text: str): + """ + 添加系统消息到对话历史(如错误提示、系统通知) + :param system_text: 系统消息文本 + """ + chat_item: ChatHistoryItem = { + "role": "system", # 系统角色 + "content": system_text.strip(), + "timestamp": datetime.utcnow().isoformat() + "Z", + "source": "system", + "asr_metadata": None + } + self.add_chat_history(chat_item) + + def get_chat_history(self, limit: Optional[int] = None) -> List[ChatHistoryItem]: + """ + 获取对话历史(支持限制返回条数) + :param limit: 限制返回的最新条数,None表示返回全部 + :return: 过滤后的对话历史 + """ + if limit and isinstance(limit, int) and limit > 0: + return self.chat_history[-limit:] # 返回最近N条 + return self.chat_history.copy() # 返回全部(拷贝,避免外部修改) def close(self): """关闭连接上下文,释放资源""" self.is_active = False self.is_processing = False - # 清空消息队列(可选) + # 清空消息队列 while not self.message_queue.empty(): try: self.message_queue.get_nowait() except asyncio.QueueEmpty: break - logger.info(f"连接上下文已关闭:client_id={self.client_id},user_id={self.user_id}") + # 记录连接关闭日志(包含对话历史统计) + logger.info( + f"连接上下文已关闭:client_id={self.client_id}," + f"user_id={self.user_id}," + f"对话历史条数={len(self.chat_history)}" + ) + def mark_disconnected(self): + self.is_active = False + self.disconnect_time = datetime.utcnow() + logger.info(f"客户端 {self.client_id} 标记为断开,待延迟清理(user_id={self.user_id})") def __del__(self): """析构函数:确保资源释放""" self.close() + # -------------------------- 核心:调用Dify流式接口 -------------------------- + async def call_dify_stream( + self, + user_text: str, + tts_client, + asr_metadata: Optional[Dict[str, Any]] = None # 接收ASR元数据 + ) -> str: + """ + 调用Dify流式接口,使用ChatHistoryItem存储完整历史 + :param user_text: ASR识别后的用户文本 + :param tts_client: TTS客户端实例 + :param asr_metadata: ASR元数据(如置信度、语音时长等) + :return: Dify完整回复文本 + """ + if not self.is_initialized: + raise InitError(f"客户端 {self.client_id} 未完成初始化,无法调用Dify") + if not user_text: + raise ValueError("用户输入文本不能为空") + full_response = "" + llm_metadata: Dict[str, Any] = {} # 存储Dify元数据 + + # 1. 添加用户输入到对话历史(user角色,source=asr) + user_history_item: ChatHistoryItem = { + "role": "user", + "content": user_text, + "timestamp": get_current_iso_timestamp(), + "source": "asr", + "asr_metadata": asr_metadata, # 传入ASR元数据 + "llm_metadata": None + } + self.add_chat_history_item(user_history_item) + + # -------------------------- 流式回调函数(闭包访问context) -------------------------- + async def stream_callback(chunk: str, conversation_id: str, is_finished: bool): + nonlocal full_response, llm_metadata + if chunk and not is_finished: + # 累加完整回复 + full_response += chunk + logger.debug( + f"客户端 {self.client_id} Dify流式片段:content_len={len(chunk)}, " + f"累计_len={len(full_response)}" + ) + + # 实时调用TTS合成音频 + try: + tts_audio = await tts_client.synthesize( + text=chunk, + user_id=self.user_id + ) + await self.add_tts_to_queue(tts_audio) + except Exception as e: + logger.error(f"客户端 {self.client_id} TTS合成失败:{str(e)}") + return + + # 流式结束:添加助手回复到对话历史 + if is_finished and full_response: + # 更新Dify会话ID和元数据 + self.dify_conversation_id = conversation_id + llm_metadata = { + "conversation_id": conversation_id, + "response_mode": "streaming", + "full_response_len": len(full_response), + "timestamp": get_current_iso_timestamp() + } + + # 添加助手回复到对话历史(assistant角色,source=llm) + assistant_history_item: ChatHistoryItem = { + "role": "assistant", + "content": full_response, + "timestamp": get_current_iso_timestamp(), + "source": "llm", + "asr_metadata": None, + "llm_metadata": llm_metadata # 存储Dify元数据 + } + self.add_chat_history_item(assistant_history_item) + + # -------------------------- 调用Dify流式接口 -------------------------- + try: + await self.llm_session.chat( + text=user_text, + user_id=self.user_id, + history=self.get_dify_compatible_history(), # 传入Dify兼容格式的历史 + stream_callback=stream_callback, + response_mode="streaming" + ) + except Exception as e: + logger.error(f"客户端 {self.client_id} Dify流式调用失败:{str(e)}") + raise + + return full_response + +# -------------------------- 关键修改:ConnectionManager 全局单例 -------------------------- class ConnectionManager: - """ - WebSocket连接全局管理器:维护所有活跃连接的上下文 - 提供创建、查询、删除连接上下文的接口(线程/异步安全) - """ + """全局连接上下文管理器(支持延迟清理和重连复用)""" + _instance: Optional["ConnectionManager"] = None + _lock = asyncio.Lock() # 单例锁 + + def __new__(cls): + raise NotImplementedError("请使用 ConnectionManager.get_instance() 获取实例") def __init__(self): - # 存储所有活跃连接:key=client_id(int),value=ConnectionContext实例 - self.connections: Dict[int, ConnectionContext] = {} - # 异步锁:确保多连接并发操作时的数据安全 - self._lock = asyncio.Lock() + # 存储所有上下文:key=client_id(当前活跃连接的唯一标识) + self.active_contexts: Dict[str, ConnectionContext] = {} + # 存储待清理的上下文:key=user_id(用户唯一标识,用于重连匹配) + self.pending_clean_contexts: Dict[str, ConnectionContext] = {} + self._internal_lock = asyncio.Lock() # 操作锁 - async def create_connection(self, client_id: int, user_id: Optional[str] = None) -> ConnectionContext: - """创建新的连接上下文(线程安全)""" - async with self._lock: - # 避免重复创建(同一client_id不会重复连接) + @classmethod + async def get_instance(cls) -> "ConnectionManager": + """获取全局唯一实例(异步安全)""" + if cls._instance is None: + async with cls._lock: + if cls._instance is None: # 双重检查锁定 + cls._instance = super().__new__(cls) + cls._instance.__init__() + logger.info("ConnectionManager 全局单例初始化成功") + return cls._instance + + async def create_connection(self, client_id: str) -> ConnectionContext: + """创建连接上下文(异步安全)""" + async with self._internal_lock: if client_id in self.connections: logger.warning(f"连接已存在:client_id={client_id},将覆盖旧连接") self.connections[client_id].close() - - # 创建新的连接上下文(包含独立LLM Session和消息队列) - context = ConnectionContext(client_id=client_id, user_id=user_id) + context = ConnectionContext(client_id=client_id) self.connections[client_id] = context - logger.info( - f"创建新连接上下文:client_id={client_id},user_id={user_id},当前活跃连接数={len(self.connections)}") + logger.info(f"创建连接上下文:client_id={client_id},活跃连接数={len(self.connections)}") return context - async def get_connection(self, client_id: int) -> Optional[ConnectionContext]: - """获取指定client_id的连接上下文(线程安全)""" - async with self._lock: + async def get_connection(self, client_id: str) -> Optional[ConnectionContext]: + """获取连接上下文(异步安全)""" + async with self._internal_lock: context = self.connections.get(client_id) if context and not context.is_active: - # 清理已断开的连接 del self.connections[client_id] return None return context - async def remove_connection(self, client_id: int): - """删除连接上下文(线程安全)""" - async with self._lock: + async def remove_connection(self, client_id: str): + """移除连接上下文(异步安全)""" + async with self._internal_lock: context = self.connections.pop(client_id, None) if context: context.close() - logger.info(f"移除连接上下文:client_id={client_id},当前活跃连接数={len(self.connections)}") + logger.info(f"移除连接上下文:client_id={client_id},活跃连接数={len(self.connections)}") async def get_active_connections_count(self) -> int: - """获取当前活跃连接数(线程安全)""" - async with self._lock: - # 过滤已断开的连接 + """获取活跃连接数(异步安全)""" + async with self._internal_lock: self.connections = {k: v for k, v in self.connections.items() if v.is_active} - return len(self.connections) \ No newline at end of file + return len(self.connections) + # 检查是否在可重连时间窗口内(30分钟) + # 标记为断开连接(不立即清理) + + + + def is_reconnectable(self, timeout: int = 30) -> bool: + if self.is_active or not self.disconnect_time: + return False # 活跃连接或未记录断开时间,不可重连 + # 计算断开时间是否在 timeout 分钟内 + return datetime.utcnow() - self.disconnect_time <= timedelta(minutes=timeout) + + + + async def create_or_reconnect_context(self, new_client_id: str, user_id: Optional[str] = None) -> ConnectionContext: + """ + 创建新上下文或重连复用旧上下文 + :param new_client_id: 新 WebSocket 连接的 client_id + :param user_id: 用户唯一标识(用于匹配旧上下文) + :return: 新上下文或复用的旧上下文 + """ + async with self._internal_lock: + # 1. 如果用户已登录(有 user_id),先尝试重连复用 + if user_id and user_id in self.pending_clean_contexts: + old_context = self.pending_clean_contexts.pop(user_id) + if old_context.is_reconnectable(): + # 重连激活旧上下文,更新 client_id + old_context.reconnect(new_client_id=new_client_id) + # 加入活跃上下文列表 + self.active_contexts[new_client_id] = old_context + return old_context + else: + # 旧上下文已超时,清理并创建新的 + old_context.close() + + # 2. 无旧上下文可复用,创建新上下文 + new_context = ConnectionContext(client_id=new_client_id) + if user_id: + new_context.user_id = user_id # 绑定用户标识(如果已提供) + self.active_contexts[new_client_id] = new_context + logger.info(f"创建新上下文:client_id={new_client_id},user_id={user_id}") + return new_context \ No newline at end of file diff --git a/audio_ai_chat/audio_ai_chat/core/llm/__pycache__/__init__.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/core/llm/__pycache__/__init__.cpython-310.pyc index b9d3785..c2daf54 100644 Binary files a/audio_ai_chat/audio_ai_chat/core/llm/__pycache__/__init__.cpython-310.pyc and b/audio_ai_chat/audio_ai_chat/core/llm/__pycache__/__init__.cpython-310.pyc differ diff --git a/audio_ai_chat/audio_ai_chat/core/llm/__pycache__/base.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/core/llm/__pycache__/base.cpython-310.pyc index 44ad478..98bd3c4 100644 Binary files a/audio_ai_chat/audio_ai_chat/core/llm/__pycache__/base.cpython-310.pyc and b/audio_ai_chat/audio_ai_chat/core/llm/__pycache__/base.cpython-310.pyc differ diff --git a/audio_ai_chat/audio_ai_chat/core/llm/__pycache__/factory.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/core/llm/__pycache__/factory.cpython-310.pyc index 6f0a486..a9a6908 100644 Binary files a/audio_ai_chat/audio_ai_chat/core/llm/__pycache__/factory.cpython-310.pyc and b/audio_ai_chat/audio_ai_chat/core/llm/__pycache__/factory.cpython-310.pyc differ diff --git a/audio_ai_chat/audio_ai_chat/core/llm/__pycache__/llm_manager.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/core/llm/__pycache__/llm_manager.cpython-310.pyc new file mode 100644 index 0000000..c422e1b Binary files /dev/null and b/audio_ai_chat/audio_ai_chat/core/llm/__pycache__/llm_manager.cpython-310.pyc differ diff --git a/audio_ai_chat/audio_ai_chat/core/llm/base.py b/audio_ai_chat/audio_ai_chat/core/llm/base.py index 48087c4..d33d803 100644 --- a/audio_ai_chat/audio_ai_chat/core/llm/base.py +++ b/audio_ai_chat/audio_ai_chat/core/llm/base.py @@ -1,26 +1,71 @@ from abc import ABC, abstractmethod -from typing import Optional, Coroutine +from typing import Optional, Dict, Callable, Awaitable +from audio_ai_chat.config.settings import settings -class LLMBase(ABC): - """LLM服务统一抽象接口""" + +# 定义回调函数类型(异步函数,与 ASR 回调风格一致) +# ChatStreamCallback = Callable[[str, bool, ConnectionContext], Awaitable[None]] +""" +Chat 流式回调函数类型: +- 第一个参数:流式文本块 +- 第二个参数:是否结束标记 +- 第三个参数:上下文对象 +""" + + +class ChatBase(ABC): + """Chat 服务统一抽象接口(与 ASRBase 接口风格完全对齐)""" def __init__(self): - self.timeout = settings.LLM_TIMEOUT - self.retry_times = settings.LLM_RETRY_TIMES - self.model = settings.LLM_MODEL # 模型版本(不同LLM可能支持不同模型) + # 公共配置(所有 Chat 实现共享) + self.timeout = settings.CHAT_TIMEOUT # 需在配置中添加 CHAT_TIMEOUT + self.retry_times = settings.CHAT_RETRY_TIMES # 需在配置中添加 CHAT_RETRY_TIMES + self.base_url = settings.CHAT_BASE_URL # 配置中添加:http://10.10.10.202/v1 + self.api_key = settings.CHAT_API_KEY # 配置中添加 API-Key @abstractmethod - async def chat( + async def initialize(self) -> bool: + """初始化 Chat 服务(如连接池、全局配置)""" + pass + + @abstractmethod + async def get_connection(self) -> Optional[object]: + """获取 Chat 连接对象(与 ASR 的 get_connection 对应)""" + pass + + @abstractmethod + async def send_message( self, - text: str, - user_id: Optional[str] = None, - history: Optional[list] = None, # 对话历史(部分LLM支持) - **kwargs - ) -> str: - """ - 大模型对话核心方法 - :param text: 用户输入文本(ASR识别结果) - :param user_id: 用户ID(可选) - :param history: 对话历史(可选,格式:[(用户输入, 模型回答), ...]) - :return: 模型生成的回答文本 - """ + conn: object, + query: str, + context: ConnectionContext, + inputs: Optional[Dict] = None + ) -> bool: + """发送聊天消息(类似 ASR 的 push_audio)""" + pass + + @abstractmethod + async def start_communication( + self, + conn: object, + callback: ChatStreamCallback, + query: str, + context: ConnectionContext, + inputs: Optional[Dict] = None + ) -> None: + """启动 Chat 通信(流式接收结果,与 ASR 的 start_communication 对应)""" + pass + + @abstractmethod + async def release_connection(self, conn: object) -> None: + """释放 Chat 连接(与 ASR 的 release_connection 对应)""" + pass + + @abstractmethod + async def close(self) -> None: + """关闭 Chat 服务(释放所有连接,与 ASR 的 close 对应)""" + pass + + @abstractmethod + async def get_valid_connection_count(self) -> int: + """获取有效连接数(与 ASR 的接口完全一致)""" pass \ No newline at end of file diff --git a/audio_ai_chat/audio_ai_chat/core/llm/dify/__init__.py b/audio_ai_chat/audio_ai_chat/core/llm/dify/__init__.py new file mode 100644 index 0000000..aba19b1 --- /dev/null +++ b/audio_ai_chat/audio_ai_chat/core/llm/dify/__init__.py @@ -0,0 +1 @@ +# from .dify import DifyLLMClient \ No newline at end of file diff --git a/audio_ai_chat/audio_ai_chat/core/llm/dify/__pycache__/__init__.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/core/llm/dify/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..36c5048 Binary files /dev/null and b/audio_ai_chat/audio_ai_chat/core/llm/dify/__pycache__/__init__.cpython-310.pyc differ diff --git a/audio_ai_chat/audio_ai_chat/core/llm/dify/__pycache__/dify.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/core/llm/dify/__pycache__/dify.cpython-310.pyc new file mode 100644 index 0000000..cfa6dbd Binary files /dev/null and b/audio_ai_chat/audio_ai_chat/core/llm/dify/__pycache__/dify.cpython-310.pyc differ diff --git a/audio_ai_chat/audio_ai_chat/core/llm/dify/dify.py b/audio_ai_chat/audio_ai_chat/core/llm/dify/dify.py new file mode 100644 index 0000000..9c75997 --- /dev/null +++ b/audio_ai_chat/audio_ai_chat/core/llm/dify/dify.py @@ -0,0 +1,175 @@ +import asyncio +import json +from typing import Optional, Dict, Callable, Awaitable +from dataclasses import dataclass, field +import aiohttp # 新增:异步HTTP库 + +# 大模型配置(集中管理) +LLM_CONFIG = { + "base_url": "http://10.10.10.202:8088/v1", + "api_key": "app-m7HZNV1aGiheh3wr6wNVHFxX", + "timeout": 30, # 请求超时时间(秒) + "default_scene": "通用聊天场景", # 默认场景描述 + "stream_chunk_size": 1024 # 流式接收块大小 +} + +# 定义流式回调函数类型(异步) +LLMStreamCallback = Callable[[str, Optional[str], bool], Awaitable[None]] +""" +回调函数参数说明: +- chunk: 单次流式返回的文本片段 +- conversation_id: 会话ID(首次返回,后续复用) +- is_finished: 是否结束(True=流式结束/同步返回完成) +""" + +@dataclass +class LLMConversation: + """会话对象(管理会话ID和上下文)""" + conversation_id: Optional[str] = None + user_id: str = "" + scene_description: str = LLM_CONFIG["default_scene"] + # 可选:存储会话历史(如需上下文管理) + history: list = field(default_factory=list) + +class LLMClient: + """大模型客户端封装(异步修复版)""" + def __init__(self): + self.base_url = LLM_CONFIG["base_url"] + self.headers = { + "Authorization": f"Bearer {LLM_CONFIG['api_key']}", + "Content-Type": "application/json" + } + self.timeout = aiohttp.ClientTimeout(total=LLM_CONFIG["timeout"]) # 异步超时 + self._session: Optional[aiohttp.ClientSession] = None # 异步会话(复用连接) + + async def _get_session(self) -> aiohttp.ClientSession: + """获取/复用异步HTTP会话""" + if self._session is None or self._session.closed: + self._session = aiohttp.ClientSession(timeout=self.timeout) + return self._session + + async def send_message( + self, + query: str, + conversation: LLMConversation, + stream_callback: Optional[LLMStreamCallback] = None, + response_mode: str = "streaming" + ) -> tuple[Optional[str], str]: + """ + 发送消息到大模型(纯异步版,无线程池阻塞) + """ + payload = { + "query": query, + "inputs": {"scene_description": conversation.scene_description}, + "response_mode": response_mode, + "user": conversation.user_id + } + if conversation.conversation_id: + payload["conversation_id"] = conversation.conversation_id + + url = f"{self.base_url}/chat-messages" + full_response = "" + res_conversation_id = conversation.conversation_id + + try: + session = await self._get_session() + if response_mode == "streaming": + # 异步流式请求(无线程池,纯异步IO) + async with session.post(url, headers=self.headers, json=payload) as response: + response.raise_for_status() + # 实时迭代流式响应 + async for line in response.content.iter_chunked(LLM_CONFIG["stream_chunk_size"]): + + if not line: + continue + line_data = line.decode("utf-8") + if line_data.startswith("data: "): + json_str = line_data[6:].strip() + if json_str == "[DONE]": + if stream_callback: + await stream_callback("", res_conversation_id, True) + break + try: + data = json.loads(json_str) + # 更新会话ID + if not res_conversation_id and "conversation_id" in data: + res_conversation_id = data["conversation_id"] + # 提取内容 + chunk = data.get("content", data.get("answer", data.get("message", ""))) + # print('大模型返回的', chunk) + if chunk: + full_response += chunk + if stream_callback: + await stream_callback(chunk, res_conversation_id, False) + await asyncio.sleep(0) # 让出调度权 + except json.JSONDecodeError as e: + # print(f"大模型解析流式数据失败: {e}") + continue + else: + # 异步非流式请求 + async with session.post(url, headers=self.headers, json=payload) as response: + response.raise_for_status() + data = await response.json() + res_conversation_id = data.get("conversation_id", conversation.conversation_id) + full_response = data.get("content", data.get("answer", data.get("message", ""))) + if stream_callback: + await stream_callback(full_response, res_conversation_id, True) + + conversation.conversation_id = res_conversation_id + return res_conversation_id, full_response + + except aiohttp.ClientError as e: + error_msg = f"大模型请求失败: {str(e)}" + print(error_msg) + if stream_callback: + await stream_callback(f"[错误] {error_msg}", res_conversation_id, True) + return res_conversation_id, "" + except Exception as e: + error_msg = f"大模型处理异常: {str(e)}" + print(error_msg) + if stream_callback: + await stream_callback(f"[错误] {error_msg}", res_conversation_id, True) + return res_conversation_id, "" + + async def close(self): + """关闭异步会话(程序退出时调用)""" + if self._session and not self._session.closed: + await self._session.close() + +# 全局单例客户端(异步版) +llm_client = LLMClient() + +# 快捷调用函数(保持原有接口不变) +async def call_llm( + query: str, + user_id: str, + scene_description: str = LLM_CONFIG["default_scene"], + conversation_id: Optional[str] = None, + stream_callback: Optional[LLMStreamCallback] = None, + response_mode: str = "streaming" +) -> tuple[Optional[str], str]: + """ + 快捷调用大模型(无需手动创建会话对象) + :param query: 用户提问 + :param user_id: 用户ID + :param scene_description: 场景描述 + :param conversation_id: 会话ID(续聊用) + :param stream_callback: 流式回调 + :param response_mode: 响应模式 + :return: (conversation_id, 完整回复) + """ + conversation = LLMConversation( + conversation_id=conversation_id, + user_id=user_id, + scene_description=scene_description + ) + return await llm_client.send_message( + query=query, + conversation=conversation, + stream_callback=stream_callback, + response_mode=response_mode + ) + +# 可选:程序退出时关闭会话(如FastAPI的shutdown事件) +async def shutdown_llm_client(): + await llm_client.close() \ No newline at end of file diff --git a/audio_ai_chat/audio_ai_chat/core/llm/factory.py b/audio_ai_chat/audio_ai_chat/core/llm/factory.py index 548e2c3..97b9a7f 100644 --- a/audio_ai_chat/audio_ai_chat/core/llm/factory.py +++ b/audio_ai_chat/audio_ai_chat/core/llm/factory.py @@ -1,22 +1,25 @@ from typing import Type from audio_ai_chat.config.settings import settings from audio_ai_chat.utils.exceptions import ServiceCallError -from .base import LLMBase -# -# from .openai_llm import OpenAILLM -# from .local_llm import LocalLLM -# -# LLM_REGISTRY: dict[str, Type[LLMBase]] = { -# "openai": OpenAILLM, -# "local": LocalLLM, -# } -# -# class LLMFactory: -# @staticmethod -# def get_llm_client() -> LLMBase: -# current_version = settings.LLM_CURRENT_VERSION -# if current_version not in LLM_REGISTRY: -# raise ServiceCallError( -# f"不支持的LLM版本:{current_version},可选版本:{list(LLM_REGISTRY.keys())}" -# ) -# return LLM_REGISTRY[current_version]() \ No newline at end of file +from .base import ChatBase +from .dify import DifyLLMClient # 具体实现类(对应 ASR 的 FunASR) + +# 注册所有 Chat 实现:key=配置中的版本名,value=对应的类 +CHAT_REGISTRY: dict[str, Type[ChatBase]] = { + "DefaultChat": DifyLLMClient, + # 新增 Chat 实现时,只需在这里注册 +} + + +class ChatFactory: + """Chat 服务工厂类(与 ASRFactory 逻辑完全一致)""" + @staticmethod + def get_chat_client() -> ChatBase: + # 从配置中获取当前指定的 Chat 版本 + current_version = settings.CHAT_CURRENT_VERSION # 配置中添加该字段 + if current_version not in CHAT_REGISTRY: + raise ServiceCallError( + f"不支持的 Chat 版本:{current_version},可选版本:{list(CHAT_REGISTRY.keys())}" + ) + # 创建并返回对应版本的实例 + return CHAT_REGISTRY[current_version]() \ No newline at end of file diff --git a/audio_ai_chat/audio_ai_chat/core/llm/llm_manager.py b/audio_ai_chat/audio_ai_chat/core/llm/llm_manager.py new file mode 100644 index 0000000..0126603 --- /dev/null +++ b/audio_ai_chat/audio_ai_chat/core/llm/llm_manager.py @@ -0,0 +1,75 @@ +from typing import Optional, Tuple, Dict +from .base import ChatBase +from .factory import ChatFactory +from audio_ai_chat.config.settings import settings + + +# # 上下文管理类(集成到管理器中,与业务逻辑解耦) +# @dataclass +# class ConnectionContext: +# """Chat 连接上下文管理类(与你的原有 Context 兼容)""" +# user_id: str +# conversation_id: Optional[str] = None +# current_context: Dict = None +# # 可添加更多业务字段(如请求ID、会话状态等) +# +# def __post_init__(self): +# if self.current_context is None: +# self.current_context = {} +# +# def update_conversation_id(self, conversation_id: str): +# """更新会话ID(历史对话用)""" +# self.conversation_id = conversation_id +# self.current_context['conversation_id'] = conversation_id +# +# def add_context_data(self, key: str, value): +# """添加上下文数据""" +# self.current_context[key] = value +# +# def get_context_data(self, key: str, default=None): +# """获取上下文数据""" +# return self.current_context.get(key, default) + + +class LLMManager: + """Chat 管理器(与 ASRManager 结构、接口完全一致)""" + _instance: Optional[ChatBase] = None # 单例存储(对应 ASRManager._instance) + + @classmethod + async def initialize(cls) -> Tuple[bool, str]: + """初始化 Chat 服务(与 ASRManager.initialize 接口一致)""" + try: + # 1. 通过工厂创建 Chat 实例 + cls._instance = ChatFactory.get_chat_client() + + # 2. 初始化 Chat 服务(如连接池) + init_success = await cls._instance.initialize() + if not init_success: + return False, "Chat 服务初始化失败" + + # 3. 检查有效连接数 + valid_conn_count = await cls._instance.get_valid_connection_count() + if valid_conn_count == 0: + return False, f"Chat 有效连接数为 0(配置池大小:{settings.CHAT_POOL_SIZE})" + + return True, f"Chat 初始化成功:有效连接数 {valid_conn_count}" + except Exception as e: + return False, f"Chat 初始化失败:{str(e)}" + + @classmethod + def get_instance(cls) -> Optional[ChatBase]: + """获取全局 Chat 实例(业务代码调用,与 ASR 用法一致)""" + return cls._instance + + @classmethod + async def close(cls): + """关闭 Chat 服务(FastAPI 关闭时调用,与 ASR 一致)""" + if cls._instance: + await cls._instance.close() + cls._instance = None + print("Chat 管理器:实例和连接池已关闭") + + @classmethod + def is_healthy(cls) -> bool: + """检查 Chat 服务健康状态(与 ASR 一致)""" + return cls._instance is not None \ No newline at end of file diff --git a/audio_ai_chat/audio_ai_chat/core/tts/__pycache__/__init__.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/core/tts/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..57473b3 Binary files /dev/null and b/audio_ai_chat/audio_ai_chat/core/tts/__pycache__/__init__.cpython-310.pyc differ diff --git a/audio_ai_chat/audio_ai_chat/core/tts/__pycache__/tts_client.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/core/tts/__pycache__/tts_client.cpython-310.pyc new file mode 100644 index 0000000..c0524cd Binary files /dev/null and b/audio_ai_chat/audio_ai_chat/core/tts/__pycache__/tts_client.cpython-310.pyc differ diff --git a/audio_ai_chat/audio_ai_chat/core/tts/tts_client.py b/audio_ai_chat/audio_ai_chat/core/tts/tts_client.py new file mode 100644 index 0000000..ed4205c --- /dev/null +++ b/audio_ai_chat/audio_ai_chat/core/tts/tts_client.py @@ -0,0 +1,692 @@ +import asyncio +import json +import websockets +import numpy as np +import sounddevice as sd +from typing import Optional, Callable, Dict, Any, List +from dataclasses import dataclass, field +import uuid +import copy +from protocols import ( + EventType, + MsgType, + finish_connection, + finish_session, + receive_message, + start_connection, + start_session, + task_request, + wait_for_event, +) + +# ------------------------------ +# 配置常量(可根据需求调整) +# ------------------------------ +DEFAULT_APPID = "7069844318" +DEFAULT_ACCESS_TOKEN = "osFMEJr20SSTWRql43cJlZkAOg7iwvxu" +DEFAULT_ENDPOINT = "wss://openspeech.bytedance.com/api/v3/tts/bidirection" +DEFAULT_VOICE_TYPE = "zh_female_gaolengyujie_emo_v2_mars_bigtts" +DEFAULT_ENCODING = "pcm" +DEFAULT_SAMPLE_RATE = 16000 + + +@dataclass +class TTSRequest: + """TTS请求对象(带唯一标识)""" + tts_text: str + voice_type: str = DEFAULT_VOICE_TYPE + encoding: str = DEFAULT_ENCODING + speed: float = 1.0 # 语速(字节跳动TTS支持,需服务端兼容) + stream: bool = True # 是否流式合成 + request_id: str = field(default_factory=lambda: str(uuid.uuid4())) # 唯一请求ID + session_id: str = field(default_factory=lambda: str(uuid.uuid4())) # 会话ID(每个请求一个会话) + +class ByteDanceTTSSocketClient: + """字节跳动 TTS WebSocket 客户端(异步/流式/带任务队列)""" + + def __init__( + self, + appid: str = DEFAULT_APPID, + access_token: str = DEFAULT_ACCESS_TOKEN, + endpoint: str = DEFAULT_ENDPOINT, + max_queue_size: int = 100 + ): + """ + 初始化客户端 + :param appid: 字节跳动APP ID + :param access_token: 访问令牌 + :param endpoint: WebSocket 服务端地址 + :param max_queue_size: 最大队列长度(防止内存溢出) + """ + # 基础配置 + self.appid = appid + self.access_token = access_token + self.endpoint = endpoint + self.max_queue_size = max_queue_size + + # WebSocket 连接状态 + self.websocket: Optional[websockets.WebSocketClientProtocol] = None + self.is_connected = False + self.is_processing = False # 是否正在处理请求 + self.logid: Optional[str] = None # 服务端返回的日志ID + + # 异步任务队列(FIFO) + self.request_queue: asyncio.Queue[TTSRequest] = asyncio.Queue(maxsize=max_queue_size) + # 回调函数定义(所有回调都带request_id,方便关联请求) + self.on_task_enqueue: Callable[[str], None] = lambda req_id: None # 任务入队回调 + self.on_start: Callable[[str, Dict[str, Any]], None] = lambda req_id, data: None # 合成开始回调 + self.on_audio_chunk: Callable[[str, bytes], None] = lambda req_id, chunk: None # 音频块回调(原始字节) + self.on_end: Callable[[str, Dict[str, Any]], None] = lambda req_id, data: None # 合成结束回调 + self.on_error: Callable[[str, str], None] = lambda req_id, msg: None # 错误回调 + self.on_queue_full: Callable[[str], None] = lambda req_id: None # 队列满回调 + + # 音频播放相关(支持MP3格式直接播放) + self.play_stream: Optional[sd.OutputStream] = None + self.current_req_id: Optional[str] = None + self.enable_playback: bool = True # 是否启用实时播放 + + # 外部回调函数(返回完整结果) + self.external_callback: Optional[Callable[[str, Dict[str, Any]], None]] = None + + # 存储每个请求的完整音频数据(原始字节) + self.audio_buffers: Dict[str, List[bytes]] = {} + + def _get_resource_id(self, voice_type: str) -> str: + """根据音色类型获取资源ID(字节跳动TTS协议要求)""" + if voice_type.startswith("S_"): + return "volc.megatts.default" + return "volc.service_type.10029" + + async def _create_websocket_connection(self): + """创建WebSocket连接(内部使用)""" + headers = { + "X-Api-App-Key": self.appid, + "X-Api-Access-Key": self.access_token, + "X-Api-Resource-Id": self._get_resource_id(DEFAULT_VOICE_TYPE), # 用默认音色获取资源ID + "X-Api-Connect-Id": str(uuid.uuid4()), + } + + print(f"连接到 TTS 服务端: {self.endpoint}") + self.websocket = await websockets.connect( + self.endpoint, + additional_headers=headers, + max_size=10 * 1024 * 1024 # 10MB缓冲区 + ) + self.is_connected = True + self.logid = self.websocket.response.headers.get("x-tt-logid") + print(f"连接成功,LogID: {self.logid}") + + # 发送连接启动指令 + await start_connection(self.websocket) + await wait_for_event( + self.websocket, MsgType.FullServerResponse, EventType.ConnectionStarted + ) + print("TTS连接已初始化完成") + + async def connect(self): + """建立WebSocket连接(外部调用,初始化一次)""" + if not self.is_connected: + try: + await self._create_websocket_connection() + # 启动队列消费协程(后台运行) + asyncio.create_task(self._consume_queue()) + except Exception as e: + error_msg = f"连接失败: {str(e)}" + print(error_msg) + raise ConnectionError(error_msg) + + async def disconnect(self): + """关闭WebSocket连接""" + if self.is_connected and self.websocket: + try: + # 发送连接结束指令 + await finish_connection(self.websocket) + await wait_for_event( + self.websocket, MsgType.FullServerResponse, EventType.ConnectionFinished + ) + except Exception as e: + print(f"关闭连接时异常: {str(e)}") + finally: + await self.websocket.close() + self.is_connected = False + self.websocket = None + print("已断开与TTS服务端的连接") + + # 清理音频播放流 + if self.play_stream: + self.play_stream.stop() + self.play_stream.close() + self.play_stream = None + + def set_external_callback(self, callback: Callable[[str, Dict[str, Any]], None]): + """设置外部回调函数,用于返回完整结果""" + self.external_callback = callback + + def set_playback_enabled(self, enabled: bool): + """设置是否启用音频实时播放""" + self.enable_playback = enabled + print(f"音频实时播放已{'启用' if enabled else '禁用'}") + + async def synthesize(self, tts_text: str, **kwargs) -> str: + """ + 异步非阻塞添加TTS请求到队列 + :param tts_text: 要合成的文本 + :param kwargs: 其他TTS参数(voice_type, encoding, speed等) + :return: 唯一请求ID + """ + # 创建请求对象(支持覆盖默认参数) + request = TTSRequest(tts_text=tts_text, **kwargs) + req_id = request.request_id + + # 初始化音频缓冲区 + self.audio_buffers[req_id] = [] + + # 异步入队(非阻塞) + try: + await self.request_queue.put(request) + self.on_task_enqueue(req_id) + print(f"请求 [{req_id[:8]}] 已加入队列,当前队列长度: {self.request_queue.qsize()}") + return req_id + except asyncio.QueueFull: + self.on_queue_full(req_id) + error_msg = f"队列已满(最大长度{self.max_queue_size}),请求 [{req_id[:8]}] 入队失败" + print(error_msg) + raise Exception(error_msg) + + async def _consume_queue(self): + """消费队列(后台协程,自动处理排队请求)""" + print("队列消费协程已启动") + while True: + try: + # 等待队列中有请求(阻塞,直到有任务) + request = await self.request_queue.get() + req_id = request.request_id + + # 标记为处理中 + self.is_processing = True + # print(f"\n开始处理请求 [{req_id[:8]}],剩余队列长度: {self.request_queue.qsize()}") + + # 处理单个请求 + await self._process_single_request(request) + + # 标记任务完成(让Queue知道可以继续) + self.request_queue.task_done() + self.is_processing = False + + except Exception as e: + error_msg = f"队列消费异常: {str(e)}" + print(error_msg) + self.is_processing = False + # 短暂等待,避免死循环占用CPU + await asyncio.sleep(0.1) + + def _build_base_request(self, request: TTSRequest) -> Dict[str, Any]: + """构建字节跳动TTS基础请求参数""" + aaa = { + "user": {"uid": str(uuid.uuid4())}, + "namespace": "BidirectionalTTS", + "req_params": { + "speaker": request.voice_type, + "audio_params": { + "format": request.encoding, + "sample_rate": DEFAULT_SAMPLE_RATE, + "enable_timestamp": True, + }, + "additions": json.dumps({"disable_markdown_filter": False}), + "speed": request.speed, # 语速参数(需服务端支持) + }, + } + print('aaa', aaa) + return aaa + async def _send_text_stream(self, request: TTSRequest, session_id: str): + """流式发送文本(逐字符发送,字节跳动TTS流式协议要求)""" + base_request = self._build_base_request(request) + text = request.tts_text.strip() + + if not text: + print(f"请求 [{request.request_id[:8]}] 文本为空,跳过发送") + return + + # 逐字符发送(控制发送速率,避免拥塞) + for char in text: + if not self.is_connected or not self.websocket: + raise ConnectionError("连接已断开,无法继续发送文本") + + # 构建单个字符的任务请求 + task_req = copy.deepcopy(base_request) + task_req["event"] = EventType.TaskRequest + task_req["req_params"]["text"] = char + + # 发送任务请求 + await task_request( + self.websocket, + json.dumps(task_req).encode("utf-8"), + session_id + ) + # 控制发送速率(5ms/字符,可调整) + await asyncio.sleep(0.005) + + # 发送会话结束指令 + await finish_session(self.websocket, session_id) + print(f"请求 [{request.request_id[:8]}] 文本发送完成") + + async def _handle_audio_response(self, req_id: str, session_id: str, request: TTSRequest) -> Dict[str, Any]: + """处理服务端的流式音频响应""" + if not self.websocket: + raise ConnectionError("WebSocket连接未建立") + + sample_rate = DEFAULT_SAMPLE_RATE + audio_received = False + + try: + while True: + # 接收服务端消息(异步阻塞) + msg = await receive_message(self.websocket) + + if msg.type == MsgType.FullServerResponse: + # 完整响应(开始/结束/错误) + if msg.event == EventType.SessionStarted: + # 会话开始回调 + start_data = { + "session_id": session_id, + "sample_rate": sample_rate, + "encoding": request.encoding, + "voice_type": request.voice_type, + "logid": self.logid + } + self.on_start(req_id, start_data) + print(f"请求 [{req_id[:8]}] 合成开始") + + elif msg.event == EventType.SessionFinished: + # 会话结束,退出循环 + end_data = {"session_id": session_id, "message": "合成完成"} + self.on_end(req_id, end_data) + print(f"请求 [{req_id[:8]}] 合成结束") + break + + elif msg.type == MsgType.AudioOnlyServer: + # 流式音频数据(原始字节) + audio_chunk = msg.payload + if audio_chunk: + audio_received = True + # 保存到缓冲区 + self.audio_buffers[req_id].append(audio_chunk) + # 音频块回调 + self.on_audio_chunk(req_id, audio_chunk) + # 实时播放(如果启用) + await self._play_audio_chunk(req_id, audio_chunk, sample_rate) + + else: + # 未知消息类型 + raise RuntimeError(f"收到未知消息类型: {msg.type}, 内容: {msg}") + + # 组装完整结果 + full_audio = b"".join(self.audio_buffers[req_id]) if self.audio_buffers[req_id] else b"" + return { + "status": "completed", + "request_id": req_id, + "session_id": session_id, + "sample_rate": sample_rate, + "encoding": request.encoding, + "audio_data": full_audio, # 完整音频字节数据 + "audio_length": len(full_audio), + "message": "合成成功" if audio_received else "合成完成但未收到音频数据" + } + + except Exception as e: + error_msg = f"处理音频响应异常: {str(e)}" + self.on_error(req_id, error_msg) + return { + "status": "error", + "request_id": req_id, + "session_id": session_id, + "message": error_msg + } + + async def _play_audio_chunk(self, req_id: str, chunk: bytes, sample_rate: int): + """实时播放音频块(支持MP3格式)""" + if not self.enable_playback: + return + + # 确保当前请求是正在播放的请求 + if self.current_req_id is None: + self.current_req_id = req_id + + if req_id != self.current_req_id: + # 切换请求时,重置播放流 + if self.play_stream: + self.play_stream.stop() + self.play_stream.close() + self.current_req_id = req_id + + try: + # 初始化播放流(如果未初始化) + if not self.play_stream: + self.play_stream = sd.OutputStream( + samplerate=sample_rate, + channels=1, # 单声道 + dtype=np.float32 + ) + self.play_stream.start() + + # MP3字节 → 音频数组(直接播放) + # 注意:sounddevice默认支持PCM格式,如果是MP3需要解码,这里简化处理(实际使用建议用pydub解码) + # 如需支持MP3播放,请安装 pydub: pip install pydub ffmpeg + try: + # 简化处理:假设服务端返回PCM(如果是MP3,需替换为解码逻辑) + audio_array = np.frombuffer(chunk, dtype=np.float32) + if audio_array.size > 0: + self.play_stream.write(audio_array) + except Exception as e: + print(f"音频播放异常: {str(e)},请确保音频格式正确") + + except Exception as e: + print(f"播放流初始化失败: {str(e)}") + + async def _process_single_request(self, request: TTSRequest): + """处理单个TTS请求(完整流程:连接→启动会话→流式发送文本→接收音频→回调结果)""" + req_id = request.request_id + session_id = request.session_id + + # 参数校验 + if not request.tts_text.strip(): + error_msg = "合成文本不能为空" + self.on_error(req_id, error_msg) + self._send_external_callback(req_id, { + "status": "error", + "request_id": req_id, + "session_id": session_id, + "message": error_msg + }) + return + + # 确保连接已建立(断开时自动重连) + if not self.is_connected: + print(f"请求 [{req_id[:8]}] 处理时连接已断开,尝试重连...") + try: + await self._create_websocket_connection() + except Exception as e: + error_msg = f"重连失败: {str(e)}" + self.on_error(req_id, error_msg) + self._send_external_callback(req_id, { + "status": "error", + "request_id": req_id, + "session_id": session_id, + "message": error_msg + }) + return + + try: + # 1. 启动会话 + base_request = self._build_base_request(request) + start_session_req = copy.deepcopy(base_request) + start_session_req["event"] = EventType.StartSession + + await start_session( + self.websocket, + json.dumps(start_session_req).encode("utf-8"), + session_id + ) + # 等待会话启动成功 + await wait_for_event( + self.websocket, MsgType.FullServerResponse, EventType.SessionStarted + ) + + # 2. 异步流式发送文本(后台任务,不阻塞接收音频) + send_task = asyncio.create_task(self._send_text_stream(request, session_id)) + + # 3. 接收并处理音频响应 + result_data = await self._handle_audio_response(req_id, session_id, request) + + # 4. 等待文本发送任务完成 + await send_task + + # 5. 发送外部回调 + self._send_external_callback(req_id, result_data) + + except Exception as e: + error_msg = f"处理请求 [{req_id[:8]}] 异常: {str(e)}" + self.on_error(req_id, error_msg) + self._send_external_callback(req_id, { + "status": "error", + "request_id": req_id, + "session_id": session_id, + "message": error_msg + }) + finally: + # 清理缓冲区 + if req_id in self.audio_buffers: + del self.audio_buffers[req_id] + + def _send_external_callback(self, req_id: str, result_data: Dict[str, Any]): + """发送外部回调(支持同步/异步回调函数)""" + if not self.external_callback: + return + + try: + # 异步回调:直接await + if asyncio.iscoroutinefunction(self.external_callback): + asyncio.create_task(self.external_callback(req_id, result_data)) + # 同步回调:在线程池中执行(避免阻塞事件循环) + else: + asyncio.get_event_loop().run_in_executor( + None, self.external_callback, req_id, result_data + ) + except Exception as e: + print(f"外部回调执行异常: {str(e)}") + + async def wait_all_completed(self): + """等待队列中所有任务处理完成(阻塞)""" + await self.request_queue.join() + print("\n所有队列任务已处理完成") + + +# ------------------------------ +# 使用示例(与你提供的风格完全一致) +# ------------------------------ +class TTSManager: + """TTS管理器 - 供外部代码调用(封装客户端,简化使用)""" + + def __init__( + self, + appid: str = DEFAULT_APPID, + access_token: str = DEFAULT_ACCESS_TOKEN, + endpoint: str = DEFAULT_ENDPOINT + ): + self.client = ByteDanceTTSSocketClient( + appid=appid, + access_token=access_token, + endpoint=endpoint + ) + self._setup_internal_callbacks() + + def _setup_internal_callbacks(self): + """设置内部回调(日志/状态提示)""" + + def on_task_enqueue(req_id: str): + """任务入队回调""" + print(f"📥 任务 [{req_id[:8]}] 已入队") + + def on_tts_start(req_id: str, data: Dict[str, Any]): + """合成开始回调""" + print(f"🎤 合成开始 [{req_id[:8]}] - 采样率: {data['sample_rate']}, 编码: {data['encoding']}") + + def on_audio_chunk(req_id: str, chunk: bytes): + """音频块回调(内部仅打印日志,外部通过external_callback获取)""" + print(f"🔊 收到音频块 [{req_id[:8]}] - 大小: {len(chunk)}字节", end="\r") + + def on_tts_end(req_id: str, data: Dict[str, Any]): + """合成结束回调""" + print(f"\n🏁 合成结束 [{req_id[:8]}] - 会话ID: {data['session_id']}") + + def on_tts_error(req_id: str, msg: str): + """错误回调""" + print(f"\n❌ 合成失败 [{req_id[:8]}] - 错误: {msg}") + + def on_queue_full(req_id: str): + """队列满回调""" + print(f"⚠️ 队列已满,请求 [{req_id[:8]}] 入队失败") + + # 绑定内部回调 + self.client.on_task_enqueue = on_task_enqueue + self.client.on_start = on_tts_start + self.client.on_audio_chunk = on_audio_chunk + self.client.on_end = on_tts_end + self.client.on_error = on_tts_error + self.client.on_queue_full = on_queue_full + + async def initialize(self): + """初始化连接""" + await self.client.connect() + + async def shutdown(self): + """关闭连接""" + await self.client.disconnect() + + def set_result_callback(self, callback: Callable[[str, Dict[str, Any]], None]): + """设置外部结果回调(获取完整音频数据)""" + self.client.set_external_callback(callback) + + def set_playback_enabled(self, enabled: bool): + """设置是否启用实时播放""" + self.client.set_playback_enabled(enabled) + + async def synthesize(self, text: str, **kwargs) -> str: + """ + 异步非阻塞合成文本 + :param text: 要合成的文本 + :param kwargs: 其他参数(voice_type, encoding, speed等) + :return: 请求ID + """ + return await self.client.synthesize(text, **kwargs) + + async def wait_all_completed(self): + """等待所有任务完成""" + await self.client.wait_all_completed() + + +# ------------------------------ +# 外部调用示例 +# ------------------------------ +async def external_usage_example(): + """外部代码使用示例""" + # 1. 创建TTS管理器(可替换为自己的appid和access_token) + tts_manager = TTSManager( + appid=DEFAULT_APPID, + access_token=DEFAULT_ACCESS_TOKEN, + endpoint=DEFAULT_ENDPOINT + ) + + # 2. 设置外部结果回调(获取完整音频数据) + def handle_tts_result(req_id: str, result: Dict[str, Any]): + """处理TTS完整结果(同步回调)""" + status = result.get("status") + if status == "completed": + audio_data = result.get("audio_data") + encoding = result.get("encoding") + audio_length = result.get("audio_length") + + print(f"\n✅ 收到完整结果 [{req_id[:8]}] - 长度: {audio_length}字节, 编码: {encoding}") + + # 保存音频文件 + filename = f"tts_output_{req_id[:8]}.{encoding}" + with open(filename, "wb") as f: + f.write(audio_data) + print(f"💾 音频文件已保存: {filename}") + + elif status == "error": + error_msg = result.get("message") + print(f"\n❌ 请求 [{req_id[:8]}] 处理失败: {error_msg}") + + # 绑定外部回调 + tts_manager.set_result_callback(handle_tts_result) + + # 3. 设置是否启用实时播放(默认True) + tts_manager.set_playback_enabled(True) + + # 4. 初始化连接 + await tts_manager.initialize() + + # 5. 异步提交多个TTS请求(非阻塞) + texts = [ + "你好,这是字节跳动TTS的流式合成测试。", + "我支持异步非阻塞调用,多个请求可以排队处理。", + "每个请求都会返回唯一的ID,方便你跟踪结果。", + "音频数据会通过回调函数返回,支持实时播放和保存文件。", + "最后一个测试句子,演示队列的自动消费功能。" + ] + + req_ids = [] + for i, text in enumerate(texts): + # 提交请求(非阻塞,立即返回) + req_id = await tts_manager.synthesize( + text, + voice_type=DEFAULT_VOICE_TYPE, + encoding=DEFAULT_ENCODING, + speed=1.0 + ) + req_ids.append(req_id) + print(f"📤 已提交请求 {i+1}: ID={req_id[:8]}") + + # 模拟其他业务逻辑(无需等待TTS完成) + await asyncio.sleep(0.3) + + # 6. 等待所有TTS任务完成(可选,根据业务需求决定是否等待) + await tts_manager.wait_all_completed() + + # 7. 关闭连接(程序退出前调用) + await tts_manager.shutdown() + + +# ------------------------------ +# 异步结果回调示例(高级用法) +# ------------------------------ +async def async_result_callback(req_id: str, result: Dict[str, Any]): + """异步结果回调(支持异步操作,如上传音频到服务器)""" + if result["status"] == "completed": + print(f"\n⚡ 异步处理结果 [{req_id[:8]}] - 开始上传音频...") + # 模拟异步上传操作 + await asyncio.sleep(0.5) + print(f"⚡ 异步处理结果 [{req_id[:8]}] - 音频上传完成") + + +async def advanced_usage_example(): + """高级使用示例:异步回调 + 禁用播放 + 批量请求""" + tts_manager = TTSManager() + + # 设置异步结果回调 + tts_manager.set_result_callback(async_result_callback) + + # 禁用实时播放(只获取音频数据) + tts_manager.set_playback_enabled(False) + + await tts_manager.initialize() + + # 批量提交请求(并行提交) + tasks = [] + for i in range(3): + text = f"这是第{i+1}个高级测试文本,使用异步回调处理结果。" + task = tts_manager.synthesize(text, speed=0.9) + tasks.append(task) + + # 并行提交所有请求 + req_ids = await asyncio.gather(*tasks) + print(f"\n已并行提交 {len(req_ids)} 个请求") + + # 等待所有任务完成 + await tts_manager.wait_all_completed() + await tts_manager.shutdown() + + +if __name__ == "__main__": + try: + # 运行基础使用示例 + asyncio.run(external_usage_example()) + + # 运行高级使用示例(取消注释) + # asyncio.run(advanced_usage_example()) + + except KeyboardInterrupt: + print("\n程序被用户中断") + except Exception as e: + print(f"程序异常: {str(e)}") \ No newline at end of file diff --git a/audio_ai_chat/audio_ai_chat/core/websocket_handler.py b/audio_ai_chat/audio_ai_chat/core/websocket_handler.py index 76d29ee..c913812 100644 --- a/audio_ai_chat/audio_ai_chat/core/websocket_handler.py +++ b/audio_ai_chat/audio_ai_chat/core/websocket_handler.py @@ -1,113 +1,91 @@ -from fastapi import WebSocket -from typing import Dict, List, Optional - -from pyexpat.errors import messages - -from audio_ai_chat.config.logger import logger -# from audio_ai_chat.core.asr.factory import ASRFactory # 导入ASR工厂 -# from audio_ai_chat.core.llm.factory import LLMFactory # 导入LLM工厂 -# from audio_ai_chat.core.tts.factory import TTSFactory # 导入TTS工厂 -from audio_ai_chat.config.settings import settings -from audio_ai_chat.utils.exceptions import ServiceCallError from fastapi import WebSocket, WebSocketDisconnect -from audio_ai_chat.codec.ProtocolCodec import ProtocolCodec -from typing import Optional, Dict, Callable, Awaitable, List, Any, Coroutine +from typing import Dict, List, Optional, Callable,Any from dataclasses import dataclass, field -import sys -import json import asyncio -import websockets import uuid import logging -from audio_ai_chat.core.connection import ConnectionContext +from audio_ai_chat.config.logger import logger +from audio_ai_chat.core.asr.asr_manager import ASRManager +from audio_ai_chat.core.connection import ConnectionManager, ConnectionContext from audio_ai_chat.codec.ProtocolCodec import ProtocolCodec, MessageType +from audio_ai_chat.utils.exceptions import ServiceCallError +from audio_ai_chat.core.llm.dify.dify import LLMConversation, llm_client +from audio_ai_chat.core.tts.tts_client import TTSManager +from functools import partial - +# 全局WebSocket连接管理器(单例模式,确保全局统一) class WebSocketConnectionManager: - """WebSocket连接管理器""" + _instance: Optional["WebSocketConnectionManager"] = None def __init__(self): - # 活跃连接列表 - self.active_connections: List[WebSocket] = [] - # 关键映射:client_id -> ConnectionContext(快速获取用户专属上下文) - self.client_context_map: Dict[str, ConnectionContext] = {} - # self.asr_client = ASRFactory.get_asr_client() - # self.tts_client = TTSFactory.get_tts_client() - # 用户LLM会话存储 - # self.user_llm_conversations: Dict[str, LLMConversation] = {} - # 全局唤醒事件 - self.consume_wakeup = asyncio.Event() + self.connection_manager = None - async def connect(self, client_id, websocket: WebSocket) -> ConnectionContext: - """ - 建立连接+身份校验(前端主动发送身份信息) - 超时逻辑:5秒内未收到前端身份信息,自动关闭连接 - 返回:校验通过的 ConnectionContext(保证非空) - """ - # 1. 接受连接并加入活跃列表 + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + cls._instance.active_connections: List[WebSocket] = [] + cls._instance.client_context_map: Dict[str, ConnectionContext] = {} + cls._instance.connection_manager: Optional[ConnectionManager] = None + cls._instance.consume_wakeup = asyncio.Event() + return cls._instance + + async def initialize(self): + """初始化:获取ConnectionManager全局单例""" + self.connection_manager = await ConnectionManager.get_instance() + logger.info("WebSocketConnectionManager 初始化成功") + + async def connect(self, client_id: str, websocket: WebSocket) -> ConnectionContext: + """建立连接+身份校验""" await websocket.accept() - context = ConnectionContext(client_id=client_id) # 提前创建上下文(保证最终返回非空) - self.active_connections.append(websocket) logger.info( - f"连接 {client_id} 已接受,等待前端发送身份信息(5秒超时)...,当前连接数: {len(self.active_connections)}") + f"连接 {client_id} 已接受,等待身份信息(5秒超时),当前连接数: {len(self.active_connections)}" + ) - # 2. 超时控制:5秒内未收到身份信息 -> 关闭连接 + # 超时接收身份包 try: - ping_packet = await asyncio.wait_for( - websocket.receive_bytes(), - timeout=5.0 - ) + ping_packet = await asyncio.wait_for(websocket.receive_bytes(), timeout=5.0) except asyncio.TimeoutError: - error_msg = f"连接 {client_id} 身份校验超时(5秒未收到消息)" + error_msg = f"连接 {client_id} 身份校验超时" logger.warning(error_msg) - # 发送超时错误响应(二进制格式) error_packet = ProtocolCodec.pack( - MessageType.ERROR, - {"code": 1008, "message": "身份校验超时,请重试"} + MessageType.ERROR, {"code": 1008, "message": "身份校验超时,请重试"} ) await websocket.send_bytes(error_packet) - raise TimeoutError(error_msg) # 抛出异常,进入后续清理逻辑 + raise TimeoutError(error_msg) - # 3. 解包并验证包类型 - print('ping_packet', ping_packet) + # 解包并验证包类型 msg_type, _, identity_data = ProtocolCodec.unpack(ping_packet) - if msg_type != MessageType.IDENTITY: - error_msg = f"连接 {client_id} 首个包类型错误(期望{MessageType.IDENTITY.value},实际{msg_type.value})" + error_msg = f"连接 {client_id} 首个包类型错误" logger.error(error_msg) - # 发送类型错误响应 error_packet = ProtocolCodec.pack( - MessageType.ERROR, - {"code": 4001, "message": "非法请求:首个包必须是身份校验包"} + MessageType.ERROR, {"code": 4001, "message": "非法请求:首个包必须是身份校验包"} ) await websocket.send_bytes(error_packet) raise ValueError(error_msg) - # 4. 身份信息并校验 - # todo - # 提取核心字段(必选字段校验) + # 校验身份信息 user_id = identity_data.get("user_id") token = identity_data.get("token") - name = identity_data.get("name") or f"用户{user_id}" # 提供默认名称 - + name = identity_data.get("name") or f"用户{user_id}" if not all([user_id, token]): - error_msg = f"连接 {client_id} 身份信息不完整(缺少user_id或token)" + error_msg = f"连接 {client_id} 身份信息不完整" logger.error(error_msg) error_packet = ProtocolCodec.pack( - MessageType.ERROR, - {"code": 4003, "message": "身份信息不完整:必须包含user_id和token"} + MessageType.ERROR, {"code": 4003, "message": "身份信息不完整:必须包含user_id和token"} ) await websocket.send_bytes(error_packet) raise ValueError(error_msg) - # TODO: 实际身份校验逻辑(根据你的业务扩展) - - # 5. 校验通过:更新上下文并响应前端 + # 创建/获取连接上下文 + context = await self.connection_manager.create_or_reconnect_context( + new_client_id=client_id, user_id=user_id + ) context.set_user_info(token, user_id, name) - self.client_context_map[client_id] = context # 加入上下文映射 + self.client_context_map[client_id] = context - # 发送成功响应 + # 响应身份校验成功 success_packet = ProtocolCodec.pack( MessageType.IDENTITY, { @@ -117,309 +95,225 @@ class WebSocketConnectionManager: } ) await websocket.send_bytes(success_packet) - logger.info(f"用户 {user_id}({name})身份校验通过,连接就绪(client_id: {client_id})") - + logger.info(f"用户 {user_id}({name})身份校验通过(client_id: {client_id})") return context - # 初始化LLM会话 - # self._init_llm_conversation(user_id) - # return conn_id, user_id, conn_id # conn_id 同时作为 tts_session_id - - # def _init_llm_conversation(self, user_id: str): - # """初始化用户LLM会话""" - # if user_id not in self.user_llm_conversations: - # self.user_llm_conversations[user_id] = LLMConversation( - # user_id=user_id, - # scene_description="语音识别对话场景" - # ) - - def disconnect(self, websocket: WebSocket, conn_id: str): - """断开连接并清理资源""" - if websocket in self.active_connections: - self.active_connections.remove(websocket) - logger.info(f"连接 {conn_id} 已断开,当前连接数: {len(self.active_connections)}") - - # async def setup_tts_manager(self, result_queue: asyncio.Queue) -> TTSManager: - # """初始化TTS管理器""" - # - # def handle_tts_result(req_id: str, result: Dict[str, Any]): - # """TTS结果回调处理""" - # try: - # status = result.get("status") - # if status == "completed": - # audio_data = result.get("audio_data") - # if audio_data is not None and len(audio_data) > 0: - # # 转换为PCM格式 - # pcm_data = (audio_data.astype(np.float32) * 32767).astype(np.int16) - # pcm_bytes = pcm_data.tobytes() - # result_queue.put_nowait(pcm_bytes) - # except Exception as e: - # logger.error(f"TTS结果处理失败: {str(e)}") - # - # self.tts_client = TTSFactory.get_tts_client() - # tts_manager = TTSManager() - # tts_manager.set_result_callback(handle_tts_result) - # tts_manager.set_playback_enabled(False) - # await tts_manager.initialize() - # return tts_manager - - async def asr_result_callback(self, result: dict, websocket: WebSocket, - user_id: str, result_queue: asyncio.Queue): - """ASR结果回调处理""" - try: - logger.info(f"ASR识别结果: {result}") - final_asr_text = result.get("text", "").strip() - - # 转发ASR结果到前端队列 - if final_asr_text: - print(f"插入ASR结果时队列大小: {result_queue.qsize()}") - self.consume_wakeup.set() # 唤醒消费协程 - - # 异步调用大模型 - llm_conversation = self.user_llm_conversations.get(user_id) - if llm_conversation: - asyncio.create_task( - self.call_llm_and_send( - query=final_asr_text, - conversation=llm_conversation, - websocket=websocket - ) + def _create_asr_callback(self, context: ConnectionContext) -> Callable[[dict], None]: + """ + 闭包:为当前连接创建专属的ASR回调函数 + 回调内部持有ConnectionContext引用,直接操作其消息队列 + """ + async def asr_result_callback(result: dict): + """专属回调:将ASR结果打包后插入当前连接的消息队列""" + try: + print('result', 'result', result) + # 处理错误结果 + if result.get("error"): + logger.error(f"ASR错误(client_id: {context.client_id}):{result['error']}") + # 打包错误消息 + error_packet = ProtocolCodec.pack( + MessageType.ERROR, + {"code": 5001, "message": f"ASR服务错误:{result['error']}"} ) + context.message_queue.put_nowait(error_packet) + else: + # 3. 调用Dify流式接口(传入ASR元数据,用于存储到对话历史) + print('context', context.user_id) + final_asr_text = result.get("text", "") + # 2. 调用大模型(异步) + if final_asr_text: + asyncio.create_task( + self.call_llm_and_send( + context=context, + query=final_asr_text, + conversation=context.llm_session + ) + ) + + logger.debug( + f"ASR结果入队(client_id: {context.client_id}):" + f"文本={result['text']},最终结果={result['is_final']}" + ) + except Exception as e: + logger.error(f"ASR回调处理失败(client_id: {context.client_id}):{str(e)}") + + return asr_result_callback + + # ====================== 调用大模型 ====================== + # ====================== 大模型流式回调 ====================== + @staticmethod + async def llm_stream_callback(context, chunk: str, conversation_id: str, is_finished: bool): + """大模型流式回调(纯异步,无阻塞)""" + if not chunk: + return + print('大模型流式回调', chunk) + req_id = await context.tts_client.synthesize(chunk) + print('req_id', req_id) + + async def call_llm_and_send(self,context ,query: str, conversation: LLMConversation): + """调用大模型,流式结果转发前端 + TTS""" + if not query: + return + logger.info(f"调用大模型 - 用户(): {query}") + + try: + + stream_callback = partial(WebSocketConnectionManager.llm_stream_callback, context) + + conv_id, full_reply = await llm_client.send_message( + query=query, + conversation=conversation, + stream_callback=stream_callback, # 传递绑定后的回调 + response_mode="streaming" + ) + logger.info(f"大模型回复完成 - 会话ID: {conv_id}, 完整回复: {full_reply}") except Exception as e: - logger.error(f"ASR回调执行失败: {str(e)}") + logger.error(f"大模型调用失败: {str(e)}") - # async def llm_stream_callback(self, chunk: str, tts_manager: TTSManager): - # """大模型流式回调处理""" - # if not chunk: - # return - # try: - # # 提交TTS合成请求 - # await tts_manager.synthesize(chunk) - # await asyncio.sleep(0) # 让出调度权 - # except Exception as e: - # logger.error(f"LLM流式回调处理失败: {str(e)}") - # async def call_llm_and_send(self, query: str, conversation: LLMConversation, websocket: WebSocket): - # """调用大模型并处理结果""" - # logger.info(f"调用大模型 - 用户({conversation.user_id}): {query}") - # try: - # conv_id, full_reply = await llm_client.send_message( - # query=query, - # conversation=conversation, - # stream_callback=self.llm_stream_callback, - # response_mode="streaming" - # ) - # logger.info(f"大模型回复完成 - 会话ID: {conv_id}, 完整回复: {full_reply}") - # except Exception as e: - # logger.error(f"大模型调用失败: {str(e)}") - # if not websocket.client_state.disconnected: - # await websocket.send_json({ - # "type": "llm_error", - # "data": {"error": str(e)} - # }) - async def recv_frontend_data(self, websocket: WebSocket, asr_conn): - """接收前端音频数据并推送到ASR""" - while not asr_conn.stop_event.is_set(): - try: - raw_bytes = await websocket.receive_bytes() - - # success = await push_audio_data(asr_conn, raw_bytes) - # if not success: - # logger.warning("音频数据插入ASR失败(队列满/连接失效)") - except WebSocketDisconnect: - logger.info("前端主动断开连接") - asr_conn.stop_event.set() - break - except Exception as e: - logger.error(f"接收前端数据失败: {str(e)}") - # asr_conn.stop_event.set() - # if not websocket.client_state.disconnected: - # await websocket.send_json({"error": f"接收数据失败: {str(e)}"}) - # break - - async def send_results(self, websocket: WebSocket, result_queue: asyncio.Queue, asr_conn): - """从结果队列发送数据到前端""" - while True: - try: - # 等待队列数据或超时 - result = await asyncio.wait_for(result_queue.get(), timeout=0.05) - # if not websocket.client_state.disconnected: - await websocket.send_bytes(result) - except asyncio.TimeoutError: - if asr_conn.stop_event.is_set(): - break - continue - except Exception as e: - logger.error(f"发送结果到前端失败: {str(e)}") - asr_conn.stop_event.set() - break async def handle_connection(self, websocket: WebSocket): """处理单个WebSocket连接的完整生命周期""" - client_id = str(id(websocket)) - context = None - + client_id = str(id(websocket)) # 生成唯一连接ID + logger.info(f"新WebSocket连接:client_id={client_id}") + context: Optional[ConnectionContext] = None + asr_conn = None + llm_conn = None # 新增:LLM连接变量 try: + # 1. 建立连接并获取上下文 context = await self.connect(client_id, websocket) + if not context: + logger.error(f"连接 {client_id} 上下文创建失败") + return - # 接收前端数据 + # 2. 获取ASR连接和专属回调 + asr_client = ASRManager.get_instance() + asr_conn = await asr_client.get_connection() + if not asr_conn: + raise ServiceCallError("获取ASR连接失败") + # 创建当前连接的专属ASR回调(闭包绑定context) + asr_callback = self._create_asr_callback(context) + + # 3. 启动ASR通信任务(传入专属回调) + communication_task = asyncio.create_task( + asr_client.start_communication(asr_conn, asr_callback) + ) + + context.llm_session = LLMConversation( + user_id=context.user_id, + scene_description="语音识别对话场景" + ) + + context.tts_client = TTSManager() + def handle_tts_result(context, req_id: str, result: Dict[str, Any]): + """处理TTS结果回调""" + status = result.get("status") + print('处理TTS结果回调') + if status == "completed": + audio_data = result.get("audio_data") + pack_data = ProtocolCodec.pack(MessageType.AUDIO_DATA, audio_data) + context.message_queue.put_nowait(pack_data) + print('插入', len(audio_data)) + + stream_callback = partial(handle_tts_result, context) + context.tts_client.set_result_callback(stream_callback) + # 3. 设置是否播放(可选,默认True) + context.tts_client.set_playback_enabled(False) # 设置为False则不播放 + + # 4. 初始化连接 + await context.tts_client.initialize() + + # 4. 定义前端数据接收任务 async def recv_frontend_data(): - """接收前端音频/控制指令""" - # while not asr_conn.stop_event.is_set(): while True: try: - if not context.message_queue.empty(): - await asyncio.sleep(0) # 立即让权 - continue raw_bytes = await websocket.receive_bytes() - unpack_bytes = ProtocolCodec.unpack(raw_bytes) - - success = await push_audio_data(asr_conn, unpack_bytes) - # if not success: - # print("音频数据插入失败(队列满/连接失效)") + msg_type, sequence, data = ProtocolCodec.unpack(raw_bytes) + if msg_type == MessageType.AUDIO_DATA: + # 推送音频数据到ASR + success = await asr_client.push_audio(asr_conn, data) + if not success: + logger.warning(f"连接 {client_id} 音频推送失败(队列满/连接失效)") + elif msg_type == MessageType.CONTROL: + # 处理控制指令(如暂停/继续ASR) + logger.info(f"连接 {client_id} 收到控制指令:{data}") + if data.get("action") == "stop_asr": + asr_conn.stop_event.set() + else: + logger.warning(f"连接 {client_id} 收到未知消息类型:{msg_type.value}") except WebSocketDisconnect: - logger.info(f"前端 {conn_id} 主动断开连接") - asr_conn.stop_event.set() + logger.info(f"前端 {client_id} 主动断开连接") break except Exception as e: - logger.error(f"接收前端数据失败: {str(e)}") - asr_conn.stop_event.set() - await websocket.send_json({"error": f"接收数据失败: {str(e)}"}) + logger.error(f"连接 {client_id} 接收前端数据失败:{str(e)}") break - # 发送 ASR 结果 + # 5. 定义ASR结果发送任务(从上下文队列取数据) async def send_asr_result(): - """从结果队列发送 ASR 结果到前端(二进制格式)""" while True: try: - result = await asyncio.wait_for(context.message_queue.get(), timeout=0.05) - - await websocket.send_bytes(result) + # 从当前连接的消息队列获取ASR结果(超时0.05秒避免阻塞) + result_packet = await asyncio.wait_for( + context.message_queue.get(), timeout=0.05 + ) + print('发送', result_packet) + await websocket.send_bytes(result_packet) except asyncio.TimeoutError: - continue + continue # 无数据时继续等待 except Exception as e: - logger.error(f"发送 ASR 结果失败: {str(e)}") + logger.error(f"连接 {client_id} 发送ASR结果失败:{str(e)}") break + # 6. 启动任务并等待完成 task_send = asyncio.create_task(send_asr_result()) task_recv = asyncio.create_task(recv_frontend_data()) - try: - # 等待两个任务,只要有一个完成就返回(比如前端断开/发送出错) - done, pending = await asyncio.wait( - [task_recv, task_send], - return_when=asyncio.FIRST_COMPLETED, - timeout=None # 无限等待,直到有任务完成 - ) - finally: - # 确保协程正确退出 - # 等待剩余任务完成 - for task in pending: - task.cancel() - await asyncio.gather(task_recv, task_send, return_exceptions=True) - pass + + done, pending = await asyncio.wait( + [task_recv, task_send, communication_task], + return_when=asyncio.FIRST_COMPLETED + ) + + # 取消未完成的任务 + for task in pending: + task.cancel() + await asyncio.gather(*pending, return_exceptions=True) + except Exception as e: - logger.error(f"WebSocket连接处理异常: {str(e)}") - - if websocket in self.active_connections: - self.active_connections.remove(websocket) - # 移除上下文映射(如果已添加) - if context is not None and context.client_id in self.client_context_map: - del self.client_context_map[client_id] + logger.error(f"连接 {client_id} 处理异常:{str(e)}") + # 异常时发送错误消息给前端 + if websocket.state == "CONNECTED": + error_packet = ProtocolCodec.pack( + MessageType.ERROR, {"code": 5000, "message": f"服务异常:{str(e)}"} + ) + await websocket.send_bytes(error_packet) finally: - # 6. 统一资源清理(无论成功/失败,都执行) - # 关闭WebSocket连接 - try: - if hasattr(websocket, "state") and websocket.state == "CONNECTED": - await websocket.close(code=1008, reason="连接终止") - except Exception as close_e: - logger.warning(f"关闭连接失败 (client_id: {client_id}): {str(close_e)}") + # 7. 资源清理(关键) + # 停止ASR通信任务 + # if communication_task and not communication_task.done(): + # communication_task.cancel() + # try: + # await communication_task + # except Exception as e: + # logger.warning(f"连接 {client_id} ASR任务取消异常:{str(e)}") - # 移除活跃连接 + # 关闭ASR连接 + # if asr_conn: + # await asr_client.close_connection(asr_conn) + + # 关闭WebSocket连接 + if websocket.state == "CONNECTED": + await websocket.close(code=1008, reason="连接终止") + + # 移除连接和上下文 if websocket in self.active_connections: self.active_connections.remove(websocket) - # 移除上下文映射 if client_id in self.client_context_map: del self.client_context_map[client_id] - if context: - pass - logger.info(f"连接资源清理完成 (client_id: {client_id}),当前连接数: {len(self.active_connections)}") - # 1. 建立连接 - - # 2. 初始化TTS - # tts_manager = await self.setup_tts_manager(result_queue) - - # 3. 获取ASR连接 - asr_conn = await get_idle_asr_connection() - if not asr_conn: - await websocket.send_json({"error": "ASR服务暂时不可用", "text": ""}) - return - - # 4. 启动ASR通信协程 - # asr_callback = lambda res: self.asr_result_callback(res, websocket, user_id, result_queue) - # asr_task = asyncio.create_task(handle_asr_communication(asr_conn, asr_callback)) - # - # # 5. 启动数据接收和发送协程 - # task_recv = asyncio.create_task(self.recv_frontend_data(websocket, asr_conn)) - # task_send = asyncio.create_task(self.send_results(websocket, result_queue, asr_conn)) - # - # # 6. 等待任一任务完成 - # done, pending = await asyncio.wait( - # [task_recv, task_send], - # return_when=asyncio.FIRST_COMPLETED - # ) - - # except Exception as e: - # logger.error(f"WebSocket连接处理异常: {str(e)}") - # if asr_conn: - # asr_conn.stop_event.set() - # if not websocket.client_state.disconnected: - # await websocket.send_json({"error": str(e)}) - - # logger.error(f"连接 {client_id} 建立失败: {type(e).__name__}: {e}") - # try: - # # 确保连接已关闭(处理未正常关闭的情况) - # if websocket.client_state == "CONNECTED": # 根据实际WebSocket类型调整状态判断 - # await websocket.close(code=1008, reason=str(e)) - # except: - # pass - - # 移除活跃连接(避免内存泄漏) - # if websocket in self.active_connections: - # self.active_connections.remove(websocket) - # # 移除上下文映射(如果已添加) - # if context is not None and context.client_id in self.client_context_map: - # del self.client_context_map[client_id] - # finally: - # pass - # 7. 资源清理 - # logger.info(f"开始清理连接 {conn_id} 的资源") - # # 停止ASR - # if asr_conn: - # asr_conn.stop_event.set() - # - # # 取消任务 - # if asr_task and not asr_task.done(): - # asr_task.cancel() - # try: - # await asr_task - # except asyncio.CancelledError: - # pass - # - # # 清理TTS - # if tts_manager: - # await tts_manager.cleanup() # 假设TTSManager有cleanup方法,无则忽略 - # - # # 断开连接 - # if websocket: - # self.disconnect(websocket, conn_id) - # try: - # await websocket.close() - # except Exception: - # pass - # - # logger.info(f"连接 {conn_id} 资源清理完成") + logger.info( + f"连接 {client_id} 资源清理完成,当前连接数: {len(self.active_connections)}" + ) diff --git a/audio_ai_chat/audio_ai_chat/core/websocket_handler_buck.py b/audio_ai_chat/audio_ai_chat/core/websocket_handler_buck.py new file mode 100644 index 0000000..62be665 --- /dev/null +++ b/audio_ai_chat/audio_ai_chat/core/websocket_handler_buck.py @@ -0,0 +1,449 @@ +from fastapi import WebSocket +from typing import Dict, List, Optional + +from pyexpat.errors import messages + +from audio_ai_chat.config.logger import logger +from audio_ai_chat.core.asr.factory import ASRFactory # 导入ASR工厂 +# from audio_ai_chat.core.llm.factory import LLMFactory # 导入LLM工厂 +# from audio_ai_chat.core.tts.factory import TTSFactory # 导入TTS工厂 +from audio_ai_chat.config.settings import settings +from audio_ai_chat.utils.exceptions import ServiceCallError +from fastapi import WebSocket, WebSocketDisconnect +from audio_ai_chat.codec.ProtocolCodec import ProtocolCodec +from typing import Optional, Dict, Callable, Awaitable, List, Any, Coroutine +from dataclasses import dataclass, field +import sys +import json +import asyncio +import websockets +import uuid +import logging +from audio_ai_chat.core.connection import ConnectionManager,ConnectionContext +from audio_ai_chat.codec.ProtocolCodec import ProtocolCodec, MessageType +from audio_ai_chat.core.asr.asr_manager import ASRManager + + +async def asr_result_callback(result: dict): + if result.get("error"): + print(f"ASR错误:{result['error']}") + else: + print(f"ASR结果:{result['text']}(最终结果:{result['is_final']})") + + +class WebSocketConnectionManager: + """WebSocket连接管理器""" + + def __init__(self): + # 活跃连接列表 + self.active_connections: List[WebSocket] = [] + # 关键映射:client_id -> ConnectionContext(快速获取用户专属上下文) + self.client_context_map: Dict[str, ConnectionContext] = {} + self.connection_manager: Optional[ConnectionManager] = None + # self.asr_client = ASRFactory.get_asr_client() + # self.tts_client = TTSFactory.get_tts_client() + # 用户LLM会话存储 + # self.user_llm_conversations: Dict[str, LLMConversation] = {} + # 全局唤醒事件 + self.consume_wakeup = asyncio.Event() + + async def initialize(self): + """初始化:获取ConnectionManager全局单例(在FastAPI启动时调用)""" + self.connection_manager = await ConnectionManager.get_instance() + logger.info("WebSocketConnectionManager 初始化成功(绑定全局ConnectionManager)") + + async def connect(self, client_id, websocket: WebSocket) -> ConnectionContext: + """ + 建立连接+身份校验(前端主动发送身份信息) + 超时逻辑:5秒内未收到前端身份信息,自动关闭连接 + 返回:校验通过的 ConnectionContext(保证非空) + """ + # 1. 接受连接并加入活跃列表 + await websocket.accept() + self.active_connections.append(websocket) + logger.info( + f"连接 {client_id} 已接受,等待前端发送身份信息(5秒超时)...,当前连接数: {len(self.active_connections)}") + # 2. 超时控制:5秒内未收到身份信息 -> 关闭连接 + try: + ping_packet = await asyncio.wait_for( + websocket.receive_bytes(), + timeout=5.0 + ) + except asyncio.TimeoutError: + error_msg = f"连接 {client_id} 身份校验超时(5秒未收到消息)" + logger.warning(error_msg) + # 发送超时错误响应(二进制格式) + error_packet = ProtocolCodec.pack( + MessageType.ERROR, + {"code": 1008, "message": "身份校验超时,请重试"} + ) + await websocket.send_bytes(error_packet) + raise TimeoutError(error_msg) # 抛出异常,进入后续清理逻辑 + + # 3. 解包并验证包类型 + print('ping_packet', ping_packet) + msg_type, _, identity_data = ProtocolCodec.unpack(ping_packet) + + if msg_type != MessageType.IDENTITY: + error_msg = f"连接 {client_id} 首个包类型错误(期望{MessageType.IDENTITY.value},实际{msg_type.value})" + logger.error(error_msg) + # 发送类型错误响应 + error_packet = ProtocolCodec.pack( + MessageType.ERROR, + {"code": 4001, "message": "非法请求:首个包必须是身份校验包"} + ) + await websocket.send_bytes(error_packet) + raise ValueError(error_msg) + + # 4. 身份信息并校验 + # todo + # 提取核心字段(必选字段校验) + user_id = identity_data.get("user_id") + token = identity_data.get("token") + name = identity_data.get("name") or f"用户{user_id}" # 提供默认名称 + + if not all([user_id, token]): + error_msg = f"连接 {client_id} 身份信息不完整(缺少user_id或token)" + logger.error(error_msg) + error_packet = ProtocolCodec.pack( + MessageType.ERROR, + {"code": 4003, "message": "身份信息不完整:必须包含user_id和token"} + ) + await websocket.send_bytes(error_packet) + raise ValueError(error_msg) + + # TODO: 实际身份校验逻辑(根据你的业务扩展) + + # 5. 校验通过:更新上下文并响应前端 + context = await self.connection_manager.create_or_reconnect_context( + new_client_id=client_id, + user_id=user_id + ) + context = ConnectionContext(client_id=client_id) # 提前创建上下文(保证最终返回非空) + context.set_user_info(token, user_id, name) + self.client_context_map[client_id] = context # 加入上下文映射 + + # 发送成功响应 + success_packet = ProtocolCodec.pack( + MessageType.IDENTITY, + { + "code": 200, + "message": "身份校验成功,连接已就绪", + "data": {"client_id": client_id, "user_id": user_id, "name": name} + } + ) + await websocket.send_bytes(success_packet) + logger.info(f"用户 {user_id}({name})身份校验通过,连接就绪(client_id: {client_id})") + + return context + + # 初始化LLM会话 + # self._init_llm_conversation(user_id) + + # return conn_id, user_id, conn_id # conn_id 同时作为 tts_session_id + + # def _init_llm_conversation(self, user_id: str): + # """初始化用户LLM会话""" + # if user_id not in self.user_llm_conversations: + # self.user_llm_conversations[user_id] = LLMConversation( + # user_id=user_id, + # scene_description="语音识别对话场景" + # ) + + def disconnect(self, websocket: WebSocket, conn_id: str): + """断开连接并清理资源""" + if websocket in self.active_connections: + self.active_connections.remove(websocket) + logger.info(f"连接 {conn_id} 已断开,当前连接数: {len(self.active_connections)}") + + # async def setup_tts_manager(self, result_queue: asyncio.Queue) -> TTSManager: + # """初始化TTS管理器""" + # + # def handle_tts_result(req_id: str, result: Dict[str, Any]): + # """TTS结果回调处理""" + # try: + # status = result.get("status") + # if status == "completed": + # audio_data = result.get("audio_data") + # if audio_data is not None and len(audio_data) > 0: + # # 转换为PCM格式 + # pcm_data = (audio_data.astype(np.float32) * 32767).astype(np.int16) + # pcm_bytes = pcm_data.tobytes() + # result_queue.put_nowait(pcm_bytes) + # except Exception as e: + # logger.error(f"TTS结果处理失败: {str(e)}") + # + # self.tts_client = TTSFactory.get_tts_client() + # tts_manager = TTSManager() + # tts_manager.set_result_callback(handle_tts_result) + # tts_manager.set_playback_enabled(False) + # await tts_manager.initialize() + # return tts_manager + + # async def asr_result_callback(self, result: dict, websocket: WebSocket, + # user_id: str, result_queue: asyncio.Queue): + # """ASR结果回调处理""" + # try: + # logger.info(f"ASR识别结果: {result}") + # final_asr_text = result.get("text", "").strip() + # + # # 转发ASR结果到前端队列 + # if final_asr_text: + # print(f"插入ASR结果时队列大小: {result_queue.qsize()}") + # self.consume_wakeup.set() # 唤醒消费协程 + # + # # 异步调用大模型 + # llm_conversation = self.user_llm_conversations.get(user_id) + # if llm_conversation: + # asyncio.create_task( + # self.call_llm_and_send( + # query=final_asr_text, + # conversation=llm_conversation, + # websocket=websocket + # ) + # ) + # except Exception as e: + # logger.error(f"ASR回调执行失败: {str(e)}") + + # async def llm_stream_callback(self, chunk: str, tts_manager: TTSManager): + # """大模型流式回调处理""" + # if not chunk: + # return + # try: + # # 提交TTS合成请求 + # await tts_manager.synthesize(chunk) + # await asyncio.sleep(0) # 让出调度权 + # except Exception as e: + # logger.error(f"LLM流式回调处理失败: {str(e)}") + + # async def call_llm_and_send(self, query: str, conversation: LLMConversation, websocket: WebSocket): + # """调用大模型并处理结果""" + # logger.info(f"调用大模型 - 用户({conversation.user_id}): {query}") + # try: + # conv_id, full_reply = await llm_client.send_message( + # query=query, + # conversation=conversation, + # stream_callback=self.llm_stream_callback, + # response_mode="streaming" + # ) + # logger.info(f"大模型回复完成 - 会话ID: {conv_id}, 完整回复: {full_reply}") + # except Exception as e: + # logger.error(f"大模型调用失败: {str(e)}") + # if not websocket.client_state.disconnected: + # await websocket.send_json({ + # "type": "llm_error", + # "data": {"error": str(e)} + # }) + + async def recv_frontend_data(self, websocket: WebSocket, asr_conn): + """接收前端音频数据并推送到ASR""" + while not asr_conn.stop_event.is_set(): + try: + raw_bytes = await websocket.receive_bytes() + + # success = await push_audio_data(asr_conn, raw_bytes) + # if not success: + # logger.warning("音频数据插入ASR失败(队列满/连接失效)") + except WebSocketDisconnect: + logger.info("前端主动断开连接") + asr_conn.stop_event.set() + break + except Exception as e: + logger.error(f"接收前端数据失败: {str(e)}") + # asr_conn.stop_event.set() + # if not websocket.client_state.disconnected: + # await websocket.send_json({"error": f"接收数据失败: {str(e)}"}) + # break + + async def send_results(self, websocket: WebSocket, result_queue: asyncio.Queue, asr_conn): + """从结果队列发送数据到前端""" + while True: + try: + # 等待队列数据或超时 + result = await asyncio.wait_for(result_queue.get(), timeout=0.05) + # if not websocket.client_state.disconnected: + await websocket.send_bytes(result) + except asyncio.TimeoutError: + if asr_conn.stop_event.is_set(): + break + continue + except Exception as e: + logger.error(f"发送结果到前端失败: {str(e)}") + asr_conn.stop_event.set() + break + + async def handle_connection(self, websocket: WebSocket): + """处理单个WebSocket连接的完整生命周期""" + new_client_id = str(id(websocket)) + logger.info(f"新WebSocket连接:client_id={new_client_id}") + context = None + try: + context = await self.connect(new_client_id, websocket) + asr_client = ASRManager.get_instance() + asr_conn = await asr_client.get_connection() + if not asr_conn: + print("获取ASR连接失败") + raise + communication_task = asyncio.create_task( + asr_client.start_communication(asr_conn, asr_result_callback) + ) + # 接收前端数据 + async def recv_frontend_data(): + """接收前端音频/控制指令""" + # while not asr_conn.stop_event.is_set(): + while True: + try: + if not context.message_queue.empty(): + await asyncio.sleep(0) # 立即让权 + continue + raw_bytes = await websocket.receive_bytes() + msg_type, sequence, unpack_bytes = ProtocolCodec.unpack(raw_bytes) + if msg_type == MessageType.AUDIO_DATA: + success = await asr_client.push_audio(asr_conn, unpack_bytes) + if not success: + print(f"音频数据插入失败(队列满/连接失效)") + else: + print(f"其他类型数据", msg_type) + except WebSocketDisconnect: + # logger.info(f"前端 {conn_id} 主动断开连接") + # asr_conn.stop_event.set() + break + except Exception as e: + logger.error(f"接收前端数据失败: {str(e)}") + # asr_conn.stop_event.set() + # await websocket.send_json({"error": f"接收数据失败: {str(e)}"}) + break + + # 发送 ASR 结果 + async def send_asr_result(): + """从结果队列发送 ASR 结果到前端(二进制格式)""" + while True: + try: + result = await asyncio.wait_for(context.message_queue.get(), timeout=0.05) + + await websocket.send_bytes(result) + except asyncio.TimeoutError: + continue + except Exception as e: + logger.error(f"发送 ASR 结果失败: {str(e)}") + break + + task_send = asyncio.create_task(send_asr_result()) + task_recv = asyncio.create_task(recv_frontend_data()) + try: + # 等待两个任务,只要有一个完成就返回(比如前端断开/发送出错) + done, pending = await asyncio.wait( + [task_recv, task_send], + return_when=asyncio.FIRST_COMPLETED, + timeout=None # 无限等待,直到有任务完成 + ) + finally: + # 确保协程正确退出 + # 等待剩余任务完成 + for task in pending: + task.cancel() + await asyncio.gather(task_recv, task_send, return_exceptions=True) + + pass + except Exception as e: + logger.error(f"WebSocket连接处理异常: {str(e)}") + + if websocket in self.active_connections: + self.active_connections.remove(websocket) + # 移除上下文映射(如果已添加) + if context is not None and context.client_id in self.client_context_map: + del self.client_context_map[new_client_id] + finally: + # 6. 统一资源清理(无论成功/失败,都执行) + # 关闭WebSocket连接 + try: + if hasattr(websocket, "state") and websocket.state == "CONNECTED": + await websocket.close(code=1008, reason="连接终止") + except Exception as close_e: + logger.warning(f"关闭连接失败 (client_id: {new_client_id}): {str(close_e)}") + + # 移除活跃连接 + if websocket in self.active_connections: + self.active_connections.remove(websocket) + # 移除上下文映射 + if new_client_id in self.client_context_map: + del self.client_context_map[new_client_id] + + if context: + pass + logger.info(f"连接资源清理完成 (client_id: {new_client_id}),当前连接数: {len(self.active_connections)}") + # 1. 建立连接 + + # 2. 初始化TTS + # tts_manager = await self.setup_tts_manager(result_queue) + + # 3. 获取ASR连接 + # asr_conn = await get_idle_asr_connection() + # if not asr_conn: + # await websocket.send_json({"error": "ASR服务暂时不可用", "text": ""}) + # return + + # 4. 启动ASR通信协程 + # asr_callback = lambda res: self.asr_result_callback(res, websocket, user_id, result_queue) + # asr_task = asyncio.create_task(handle_asr_communication(asr_conn, asr_callback)) + # + # # 5. 启动数据接收和发送协程 + # task_recv = asyncio.create_task(self.recv_frontend_data(websocket, asr_conn)) + # task_send = asyncio.create_task(self.send_results(websocket, result_queue, asr_conn)) + # + # # 6. 等待任一任务完成 + # done, pending = await asyncio.wait( + # [task_recv, task_send], + # return_when=asyncio.FIRST_COMPLETED + # ) + + # except Exception as e: + # logger.error(f"WebSocket连接处理异常: {str(e)}") + # if asr_conn: + # asr_conn.stop_event.set() + # if not websocket.client_state.disconnected: + # await websocket.send_json({"error": str(e)}) + + # logger.error(f"连接 {client_id} 建立失败: {type(e).__name__}: {e}") + # try: + # # 确保连接已关闭(处理未正常关闭的情况) + # if websocket.client_state == "CONNECTED": # 根据实际WebSocket类型调整状态判断 + # await websocket.close(code=1008, reason=str(e)) + # except: + # pass + + # 移除活跃连接(避免内存泄漏) + # if websocket in self.active_connections: + # self.active_connections.remove(websocket) + # # 移除上下文映射(如果已添加) + # if context is not None and context.client_id in self.client_context_map: + # del self.client_context_map[client_id] + # finally: + # pass + # 7. 资源清理 + # logger.info(f"开始清理连接 {conn_id} 的资源") + # # 停止ASR + # if asr_conn: + # asr_conn.stop_event.set() + # + # # 取消任务 + # if asr_task and not asr_task.done(): + # asr_task.cancel() + # try: + # await asr_task + # except asyncio.CancelledError: + # pass + # + # # 清理TTS + # if tts_manager: + # await tts_manager.cleanup() # 假设TTSManager有cleanup方法,无则忽略 + # + # # 断开连接 + # if websocket: + # self.disconnect(websocket, conn_id) + # try: + # await websocket.close() + # except Exception: + # pass + # + # logger.info(f"连接 {conn_id} 资源清理完成") diff --git a/audio_ai_chat/audio_ai_chat/main.py b/audio_ai_chat/audio_ai_chat/main.py index eb9b63f..214a0f3 100644 --- a/audio_ai_chat/audio_ai_chat/main.py +++ b/audio_ai_chat/audio_ai_chat/main.py @@ -8,18 +8,35 @@ from audio_ai_chat.codec.ProtocolCodec import ProtocolCodec # 已有加密类 from audio_ai_chat.utils.exceptions import CodecError, ServiceCallError from contextlib import asynccontextmanager from frontend_ws import frontend_websocket_handler +from audio_ai_chat.core.asr.asr_manager import ASRManager + # FastAPI 启动时初始化 ASR 连接池 @asynccontextmanager async def lifespan(app: FastAPI): # 启动时执行(原 startup 逻辑) print(' FastAPI 启动时初始化 ASR 连接池') - # await init_asr_pool() yield # 应用运行中 # 关闭时执行(可选,比如清理连接池) print("应用关闭,开始清理 ASR 连接池...") - # await close_asr_pool() +# 生命周期函数 +@asynccontextmanager +async def lifespan(app: FastAPI): + print("=== 开始初始化 ASR 服务 ===") + init_success, init_msg = await ASRManager.initialize() + print(f"ASR 初始化结果:{init_msg}") + # if not init_success: + # 连接池为空/初始化失败,终止服务启动 + # raise ServiceInitError(f"服务启动失败:{init_msg}") + # 2. 初始化WebSocketConnectionManager(绑定全局ConnectionManager) + await ws_manager.initialize() + print("=== ASR 服务初始化完成 ===") + yield # 应用运行中 + # 关闭时清理 + print("=== 开始关闭 ASR 服务 ===") + await ASRManager.close() + print("=== ASR 服务关闭完成 ===") app = FastAPI( title="语音AI对话系统", diff --git a/audio_ai_chat/audio_ai_chat/utils/__pycache__/__init__.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/utils/__pycache__/__init__.cpython-310.pyc index 714d647..ccec488 100644 Binary files a/audio_ai_chat/audio_ai_chat/utils/__pycache__/__init__.cpython-310.pyc and b/audio_ai_chat/audio_ai_chat/utils/__pycache__/__init__.cpython-310.pyc differ diff --git a/audio_ai_chat/audio_ai_chat/utils/__pycache__/exceptions.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/utils/__pycache__/exceptions.cpython-310.pyc index 5b58a89..6a7230c 100644 Binary files a/audio_ai_chat/audio_ai_chat/utils/__pycache__/exceptions.cpython-310.pyc and b/audio_ai_chat/audio_ai_chat/utils/__pycache__/exceptions.cpython-310.pyc differ diff --git a/audio_ai_chat/logs/app.log b/audio_ai_chat/logs/app.log index bb3d9e3..3e68924 100644 --- a/audio_ai_chat/logs/app.log +++ b/audio_ai_chat/logs/app.log @@ -872,3 +872,1001 @@ 2025-12-03 00:27:31.740 | ERROR | audio_ai_chat.core.websocket_handler:recv_frontend_data:288 - 接收前端数据失败: name 'push_audio_data' is not defined 2025-12-03 00:27:31.741 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:350 - 连接资源清理完成 (client_id: 2895502796560),当前连接数: 0 2025-12-03 00:27:31.754 | INFO | audio_ai_chat.core.connection:close:88 - 连接上下文已关闭:client_id=2895502796560,user_id=1001 +2025-12-03 08:34:21.546 | INFO | audio_ai_chat.core.websocket_handler:connect:52 - 连接 2065042265648 已接受,等待前端发送身份信息(5秒超时)...,当前连接数: 1 +2025-12-03 08:34:21.549 | INFO | audio_ai_chat.core.websocket_handler:connect:120 - 用户 1001(测试用户)身份校验通过,连接就绪(client_id: 2065042265648) +2025-12-03 08:34:22.622 | ERROR | audio_ai_chat.core.websocket_handler:recv_frontend_data:288 - 接收前端数据失败: name 'push_audio_data' is not defined +2025-12-03 08:34:22.623 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:350 - 连接资源清理完成 (client_id: 2065042265648),当前连接数: 0 +2025-12-03 08:34:22.639 | INFO | audio_ai_chat.core.connection:close:88 - 连接上下文已关闭:client_id=2065042265648,user_id=1001 +2025-12-03 09:38:36.138 | INFO | audio_ai_chat.core.websocket_handler:connect:60 - 连接 2759704914736 已接受,等待前端发送身份信息(5秒超时)...,当前连接数: 1 +2025-12-03 09:38:36.145 | INFO | audio_ai_chat.core.websocket_handler:connect:128 - 用户 1001(测试用户)身份校验通过,连接就绪(client_id: 2759704914736) +2025-12-03 09:39:35.695 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:366 - 连接资源清理完成 (client_id: 2759704914736),当前连接数: 0 +2025-12-03 09:39:35.696 | INFO | audio_ai_chat.core.connection:close:88 - 连接上下文已关闭:client_id=2759704914736,user_id=1001 +2025-12-03 09:41:36.547 | INFO | audio_ai_chat.core.websocket_handler:connect:60 - 连接 2420289232640 已接受,等待前端发送身份信息(5秒超时)...,当前连接数: 1 +2025-12-03 09:41:36.550 | INFO | audio_ai_chat.core.websocket_handler:connect:128 - 用户 1001(测试用户)身份校验通过,连接就绪(client_id: 2420289232640) +2025-12-03 09:41:43.696 | INFO | audio_ai_chat.core.websocket_handler:connect:60 - 连接 2420289652960 已接受,等待前端发送身份信息(5秒超时)...,当前连接数: 2 +2025-12-03 09:41:43.703 | INFO | audio_ai_chat.core.websocket_handler:connect:128 - 用户 1001(测试用户)身份校验通过,连接就绪(client_id: 2420289652960) +2025-12-03 09:42:04.990 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:366 - 连接资源清理完成 (client_id: 2420289232640),当前连接数: 1 +2025-12-03 09:42:04.990 | INFO | audio_ai_chat.core.connection:close:88 - 连接上下文已关闭:client_id=2420289232640,user_id=1001 +2025-12-03 09:42:04.990 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:366 - 连接资源清理完成 (client_id: 2420289652960),当前连接数: 0 +2025-12-03 09:42:04.991 | INFO | audio_ai_chat.core.connection:close:88 - 连接上下文已关闭:client_id=2420289652960,user_id=1001 +2025-12-03 10:45:19.517 | INFO | audio_ai_chat.core.connection:get_instance:240 - ConnectionManager 全局单例初始化成功 +2025-12-03 10:45:19.517 | INFO | audio_ai_chat.core.websocket_handler:initialize:53 - WebSocketConnectionManager 初始化成功(绑定全局ConnectionManager) +2025-12-03 11:06:27.782 | INFO | audio_ai_chat.core.connection:get_instance:248 - ConnectionManager 全局单例初始化成功 +2025-12-03 11:06:27.782 | INFO | audio_ai_chat.core.websocket_handler:initialize:53 - WebSocketConnectionManager 初始化成功(绑定全局ConnectionManager) +2025-12-03 11:06:29.463 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:278 - 新WebSocket连接:client_id=1990428894464 +2025-12-03 11:06:29.464 | INFO | audio_ai_chat.core.websocket_handler:connect:64 - 连接 1990428894464 已接受,等待前端发送身份信息(5秒超时)...,当前连接数: 1 +2025-12-03 11:06:29.468 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:323 - 创建新上下文:client_id=1990428894464,user_id=1001 +2025-12-03 11:06:29.468 | INFO | audio_ai_chat.core.websocket_handler:connect:136 - 用户 1001(测试用户)身份校验通过,连接就绪(client_id: 1990428894464) +2025-12-03 11:09:40.626 | INFO | audio_ai_chat.core.connection:get_instance:248 - ConnectionManager 全局单例初始化成功 +2025-12-03 11:09:40.626 | INFO | audio_ai_chat.core.websocket_handler:initialize:53 - WebSocketConnectionManager 初始化成功(绑定全局ConnectionManager) +2025-12-03 11:09:43.339 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:278 - 新WebSocket连接:client_id=2298590522640 +2025-12-03 11:09:43.339 | INFO | audio_ai_chat.core.websocket_handler:connect:64 - 连接 2298590522640 已接受,等待前端发送身份信息(5秒超时)...,当前连接数: 1 +2025-12-03 11:09:43.349 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:323 - 创建新上下文:client_id=2298590522640,user_id=1001 +2025-12-03 11:09:43.349 | INFO | audio_ai_chat.core.websocket_handler:connect:136 - 用户 1001(测试用户)身份校验通过,连接就绪(client_id: 2298590522640) +2025-12-03 11:10:08.001 | INFO | audio_ai_chat.core.connection:get_instance:248 - ConnectionManager 全局单例初始化成功 +2025-12-03 11:10:08.002 | INFO | audio_ai_chat.core.websocket_handler:initialize:53 - WebSocketConnectionManager 初始化成功(绑定全局ConnectionManager) +2025-12-03 11:10:13.020 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:278 - 新WebSocket连接:client_id=1545668847040 +2025-12-03 11:10:13.021 | INFO | audio_ai_chat.core.websocket_handler:connect:64 - 连接 1545668847040 已接受,等待前端发送身份信息(5秒超时)...,当前连接数: 1 +2025-12-03 11:10:13.024 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:323 - 创建新上下文:client_id=1545668847040,user_id=1001 +2025-12-03 11:10:13.025 | INFO | audio_ai_chat.core.websocket_handler:connect:136 - 用户 1001(测试用户)身份校验通过,连接就绪(client_id: 1545668847040) +2025-12-03 14:17:42.638 | INFO | audio_ai_chat.core.connection:get_instance:248 - ConnectionManager 全局单例初始化成功 +2025-12-03 14:17:42.638 | INFO | audio_ai_chat.core.websocket_handler:initialize:53 - WebSocketConnectionManager 初始化成功(绑定全局ConnectionManager) +2025-12-03 14:17:44.606 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:278 - 新WebSocket连接:client_id=2126429793600 +2025-12-03 14:17:44.606 | INFO | audio_ai_chat.core.websocket_handler:connect:64 - 连接 2126429793600 已接受,等待前端发送身份信息(5秒超时)...,当前连接数: 1 +2025-12-03 14:17:44.610 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:323 - 创建新上下文:client_id=2126429793600,user_id=1001 +2025-12-03 14:17:44.611 | INFO | audio_ai_chat.core.websocket_handler:connect:136 - 用户 1001(测试用户)身份校验通过,连接就绪(client_id: 2126429793600) +2025-12-03 14:18:05.636 | ERROR | audio_ai_chat.core.websocket_handler:handle_connection:349 - WebSocket连接处理异常: No active exception to reraise +2025-12-03 14:32:52.554 | INFO | audio_ai_chat.core.connection:get_instance:248 - ConnectionManager 全局单例初始化成功 +2025-12-03 14:32:52.555 | INFO | audio_ai_chat.core.websocket_handler:initialize:53 - WebSocketConnectionManager 初始化成功(绑定全局ConnectionManager) +2025-12-03 14:36:45.558 | INFO | audio_ai_chat.core.connection:get_instance:248 - ConnectionManager 全局单例初始化成功 +2025-12-03 14:36:45.558 | INFO | audio_ai_chat.core.websocket_handler:initialize:53 - WebSocketConnectionManager 初始化成功(绑定全局ConnectionManager) +2025-12-03 14:37:13.651 | INFO | audio_ai_chat.core.connection:get_instance:248 - ConnectionManager 全局单例初始化成功 +2025-12-03 14:37:13.652 | INFO | audio_ai_chat.core.websocket_handler:initialize:53 - WebSocketConnectionManager 初始化成功(绑定全局ConnectionManager) +2025-12-03 14:42:22.498 | INFO | audio_ai_chat.core.connection:get_instance:248 - ConnectionManager 全局单例初始化成功 +2025-12-03 14:42:22.498 | INFO | audio_ai_chat.core.websocket_handler:initialize:53 - WebSocketConnectionManager 初始化成功(绑定全局ConnectionManager) +2025-12-03 14:42:29.537 | INFO | audio_ai_chat.core.connection:get_instance:248 - ConnectionManager 全局单例初始化成功 +2025-12-03 14:42:29.537 | INFO | audio_ai_chat.core.websocket_handler:initialize:53 - WebSocketConnectionManager 初始化成功(绑定全局ConnectionManager) +2025-12-03 14:42:39.444 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:278 - 新WebSocket连接:client_id=2696752407712 +2025-12-03 14:42:39.445 | INFO | audio_ai_chat.core.websocket_handler:connect:64 - 连接 2696752407712 已接受,等待前端发送身份信息(5秒超时)...,当前连接数: 1 +2025-12-03 14:42:39.451 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:323 - 创建新上下文:client_id=2696752407712,user_id=1001 +2025-12-03 14:42:39.451 | INFO | audio_ai_chat.core.websocket_handler:connect:136 - 用户 1001(测试用户)身份校验通过,连接就绪(client_id: 2696752407712) +2025-12-03 14:43:43.058 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:374 - 连接资源清理完成 (client_id: 2696752407712),当前连接数: 0 +2025-12-03 14:43:43.058 | INFO | audio_ai_chat.core.connection:close:209 - 连接上下文已关闭:client_id=2696752407712,user_id=1001,对话历史条数=0 +2025-12-03 14:50:34.881 | INFO | audio_ai_chat.core.connection:get_instance:248 - ConnectionManager 全局单例初始化成功 +2025-12-03 14:50:34.881 | INFO | audio_ai_chat.core.websocket_handler:initialize:33 - WebSocketConnectionManager 初始化成功 +2025-12-03 14:50:36.212 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:141 - 新WebSocket连接:client_id=2079417607856 +2025-12-03 14:50:36.213 | INFO | audio_ai_chat.core.websocket_handler:connect:39 - 连接 2079417607856 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 14:50:36.215 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:323 - 创建新上下文:client_id=2079417607856,user_id=1001 +2025-12-03 14:50:36.215 | INFO | audio_ai_chat.core.websocket_handler:connect:96 - 用户 1001(测试用户)身份校验通过(client_id: 2079417607856) +2025-12-03 14:50:40.636 | ERROR | audio_ai_chat.core.websocket_handler:asr_result_callback:134 - ASR回调处理失败(client_id: 2079417607856):ASR_RESULT +2025-12-03 14:50:40.636 | ERROR | audio_ai_chat.core.websocket_handler:asr_result_callback:109 - ASR错误(client_id: 2079417607856):接收结果失败:object NoneType can't be used in 'await' expression +2025-12-03 14:57:51.323 | INFO | audio_ai_chat.core.connection:get_instance:248 - ConnectionManager 全局单例初始化成功 +2025-12-03 14:57:51.324 | INFO | audio_ai_chat.core.websocket_handler:initialize:33 - WebSocketConnectionManager 初始化成功 +2025-12-03 14:57:53.114 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:141 - 新WebSocket连接:client_id=2330088859216 +2025-12-03 14:57:53.115 | INFO | audio_ai_chat.core.websocket_handler:connect:39 - 连接 2330088859216 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 14:57:53.117 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:323 - 创建新上下文:client_id=2330088859216,user_id=1001 +2025-12-03 14:57:53.118 | INFO | audio_ai_chat.core.websocket_handler:connect:96 - 用户 1001(测试用户)身份校验通过(client_id: 2330088859216) +2025-12-03 14:57:56.758 | ERROR | audio_ai_chat.core.websocket_handler:asr_result_callback:134 - ASR回调处理失败(client_id: 2330088859216):ASR_RESULT +2025-12-03 14:57:56.758 | ERROR | audio_ai_chat.core.websocket_handler:asr_result_callback:109 - ASR错误(client_id: 2330088859216):接收结果失败:object NoneType can't be used in 'await' expression +2025-12-03 14:57:56.791 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:254 - 连接 2330088859216 资源清理完成,当前连接数: 0 +2025-12-03 14:58:21.344 | INFO | audio_ai_chat.core.connection:get_instance:248 - ConnectionManager 全局单例初始化成功 +2025-12-03 14:58:21.345 | INFO | audio_ai_chat.core.websocket_handler:initialize:33 - WebSocketConnectionManager 初始化成功 +2025-12-03 14:58:22.628 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:142 - 新WebSocket连接:client_id=1707268335184 +2025-12-03 14:58:22.629 | INFO | audio_ai_chat.core.websocket_handler:connect:39 - 连接 1707268335184 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 14:58:22.632 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:323 - 创建新上下文:client_id=1707268335184,user_id=1001 +2025-12-03 14:58:22.633 | INFO | audio_ai_chat.core.websocket_handler:connect:96 - 用户 1001(测试用户)身份校验通过(client_id: 1707268335184) +2025-12-03 14:58:27.296 | ERROR | audio_ai_chat.core.websocket_handler:asr_result_callback:135 - ASR回调处理失败(client_id: 1707268335184):ASR_RESULT +2025-12-03 14:58:27.297 | ERROR | audio_ai_chat.core.websocket_handler:asr_result_callback:110 - ASR错误(client_id: 1707268335184):接收结果失败:object NoneType can't be used in 'await' expression +2025-12-03 14:58:27.298 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:255 - 连接 1707268335184 资源清理完成,当前连接数: 0 +2025-12-03 15:00:27.352 | INFO | audio_ai_chat.core.connection:get_instance:248 - ConnectionManager 全局单例初始化成功 +2025-12-03 15:00:27.352 | INFO | audio_ai_chat.core.websocket_handler:initialize:33 - WebSocketConnectionManager 初始化成功 +2025-12-03 15:00:28.200 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:142 - 新WebSocket连接:client_id=3127897990736 +2025-12-03 15:00:28.201 | INFO | audio_ai_chat.core.websocket_handler:connect:39 - 连接 3127897990736 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 15:00:28.203 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:323 - 创建新上下文:client_id=3127897990736,user_id=1001 +2025-12-03 15:00:28.204 | INFO | audio_ai_chat.core.websocket_handler:connect:96 - 用户 1001(测试用户)身份校验通过(client_id: 3127897990736) +2025-12-03 15:00:33.403 | ERROR | audio_ai_chat.core.websocket_handler:asr_result_callback:135 - ASR回调处理失败(client_id: 3127897990736):ASR_RESULT +2025-12-03 15:00:36.162 | ERROR | audio_ai_chat.core.websocket_handler:asr_result_callback:135 - ASR回调处理失败(client_id: 3127897990736):ASR_RESULT +2025-12-03 15:00:38.354 | ERROR | audio_ai_chat.core.websocket_handler:asr_result_callback:135 - ASR回调处理失败(client_id: 3127897990736):ASR_RESULT +2025-12-03 15:00:39.704 | ERROR | audio_ai_chat.core.websocket_handler:asr_result_callback:135 - ASR回调处理失败(client_id: 3127897990736):ASR_RESULT +2025-12-03 15:00:43.462 | ERROR | audio_ai_chat.core.websocket_handler:asr_result_callback:135 - ASR回调处理失败(client_id: 3127897990736):ASR_RESULT +2025-12-03 15:00:49.348 | ERROR | audio_ai_chat.core.websocket_handler:asr_result_callback:135 - ASR回调处理失败(client_id: 3127897990736):ASR_RESULT +2025-12-03 15:00:49.997 | ERROR | audio_ai_chat.core.websocket_handler:asr_result_callback:135 - ASR回调处理失败(client_id: 3127897990736):ASR_RESULT +2025-12-03 15:01:08.454 | ERROR | audio_ai_chat.core.websocket_handler:asr_result_callback:135 - ASR回调处理失败(client_id: 3127897990736):ASR_RESULT +2025-12-03 15:01:09.845 | ERROR | audio_ai_chat.core.websocket_handler:asr_result_callback:135 - ASR回调处理失败(client_id: 3127897990736):ASR_RESULT +2025-12-03 15:01:15.831 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:188 - 前端 3127897990736 主动断开连接 +2025-12-03 15:01:15.832 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:255 - 连接 3127897990736 资源清理完成,当前连接数: 0 +2025-12-03 15:16:04.053 | INFO | audio_ai_chat.core.connection:get_instance:248 - ConnectionManager 全局单例初始化成功 +2025-12-03 15:16:04.054 | INFO | audio_ai_chat.core.websocket_handler:initialize:33 - WebSocketConnectionManager 初始化成功 +2025-12-03 16:23:52.367 | INFO | audio_ai_chat.core.connection:get_instance:377 - ConnectionManager 全局单例初始化成功 +2025-12-03 16:23:52.368 | INFO | audio_ai_chat.core.websocket_handler:initialize:33 - WebSocketConnectionManager 初始化成功 +2025-12-03 16:24:02.229 | INFO | audio_ai_chat.core.connection:get_instance:377 - ConnectionManager 全局单例初始化成功 +2025-12-03 16:24:02.230 | INFO | audio_ai_chat.core.websocket_handler:initialize:33 - WebSocketConnectionManager 初始化成功 +2025-12-03 16:24:04.204 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:142 - 新WebSocket连接:client_id=2143778359136 +2025-12-03 16:24:04.205 | INFO | audio_ai_chat.core.websocket_handler:connect:39 - 连接 2143778359136 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 16:24:04.209 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:452 - 创建新上下文:client_id=2143778359136,user_id=1001 +2025-12-03 16:24:04.210 | INFO | audio_ai_chat.core.websocket_handler:connect:96 - 用户 1001(测试用户)身份校验通过(client_id: 2143778359136) +2025-12-03 16:24:04.210 | ERROR | audio_ai_chat.core.websocket_handler:handle_connection:225 - 连接 2143778359136 处理异常:LLM工厂:不支持的客户端版本:None +2025-12-03 16:24:32.189 | INFO | audio_ai_chat.core.connection:get_instance:377 - ConnectionManager 全局单例初始化成功 +2025-12-03 16:24:32.189 | INFO | audio_ai_chat.core.websocket_handler:initialize:33 - WebSocketConnectionManager 初始化成功 +2025-12-03 16:25:37.771 | INFO | audio_ai_chat.core.connection:get_instance:377 - ConnectionManager 全局单例初始化成功 +2025-12-03 16:25:37.771 | INFO | audio_ai_chat.core.websocket_handler:initialize:33 - WebSocketConnectionManager 初始化成功 +2025-12-03 16:25:46.167 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:142 - 新WebSocket连接:client_id=2868733986560 +2025-12-03 16:25:46.167 | INFO | audio_ai_chat.core.websocket_handler:connect:39 - 连接 2868733986560 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 16:25:46.170 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:452 - 创建新上下文:client_id=2868733986560,user_id=1001 +2025-12-03 16:25:46.171 | INFO | audio_ai_chat.core.websocket_handler:connect:96 - 用户 1001(测试用户)身份校验通过(client_id: 2868733986560) +2025-12-03 16:26:14.348 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:189 - 前端 2868733986560 主动断开连接 +2025-12-03 16:26:14.349 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:256 - 连接 2868733986560 资源清理完成,当前连接数: 0 +2025-12-03 16:37:30.842 | INFO | audio_ai_chat.core.connection:get_instance:377 - ConnectionManager 全局单例初始化成功 +2025-12-03 16:37:30.843 | INFO | audio_ai_chat.core.websocket_handler:initialize:33 - WebSocketConnectionManager 初始化成功 +2025-12-03 16:37:33.812 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:157 - 新WebSocket连接:client_id=2767719204800 +2025-12-03 16:37:33.813 | INFO | audio_ai_chat.core.websocket_handler:connect:39 - 连接 2767719204800 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 16:37:33.816 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:452 - 创建新上下文:client_id=2767719204800,user_id=1001 +2025-12-03 16:37:33.817 | INFO | audio_ai_chat.core.websocket_handler:connect:96 - 用户 1001(测试用户)身份校验通过(client_id: 2767719204800) +2025-12-03 16:37:55.974 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:204 - 前端 2767719204800 主动断开连接 +2025-12-03 16:37:55.975 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:271 - 连接 2767719204800 资源清理完成,当前连接数: 0 +2025-12-03 16:46:18.251 | INFO | audio_ai_chat.core.connection:get_instance:377 - ConnectionManager 全局单例初始化成功 +2025-12-03 16:46:18.252 | INFO | audio_ai_chat.core.websocket_handler:initialize:33 - WebSocketConnectionManager 初始化成功 +2025-12-03 16:46:30.937 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:157 - 新WebSocket连接:client_id=1390007977824 +2025-12-03 16:46:30.938 | INFO | audio_ai_chat.core.websocket_handler:connect:39 - 连接 1390007977824 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 16:46:30.940 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:452 - 创建新上下文:client_id=1390007977824,user_id=1001 +2025-12-03 16:46:30.941 | INFO | audio_ai_chat.core.websocket_handler:connect:96 - 用户 1001(测试用户)身份校验通过(client_id: 1390007977824) +2025-12-03 18:24:53.689 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:204 - 前端 1390007977824 主动断开连接 +2025-12-03 18:24:53.694 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:271 - 连接 1390007977824 资源清理完成,当前连接数: 0 +2025-12-03 18:26:05.570 | INFO | audio_ai_chat.core.connection:get_instance:377 - ConnectionManager 全局单例初始化成功 +2025-12-03 18:26:05.571 | INFO | audio_ai_chat.core.websocket_handler:initialize:33 - WebSocketConnectionManager 初始化成功 +2025-12-03 18:26:08.001 | INFO | audio_ai_chat.core.connection:get_instance:377 - ConnectionManager 全局单例初始化成功 +2025-12-03 18:26:08.001 | INFO | audio_ai_chat.core.websocket_handler:initialize:33 - WebSocketConnectionManager 初始化成功 +2025-12-03 18:26:09.704 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:171 - 新WebSocket连接:client_id=2312300151856 +2025-12-03 18:26:09.704 | INFO | audio_ai_chat.core.websocket_handler:connect:39 - 连接 2312300151856 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 18:26:09.707 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:452 - 创建新上下文:client_id=2312300151856,user_id=1001 +2025-12-03 18:26:09.708 | INFO | audio_ai_chat.core.websocket_handler:connect:96 - 用户 1001(测试用户)身份校验通过(client_id: 2312300151856) +2025-12-03 18:26:11.824 | ERROR | audio_ai_chat.core.websocket_handler:asr_result_callback:136 - ASR回调处理失败(client_id: 2312300151856):name 'call_llm_and_send' is not defined +2025-12-03 18:26:13.594 | ERROR | audio_ai_chat.core.websocket_handler:asr_result_callback:136 - ASR回调处理失败(client_id: 2312300151856):name 'call_llm_and_send' is not defined +2025-12-03 18:26:19.839 | ERROR | audio_ai_chat.core.websocket_handler:asr_result_callback:136 - ASR回调处理失败(client_id: 2312300151856):name 'call_llm_and_send' is not defined +2025-12-03 18:26:23.701 | ERROR | audio_ai_chat.core.websocket_handler:asr_result_callback:136 - ASR回调处理失败(client_id: 2312300151856):name 'call_llm_and_send' is not defined +2025-12-03 18:26:26.864 | ERROR | audio_ai_chat.core.websocket_handler:asr_result_callback:136 - ASR回调处理失败(client_id: 2312300151856):name 'call_llm_and_send' is not defined +2025-12-03 18:26:29.016 | ERROR | audio_ai_chat.core.websocket_handler:asr_result_callback:136 - ASR回调处理失败(client_id: 2312300151856):name 'call_llm_and_send' is not defined +2025-12-03 18:26:31.871 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:222 - 前端 2312300151856 主动断开连接 +2025-12-03 18:26:31.872 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:289 - 连接 2312300151856 资源清理完成,当前连接数: 0 +2025-12-03 18:26:33.840 | INFO | audio_ai_chat.core.connection:get_instance:377 - ConnectionManager 全局单例初始化成功 +2025-12-03 18:26:33.840 | INFO | audio_ai_chat.core.websocket_handler:initialize:33 - WebSocketConnectionManager 初始化成功 +2025-12-03 18:26:34.838 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:171 - 新WebSocket连接:client_id=2021290099376 +2025-12-03 18:26:34.838 | INFO | audio_ai_chat.core.websocket_handler:connect:39 - 连接 2021290099376 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 18:26:34.846 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:452 - 创建新上下文:client_id=2021290099376,user_id=1001 +2025-12-03 18:26:34.846 | INFO | audio_ai_chat.core.websocket_handler:connect:96 - 用户 1001(测试用户)身份校验通过(client_id: 2021290099376) +2025-12-03 18:26:41.966 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:152 - 调用大模型 - 用户(): 你好你好,我以前告诉过我们怎么做,要做好的,对 +2025-12-03 18:26:43.803 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:161 - 大模型回复完成 - 会话ID: 04988e35-af6f-4588-92b5-234f9d5d9d01, 完整回复: 你好,请问有什么紧急的事情需要帮忙吗? +2025-12-03 18:26:43.938 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:152 - 调用大模型 - 用户(): 你好,你好,怎么卖,你能让他吗 +2025-12-03 18:26:44.974 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:161 - 大模型回复完成 - 会话ID: 04988e35-af6f-4588-92b5-234f9d5d9d01, 完整回复: 我的银行卡丢了,现在急着取钱,怎么办? +2025-12-03 18:26:50.290 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:152 - 调用大模型 - 用户(): ?回来了,终于回来了,明天不练了,明天 +2025-12-03 18:26:51.545 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:161 - 大模型回复完成 - 会话ID: 04988e35-af6f-4588-92b5-234f9d5d9d01, 完整回复: 我的银行卡丢了,但我现在急需取钱,这可怎么办啊? +2025-12-03 18:26:51.949 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:152 - 调用大模型 - 用户(): 拥有什么好这个 +2025-12-03 18:26:53.032 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:161 - 大模型回复完成 - 会话ID: 04988e35-af6f-4588-92b5-234f9d5d9d01, 完整回复: 我姓李,银行卡丢了,能先取点钱吗? +2025-12-03 18:26:55.240 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:152 - 调用大模型 - 用户(): 在东北方 +2025-12-03 18:26:56.208 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:161 - 大模型回复完成 - 会话ID: 04988e35-af6f-4588-92b5-234f9d5d9d01, 完整回复: 快帮我解决一下,真的急! +2025-12-03 18:26:59.211 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:152 - 调用大模型 - 用户(): ,但是我要目前 +2025-12-03 18:26:59.685 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:222 - 前端 2021290099376 主动断开连接 +2025-12-03 18:26:59.686 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:289 - 连接 2021290099376 资源清理完成,当前连接数: 0 +2025-12-03 19:10:11.036 | INFO | audio_ai_chat.core.connection:get_instance:377 - ConnectionManager 全局单例初始化成功 +2025-12-03 19:10:11.037 | INFO | audio_ai_chat.core.websocket_handler:initialize:33 - WebSocketConnectionManager 初始化成功 +2025-12-03 19:10:14.900 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:172 - 新WebSocket连接:client_id=2176613583168 +2025-12-03 19:10:14.900 | INFO | audio_ai_chat.core.websocket_handler:connect:39 - 连接 2176613583168 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 19:10:14.903 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:452 - 创建新上下文:client_id=2176613583168,user_id=1001 +2025-12-03 19:10:14.903 | INFO | audio_ai_chat.core.websocket_handler:connect:96 - 用户 1001(测试用户)身份校验通过(client_id: 2176613583168) +2025-12-03 19:10:29.512 | INFO | audio_ai_chat.core.connection:get_instance:377 - ConnectionManager 全局单例初始化成功 +2025-12-03 19:10:29.512 | INFO | audio_ai_chat.core.websocket_handler:initialize:33 - WebSocketConnectionManager 初始化成功 +2025-12-03 19:10:32.143 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:172 - 新WebSocket连接:client_id=2123932975888 +2025-12-03 19:10:32.143 | INFO | audio_ai_chat.core.websocket_handler:connect:39 - 连接 2123932975888 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 19:10:32.151 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:452 - 创建新上下文:client_id=2123932975888,user_id=1001 +2025-12-03 19:10:32.152 | INFO | audio_ai_chat.core.websocket_handler:connect:96 - 用户 1001(测试用户)身份校验通过(client_id: 2123932975888) +2025-12-03 19:10:36.557 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:153 - 调用大模型 - 用户(): 呃,1234567 +2025-12-03 19:10:37.956 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:162 - 大模型回复完成 - 会话ID: 36b81a62-b81d-427f-a22c-275305bb798c, 完整回复: 我的银行卡丢了,现在急着取钱,怎么办? +2025-12-03 19:10:40.890 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:153 - 调用大模型 - 用户(): ,他是 +2025-12-03 19:10:42.222 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:162 - 大模型回复完成 - 会话ID: 36b81a62-b81d-427f-a22c-275305bb798c, 完整回复: 我姓李,能快点帮我解决取钱的问题吗? +2025-12-03 19:10:46.065 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:223 - 前端 2123932975888 主动断开连接 +2025-12-03 19:10:46.066 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:290 - 连接 2123932975888 资源清理完成,当前连接数: 0 +2025-12-03 19:16:31.860 | INFO | audio_ai_chat.core.connection:get_instance:377 - ConnectionManager 全局单例初始化成功 +2025-12-03 19:16:31.860 | INFO | audio_ai_chat.core.websocket_handler:initialize:34 - WebSocketConnectionManager 初始化成功 +2025-12-03 19:19:25.075 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 19:19:25.075 | INFO | audio_ai_chat.core.websocket_handler:initialize:34 - WebSocketConnectionManager 初始化成功 +2025-12-03 19:21:04.602 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 19:21:04.602 | INFO | audio_ai_chat.core.websocket_handler:initialize:34 - WebSocketConnectionManager 初始化成功 +2025-12-03 19:21:07.863 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:176 - 新WebSocket连接:client_id=2553028741056 +2025-12-03 19:21:07.864 | INFO | audio_ai_chat.core.websocket_handler:connect:40 - 连接 2553028741056 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 19:21:07.866 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=2553028741056,user_id=1001 +2025-12-03 19:21:07.867 | INFO | audio_ai_chat.core.websocket_handler:connect:97 - 用户 1001(测试用户)身份校验通过(client_id: 2553028741056) +2025-12-03 19:21:09.904 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:157 - 调用大模型 - 用户(): 想要他 +2025-12-03 19:21:09.904 | ERROR | audio_ai_chat.core.websocket_handler:call_llm_and_send:168 - 大模型调用失败: WebSocketConnectionManager.llm_stream_callback() missing 3 required positional arguments: 'chunk', 'conversation_id', and 'is_finished' +2025-12-03 19:21:12.340 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:157 - 调用大模型 - 用户(): ,你好你好 +2025-12-03 19:21:12.341 | ERROR | audio_ai_chat.core.websocket_handler:call_llm_and_send:168 - 大模型调用失败: WebSocketConnectionManager.llm_stream_callback() missing 3 required positional arguments: 'chunk', 'conversation_id', and 'is_finished' +2025-12-03 19:21:14.899 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:157 - 调用大模型 - 用户(): ,如如果呢 +2025-12-03 19:21:14.899 | ERROR | audio_ai_chat.core.websocket_handler:call_llm_and_send:168 - 大模型调用失败: WebSocketConnectionManager.llm_stream_callback() missing 3 required positional arguments: 'chunk', 'conversation_id', and 'is_finished' +2025-12-03 19:21:14.974 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:241 - 前端 2553028741056 主动断开连接 +2025-12-03 19:21:14.975 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:308 - 连接 2553028741056 资源清理完成,当前连接数: 0 +2025-12-03 19:25:02.038 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 19:25:02.038 | INFO | audio_ai_chat.core.websocket_handler:initialize:34 - WebSocketConnectionManager 初始化成功 +2025-12-03 19:25:03.805 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:179 - 新WebSocket连接:client_id=1972152108992 +2025-12-03 19:25:03.806 | INFO | audio_ai_chat.core.websocket_handler:connect:40 - 连接 1972152108992 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 19:25:03.808 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=1972152108992,user_id=1001 +2025-12-03 19:25:03.809 | INFO | audio_ai_chat.core.websocket_handler:connect:97 - 用户 1001(测试用户)身份校验通过(client_id: 1972152108992) +2025-12-03 19:25:06.648 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:157 - 调用大模型 - 用户(): 喂喂喂 +2025-12-03 19:25:06.648 | ERROR | audio_ai_chat.core.websocket_handler:call_llm_and_send:171 - 大模型调用失败: 'LLMConversation' object has no attribute 'send_message' +2025-12-03 19:25:08.000 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:157 - 调用大模型 - 用户(): ,那我 +2025-12-03 19:25:08.000 | ERROR | audio_ai_chat.core.websocket_handler:call_llm_and_send:171 - 大模型调用失败: 'LLMConversation' object has no attribute 'send_message' +2025-12-03 19:25:09.665 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:157 - 调用大模型 - 用户(): 好吗 +2025-12-03 19:25:09.665 | ERROR | audio_ai_chat.core.websocket_handler:call_llm_and_send:171 - 大模型调用失败: 'LLMConversation' object has no attribute 'send_message' +2025-12-03 19:25:10.571 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:244 - 前端 1972152108992 主动断开连接 +2025-12-03 19:25:10.572 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:311 - 连接 1972152108992 资源清理完成,当前连接数: 0 +2025-12-03 19:27:27.796 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 19:27:27.796 | INFO | audio_ai_chat.core.websocket_handler:initialize:34 - WebSocketConnectionManager 初始化成功 +2025-12-03 19:27:28.626 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:179 - 新WebSocket连接:client_id=2114206697456 +2025-12-03 19:27:28.627 | INFO | audio_ai_chat.core.websocket_handler:connect:40 - 连接 2114206697456 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 19:27:28.634 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=2114206697456,user_id=1001 +2025-12-03 19:27:28.635 | INFO | audio_ai_chat.core.websocket_handler:connect:97 - 用户 1001(测试用户)身份校验通过(client_id: 2114206697456) +2025-12-03 19:27:31.637 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:157 - 调用大模型 - 用户(): 关注他 +2025-12-03 19:27:32.991 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:169 - 大模型回复完成 - 会话ID: c8b6c261-b37f-4f9e-8704-559b7316f1b0, 完整回复: 我的银行卡丢了,现在急着取钱,怎么办啊? +2025-12-03 19:27:36.432 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:244 - 前端 2114206697456 主动断开连接 +2025-12-03 19:27:36.433 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:311 - 连接 2114206697456 资源清理完成,当前连接数: 0 +2025-12-03 19:30:24.140 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 19:30:24.141 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 19:30:25.821 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2953006548928 +2025-12-03 19:30:25.822 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2953006548928 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 19:30:25.825 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=2953006548928,user_id=1001 +2025-12-03 19:30:25.826 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 2953006548928) +2025-12-03 19:30:28.695 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 喂喂喂 +2025-12-03 19:30:30.019 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 52b88cf4-7ee2-434d-b8f4-025f04cd7406, 完整回复: 我的银行卡丢了,现在急着取钱,怎么办啊? +2025-12-03 19:30:32.520 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,有什么好处的 +2025-12-03 19:30:32.708 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 2953006548928 主动断开连接 +2025-12-03 19:30:32.709 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:315 - 连接 2953006548928 资源清理完成,当前连接数: 0 +2025-12-03 19:32:55.617 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 19:32:55.618 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 19:32:57.685 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2903164519808 +2025-12-03 19:32:57.686 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2903164519808 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 19:32:57.689 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=2903164519808,user_id=1001 +2025-12-03 19:32:57.689 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 2903164519808) +2025-12-03 19:33:00.386 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): hello,可以见吗 +2025-12-03 19:33:01.674 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: ef042312-1e39-409f-8136-cf8bdc837236, 完整回复: 我现在特别能帮我解决一下银行卡丢了取钱的问题吗? +2025-12-03 19:33:04.991 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ?就是那个 +2025-12-03 19:33:06.047 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: ef042312-1e39-409f-8136-cf8bdc837236, 完整回复: 我姓李,银行卡丢了,现在急需取钱! +2025-12-03 19:33:18.084 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 是啊,没发呀,刚收了,这不要命 +2025-12-03 19:33:19.238 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: ef042312-1e39-409f-8136-cf8bdc837236, 完整回复: 我急用钱,没卡怎么取?能想想办法吗? +2025-12-03 19:33:19.794 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,你好 +2025-12-03 19:33:20.925 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: ef042312-1e39-409f-8136-cf8bdc837236, 完整回复: 我银行卡丢了,现在必须取钱,能帮帮我吗? +2025-12-03 19:33:25.367 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,怎么说呢 +2025-12-03 19:33:26.452 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: ef042312-1e39-409f-8136-cf8bdc837236, 完整回复: 我真的很急,银行卡丢了,能不能先取钱? +2025-12-03 19:33:28.094 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,你们什么 +2025-12-03 19:33:29.127 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: ef042312-1e39-409f-8136-cf8bdc837236, 完整回复: 你们能取钱再补卡吗?我 +2025-12-03 19:33:29.581 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 啊,这是明点 +2025-12-03 19:33:30.785 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: ef042312-1e39-409f-8136-cf8bdc837236, 完整回复: 我急用钱,卡丢了怎么解决?快帮帮我! +2025-12-03 19:33:31.852 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 2903164519808 主动断开连接 +2025-12-03 19:33:31.853 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2903164519808 资源清理完成,当前连接数: 0 +2025-12-03 19:34:36.942 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 19:34:36.943 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 19:34:38.230 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2319484372544 +2025-12-03 19:34:38.231 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2319484372544 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 19:34:38.234 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=2319484372544,user_id=1001 +2025-12-03 19:34:38.235 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 2319484372544) +2025-12-03 19:34:41.635 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): hellohello +2025-12-03 19:34:42.845 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 7a686bd4-99d6-4d0e-aae0-43779509db57, 完整回复: 我的银行卡丢了,我现在急着取钱,怎么办? +2025-12-03 19:34:48.583 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 用这张表的 +2025-12-03 19:34:49.599 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 7a686bd4-99d6-4d0e-aae0-43779509db57, 完整回复: 请帮我查一下账户余额,我急着用钱! +2025-12-03 19:34:52.174 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 2319484372544 主动断开连接 +2025-12-03 19:34:52.175 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2319484372544 资源清理完成,当前连接数: 0 +2025-12-03 19:36:40.032 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 19:36:40.032 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 19:36:42.065 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2191861661952 +2025-12-03 19:36:42.066 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2191861661952 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 19:36:44.467 | ERROR | audio_ai_chat.core.websocket_handler:handle_connection:286 - 连接 2191861661952 处理异常:(1001, '') +2025-12-03 19:36:44.467 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2191861661952 资源清理完成,当前连接数: 0 +2025-12-03 19:36:45.596 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2191861666080 +2025-12-03 19:36:45.596 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2191861666080 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 19:36:45.599 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=2191861666080,user_id=1001 +2025-12-03 19:36:45.599 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 2191861666080) +2025-12-03 19:36:47.362 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 是吗 +2025-12-03 19:36:48.587 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 94042906-4e48-410a-8561-f957004d53c9, 完整回复: 我的银行卡丢了,现在急着取钱,有什么办法吗? +2025-12-03 19:36:50.335 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ?你那个昨天发的图片还在用吗 +2025-12-03 19:36:51.361 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 94042906-4e48-410a-8561-f957004d53c9, 完整回复: 在的,我现在就用那个方法试试。 +2025-12-03 19:36:52.777 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ?啊,上一个 +2025-12-03 19:36:53.831 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 94042906-4e48-410a-8561-f957004d53c9, 完整回复: 那个方法不行,我现在更急了! +2025-12-03 19:36:55.586 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 你只要打板就算了 +2025-12-03 19:36:56.627 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 94042906-4e48-410a-8561-f957004d53c9, 完整回复: 我真的很急,能不能快点帮我解决? +2025-12-03 19:36:58.688 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,嗯,是你改的这个鲁班是吧 +2025-12-03 19:36:59.913 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 94042906-4e48-410a-8561-f957004d53c9, 完整回复: 鲁班改了我也不知道,我现在只能靠自己! +2025-12-03 19:37:02.479 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ?明天早上来了,我发一下深度测试 +2025-12-03 19:37:03.344 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 94042906-4e48-410a-8561-f957004d53c9, 完整回复: 明天来不及,我现在就必须解决这个问题! +2025-12-03 19:37:05.114 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 2191861666080 主动断开连接 +2025-12-03 19:37:05.115 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2191861666080 资源清理完成,当前连接数: 0 +2025-12-03 19:38:06.868 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 19:38:06.868 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 19:38:11.312 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=1606372820928 +2025-12-03 19:38:11.312 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 1606372820928 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 19:38:11.315 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=1606372820928,user_id=1001 +2025-12-03 19:38:11.316 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 1606372820928) +2025-12-03 19:38:14.664 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 等等一 +2025-12-03 19:38:15.818 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 9661da0d-5521-4197-9163-5de4764d37ca, 完整回复: 我的银行卡丢了,现在急着取钱,怎么办啊? +2025-12-03 19:38:23.045 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 1606372820928 主动断开连接 +2025-12-03 19:38:23.046 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 1606372820928 资源清理完成,当前连接数: 0 +2025-12-03 19:39:10.381 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 19:39:10.381 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 19:39:13.036 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2590293082176 +2025-12-03 19:39:13.036 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2590293082176 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 19:39:18.055 | WARNING | audio_ai_chat.core.websocket_handler:connect:50 - 连接 2590293082176 身份校验超时 +2025-12-03 19:39:18.055 | ERROR | audio_ai_chat.core.websocket_handler:handle_connection:286 - 连接 2590293082176 处理异常:连接 2590293082176 身份校验超时 +2025-12-03 19:39:18.056 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2590293082176 资源清理完成,当前连接数: 0 +2025-12-03 19:39:18.656 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2590293082368 +2025-12-03 19:39:18.656 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2590293082368 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 19:39:23.652 | WARNING | audio_ai_chat.core.websocket_handler:connect:50 - 连接 2590293082368 身份校验超时 +2025-12-03 19:39:23.652 | ERROR | audio_ai_chat.core.websocket_handler:handle_connection:286 - 连接 2590293082368 处理异常:连接 2590293082368 身份校验超时 +2025-12-03 19:39:23.652 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2590293082368 资源清理完成,当前连接数: 0 +2025-12-03 19:39:25.629 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2590293088512 +2025-12-03 19:39:25.629 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2590293088512 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 19:39:30.624 | WARNING | audio_ai_chat.core.websocket_handler:connect:50 - 连接 2590293088512 身份校验超时 +2025-12-03 19:39:30.624 | ERROR | audio_ai_chat.core.websocket_handler:handle_connection:286 - 连接 2590293088512 处理异常:连接 2590293088512 身份校验超时 +2025-12-03 19:39:30.624 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2590293088512 资源清理完成,当前连接数: 0 +2025-12-03 19:41:48.466 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2590293091968 +2025-12-03 19:41:48.466 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2590293091968 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 19:41:53.464 | WARNING | audio_ai_chat.core.websocket_handler:connect:50 - 连接 2590293091968 身份校验超时 +2025-12-03 19:41:53.465 | ERROR | audio_ai_chat.core.websocket_handler:handle_connection:286 - 连接 2590293091968 处理异常:连接 2590293091968 身份校验超时 +2025-12-03 19:41:53.465 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2590293091968 资源清理完成,当前连接数: 0 +2025-12-03 19:43:15.401 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2590293093216 +2025-12-03 19:43:15.402 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2590293093216 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 19:43:20.415 | WARNING | audio_ai_chat.core.websocket_handler:connect:50 - 连接 2590293093216 身份校验超时 +2025-12-03 19:43:20.416 | ERROR | audio_ai_chat.core.websocket_handler:handle_connection:286 - 连接 2590293093216 处理异常:连接 2590293093216 身份校验超时 +2025-12-03 19:43:20.416 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2590293093216 资源清理完成,当前连接数: 0 +2025-12-03 19:43:32.297 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2590293095952 +2025-12-03 19:43:32.298 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2590293095952 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 19:43:32.321 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=2590293095952,user_id=1001 +2025-12-03 19:43:32.322 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 2590293095952) +2025-12-03 19:43:32.910 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 2590293095952 主动断开连接 +2025-12-03 19:43:32.911 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2590293095952 资源清理完成,当前连接数: 0 +2025-12-03 19:43:35.101 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2590293097584 +2025-12-03 19:43:35.101 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2590293097584 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 19:43:35.368 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=2590293097584,user_id=1001 +2025-12-03 19:43:35.368 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 2590293097584) +2025-12-03 19:43:35.881 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 2590293097584 主动断开连接 +2025-12-03 19:43:35.882 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2590293097584 资源清理完成,当前连接数: 0 +2025-12-03 19:43:51.253 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2590293607568 +2025-12-03 19:43:51.253 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2590293607568 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 19:43:51.262 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=2590293607568,user_id=1001 +2025-12-03 19:43:51.262 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 2590293607568) +2025-12-03 19:43:55.234 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 你好你好 +2025-12-03 19:43:56.291 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 4a448318-df14-478d-96b2-ae35d3096cec, 完整回复: 你好能帮我一下吗? +2025-12-03 19:44:00.416 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 啊 +2025-12-03 19:44:01.428 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 4a448318-df14-478d-96b2-ae35d3096cec, 完整回复: 我的我现在急着取钱! +2025-12-03 19:44:02.685 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 嗯 +2025-12-03 19:44:03.644 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 4a448318-df14-478d-96b2-ae35d3096cec, 完整回复: 我姓李,真的非常着急,能想想办法吗? +2025-12-03 19:44:04.937 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 嗯 +2025-12-03 19:44:07.191 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,好的好的好的好的 +2025-12-03 19:44:07.377 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 4a448318-df14-478d-96b2-ae35d3096cec, 完整回复: 我身上没带其他证件,但可以提供身份证号码! +2025-12-03 19:44:08.155 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 4a448318-df14-478d-96b2-ae35d3096cec, 完整回复: 那帮你联系处理,稍等一下! +2025-12-03 19:44:08.807 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 2590293607568 主动断开连接 +2025-12-03 19:44:08.808 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2590293607568 资源清理完成,当前连接数: 0 +2025-12-03 19:50:06.816 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 19:50:06.816 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 19:50:10.021 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2221647381264 +2025-12-03 19:50:10.022 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2221647381264 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 19:50:10.232 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=2221647381264,user_id=1001 +2025-12-03 19:50:10.233 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 2221647381264) +2025-12-03 19:50:10.841 | ERROR | audio_ai_chat.core.websocket_handler:handle_connection:286 - 连接 2221647381264 处理异常:连接失败: 'ByteDanceTTSSocketClient' object has no attribute '_consume_queue' +2025-12-03 19:50:10.841 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2221647381264 资源清理完成,当前连接数: 0 +2025-12-03 19:50:12.601 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2221647385392 +2025-12-03 19:50:12.602 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2221647385392 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 19:50:12.684 | ERROR | audio_ai_chat.core.websocket_handler:connect:61 - 连接 2221647385392 首个包类型错误 +2025-12-03 19:50:12.684 | ERROR | audio_ai_chat.core.websocket_handler:handle_connection:286 - 连接 2221647385392 处理异常:连接 2221647385392 首个包类型错误 +2025-12-03 19:50:12.684 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2221647385392 资源清理完成,当前连接数: 0 +2025-12-03 19:50:41.192 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 19:50:41.192 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 19:50:42.898 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2094654161680 +2025-12-03 19:50:42.899 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2094654161680 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 19:50:42.912 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=2094654161680,user_id=1001 +2025-12-03 19:50:42.913 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 2094654161680) +2025-12-03 19:50:43.403 | ERROR | audio_ai_chat.core.websocket_handler:handle_connection:286 - 连接 2094654161680 处理异常:连接失败: 'ByteDanceTTSSocketClient' object has no attribute '_consume_queue' +2025-12-03 19:50:43.403 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2094654161680 资源清理完成,当前连接数: 0 +2025-12-03 19:51:37.470 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 19:51:37.470 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 19:52:13.511 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 19:52:13.511 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 19:52:16.093 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=1873070899600 +2025-12-03 19:52:16.093 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 1873070899600 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 19:52:16.125 | ERROR | audio_ai_chat.core.websocket_handler:connect:61 - 连接 1873070899600 首个包类型错误 +2025-12-03 19:52:16.125 | ERROR | audio_ai_chat.core.websocket_handler:handle_connection:286 - 连接 1873070899600 处理异常:连接 1873070899600 首个包类型错误 +2025-12-03 19:52:16.126 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 1873070899600 资源清理完成,当前连接数: 0 +2025-12-03 19:52:25.826 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 19:52:25.826 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 19:52:28.773 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2488308318512 +2025-12-03 19:52:28.774 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2488308318512 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 19:52:28.876 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=2488308318512,user_id=1001 +2025-12-03 19:52:28.876 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 2488308318512) +2025-12-03 19:52:33.752 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 2488308318512 主动断开连接 +2025-12-03 19:52:33.753 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2488308318512 资源清理完成,当前连接数: 0 +2025-12-03 19:53:05.217 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 19:53:05.218 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 19:53:10.553 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=1841858527680 +2025-12-03 19:53:10.554 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 1841858527680 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 19:53:10.680 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=1841858527680,user_id=1001 +2025-12-03 19:53:10.681 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 1841858527680) +2025-12-03 19:53:13.929 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 啊 +2025-12-03 19:53:15.320 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 7616a83d-75a9-4484-9fa1-98d89943defd, 完整回复: 我的银行卡丢了,现在急着取钱,能帮帮我吗? +2025-12-03 19:53:19.981 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,我我真不知道 +2025-12-03 19:53:21.099 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 7616a83d-75a9-4484-9fa1-98d89943defd, 完整回复: 我真的很着急,能不能先想办法取到钱? +2025-12-03 19:53:22.287 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,不是 +2025-12-03 19:53:23.639 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 7616a83d-75a9-4484-9fa1-98d89943defd, 完整回复: 我不是故意丢卡的,现在真的太急了! +2025-12-03 19:53:25.558 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 走走走走 +2025-12-03 19:53:27.271 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 7616a83d-75a9-4484-9fa1-98d89943defd, 完整回复: 我得赶紧取到钱,能快点帮我处理吗? +2025-12-03 19:53:31.730 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,你怎么了?兄弟 +2025-12-03 19:53:33.076 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 7616a83d-75a9-4484-9fa1-98d89943defd, 完整回复: 我卡丢了,急着用钱,能帮我想办法吗? +2025-12-03 19:53:37.957 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,你是不是丢了 +2025-12-03 19:53:39.179 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 7616a83d-75a9-4484-9fa1-98d89943defd, 完整回复: 对,卡丢了,我现在必须取到钱! +2025-12-03 19:53:40.144 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,有没有 +2025-12-03 19:53:41.227 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 7616a83d-75a9-4484-9fa1-98d89943defd, 完整回复: 有没有别的办法能先取到钱? +2025-12-03 19:53:44.268 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 你爱娶不娶 +2025-12-03 19:53:45.264 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ?那来讲 +2025-12-03 19:53:45.630 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 7616a83d-75a9-4484-9fa1-98d89943defd, 完整回复: 我现在真的急用钱,你能不能帮帮我? +2025-12-03 19:53:46.552 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 7616a83d-75a9-4484-9fa1-98d89943defd, 完整回复: 我卡丢了,现在必须取钱,你得帮我想办法! +2025-12-03 19:53:47.211 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 我们 +2025-12-03 19:53:48.396 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 7616a83d-75a9-4484-9fa1-98d89943defd, 完整回复: 快帮我想想办法,我真的很急! +2025-12-03 19:53:50.955 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 没有回复 +2025-12-03 19:53:52.065 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 7616a83d-75a9-4484-9fa1-98d89943defd, 完整回复: 我真的很急,求你帮帮我! +2025-12-03 19:53:52.785 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 嗯,有 +2025-12-03 19:53:53.833 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 7616a83d-75a9-4484-9fa1-98d89943defd, 完整回复: 有什么办法,我等不了 +2025-12-03 19:53:55.459 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 重新一下 +2025-12-03 19:53:56.549 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 7616a83d-75a9-4484-9fa1-98d89943defd, 完整回复: 快点重新办卡,我急着用钱! +2025-12-03 19:53:59.177 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 小事儿,你都办不了 +2025-12-03 19:54:00.376 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 1841858527680 主动断开连接 +2025-12-03 19:54:00.377 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 1841858527680 资源清理完成,当前连接数: 0 +2025-12-03 19:54:00.437 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 7616a83d-75a9-4484-9fa1-98d89943defd, 完整回复: 我这不是急了吗?你当真小事儿啊! +2025-12-03 19:56:04.516 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 19:56:04.516 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 19:56:08.127 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=1782830121360 +2025-12-03 19:56:08.128 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 1782830121360 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 19:56:08.185 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=1782830121360,user_id=1001 +2025-12-03 19:56:08.185 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 1782830121360) +2025-12-03 19:56:12.170 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 如果 +2025-12-03 19:56:13.413 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: b0a151e5-e167-4f26-87f2-2bd855cd9c49, 完整回复: 我的银行卡丢了,现在急着用钱,能不能帮忙想想办法? +2025-12-03 19:56:13.803 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 什么呢 +2025-12-03 19:56:14.784 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: b0a151e5-e167-4f26-87f2-2bd855cd9c49, 完整回复: 能不能先取现,我实在没办法等了! +2025-12-03 19:56:18.341 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ?什么时候 +2025-12-03 19:56:19.438 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: b0a151e5-e167-4f26-87f2-2bd855cd9c49, 完整回复: 现在!立刻!我有急事需要用钱! +2025-12-03 19:56:28.915 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 你知道 +2025-12-03 19:56:30.238 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: b0a151e5-e167-4f26-87f2-2bd855cd9c49, 完整回复: 我姓李,真的非常急,能快点处理吗? +2025-12-03 19:56:31.109 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 你不不 +2025-12-03 19:56:32.289 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: b0a151e5-e167-4f26-87f2-2bd855cd9c49, 完整回复: 我不能等了,必须马上解决! +2025-12-03 19:56:32.542 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 嗯 +2025-12-03 19:56:33.496 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: b0a151e5-e167-4f26-87f2-2bd855cd9c49, 完整回复: 快点,我时间不多! +2025-12-03 19:56:34.386 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,对吧 +2025-12-03 19:56:35.568 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: b0a151e5-e167-4f26-87f2-2bd855cd9c49, 完整回复: 对,我真的很急,能帮帮我吗? +2025-12-03 19:56:41.127 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 1782830121360 主动断开连接 +2025-12-03 19:56:41.128 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 1782830121360 资源清理完成,当前连接数: 0 +2025-12-03 19:56:43.020 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 19:56:43.020 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 19:56:56.250 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=1972956414256 +2025-12-03 19:56:56.250 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 1972956414256 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 19:56:56.450 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=1972956414256,user_id=1001 +2025-12-03 19:56:56.451 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 1972956414256) +2025-12-03 19:57:01.239 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 你好你好 +2025-12-03 19:57:02.354 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 0ebd810c-e409-4514-a3c5-2f1eda4774f0, 完整回复: 我的银行卡丢了,现在急着取钱,怎么办啊 +2025-12-03 19:57:04.901 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 嗯 +2025-12-03 19:57:05.825 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 0ebd810c-e409-4514-a3c5-2f1eda4774f0, 完整回复: 我姓李,真的非常着急! +2025-12-03 19:57:10.571 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,必须都 +2025-12-03 19:57:11.780 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 0ebd810c-e409-4514-a3c5-2f1eda4774f0, 完整回复: 我现金急用,能不能先取钱再补卡? +2025-12-03 19:57:14.584 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 嗯 +2025-12-03 19:57:15.704 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 0ebd810c-e409-4514-a3c5-2f1eda4774f0, 完整回复: 我能不能先取钱,再补办银行卡? +2025-12-03 19:57:16.128 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 因为 +2025-12-03 19:57:17.126 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 0ebd810c-e409-4514-a3c5-2f1eda4774f0, 完整回复: 我没有,真的急用钱! +2025-12-03 19:57:19.032 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 另外 +2025-12-03 19:57:20.160 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 0ebd810c-e409-4514-a3c5-2f1eda4774f0, 完整回复: 我明天就要用钱,今天必须解决! +2025-12-03 19:57:22.233 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 我们 +2025-12-03 19:57:23.336 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 0ebd810c-e409-4514-a3c5-2f1eda4774f0, 完整回复: 你们能快点处理吗?我真的等不了! +2025-12-03 19:57:26.876 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 嗯 +2025-12-03 19:57:28.043 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 0ebd810c-e409-4514-a3c5-2f1eda4774f0, 完整回复: 我急得都快哭了,求你们帮忙! +2025-12-03 19:57:31.284 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 真的 +2025-12-03 19:57:32.414 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 0ebd810c-e409-4514-a3c5-2f1eda4774f0, 完整回复: 我再找不到解决办法就只能借钱了! +2025-12-03 19:57:35.152 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 你爱我 +2025-12-03 19:57:36.133 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 0ebd810c-e409-4514-a3c5-2f1eda4774f0, 完整回复: 谢谢你的关心,快帮我解决这个问题吧! +2025-12-03 19:57:36.972 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 一个人 +2025-12-03 19:57:37.874 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 0ebd810c-e409-4514-a3c5-2f1eda4774f0, 完整回复: 我一个人在外地,真的没办法等! +2025-12-03 19:57:38.307 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,那不是 +2025-12-03 19:57:38.593 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 1972956414256 主动断开连接 +2025-12-03 19:57:38.594 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 1972956414256 资源清理完成,当前连接数: 0 +2025-12-03 19:58:58.062 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 19:58:58.062 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 19:58:58.991 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=1958433702144 +2025-12-03 19:58:58.991 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 1958433702144 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 19:59:01.692 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=1958433708096 +2025-12-03 19:59:01.693 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 1958433708096 已接受,等待身份信息(5秒超时),当前连接数: 2 +2025-12-03 19:59:01.708 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=1958433708096,user_id=1001 +2025-12-03 19:59:01.709 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 1958433708096) +2025-12-03 19:59:03.986 | WARNING | audio_ai_chat.core.websocket_handler:connect:50 - 连接 1958433702144 身份校验超时 +2025-12-03 19:59:03.986 | ERROR | audio_ai_chat.core.websocket_handler:handle_connection:286 - 连接 1958433702144 处理异常:连接 1958433702144 身份校验超时 +2025-12-03 19:59:03.987 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 1958433702144 资源清理完成,当前连接数: 1 +2025-12-03 19:59:06.000 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 那个 +2025-12-03 19:59:07.315 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 938cf556-ef07-4ecb-9a31-91e7cb0a8ab3, 完整回复: 李阿姨,我的银行卡丢了,现在急着取钱,有什么办法吗? +2025-12-03 19:59:07.506 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 啊你好啊 +2025-12-03 19:59:08.433 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 938cf556-ef07-4ecb-9a31-91e7cb0a8ab3, 完整回复: 我银行卡丢了,现在急着取钱! +2025-12-03 19:59:11.413 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 嗯 +2025-12-03 19:59:12.341 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 938cf556-ef07-4ecb-9a31-91e7cb0a8ab3, 完整回复: 我银行卡丢了,现在急着取钱! +2025-12-03 19:59:20.894 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,对呀 +2025-12-03 19:59:22.022 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 938cf556-ef07-4ecb-9a31-91e7cb0a8ab3, 完整回复: 我银行卡丢了,现在急着取钱! +2025-12-03 19:59:23.848 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,喂你好 +2025-12-03 19:59:25.791 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 938cf556-ef07-4ecb-9a31-91e7cb0a8ab3, 完整回复: 我银行卡丢了,现在急着取钱! +2025-12-03 19:59:25.940 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,怎么了 +2025-12-03 19:59:26.872 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 938cf556-ef07-4ecb-9a31-91e7cb0a8ab3, 完整回复: 我银行卡现在急着取钱! +2025-12-03 19:59:28.008 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ?老师 +2025-12-03 19:59:28.980 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 938cf556-ef07-4ecb-9a31-91e7cb0a8ab3, 完整回复: 我银行卡丢了,现在急着取钱! +2025-12-03 19:59:29.799 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,然后 +2025-12-03 19:59:30.756 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 938cf556-ef07-4ecb-9a31-91e7cb0a8ab3, 完整回复: 我银行卡丢了,现在急着取钱! +2025-12-03 19:59:31.092 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 看看那个 +2025-12-03 19:59:32.267 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 938cf556-ef07-4ecb-9a31-91e7cb0a8ab3, 完整回复: 我银行卡现在急着取 +2025-12-03 19:59:33.076 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 这对 +2025-12-03 19:59:34.101 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 938cf556-ef07-4ecb-9a31-91e7cb0a8ab3, 完整回复: 我银行卡丢了,现在急着取钱! +2025-12-03 19:59:37.612 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 你这个文件 +2025-12-03 19:59:38.710 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 938cf556-ef07-4ecb-9a31-91e7cb0a8ab3, 完整回复: 我银行卡丢了,现在急着取钱! +2025-12-03 19:59:40.143 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,我爱你,你好表情 +2025-12-03 19:59:41.123 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 938cf556-ef07-4ecb-9a31-91e7cb0a8ab3, 完整回复: 我银行卡丢了,现在急着取钱! +2025-12-03 19:59:54.993 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 知不知道 +2025-12-03 19:59:56.042 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 938cf556-ef07-4ecb-9a31-91e7cb0a8ab3, 完整回复: 我银行卡丢了,现在急着取钱! +2025-12-03 20:00:00.723 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 问题?我我我不知道,我就要解释他是对,那我的这个问题 +2025-12-03 20:00:01.815 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 938cf556-ef07-4ecb-9a31-91e7cb0a8ab3, 完整回复: 我银行卡丢了,现在急着取钱! +2025-12-03 20:00:04.311 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 打是打这的员工 +2025-12-03 20:00:05.272 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 938cf556-ef07-4ecb-9a31-91e7cb0a8ab3, 完整回复: 我银行卡丢了,现在急着取钱! +2025-12-03 20:00:06.013 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,真的假 +2025-12-03 20:00:06.912 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 938cf556-ef07-4ecb-9a31-91e7cb0a8ab3, 完整回复: 丢了,现在急着取钱! +2025-12-03 20:00:08.260 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 嗯,我说一点 +2025-12-03 20:00:09.073 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,你把 +2025-12-03 20:00:09.325 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 938cf556-ef07-4ecb-9a31-91e7cb0a8ab3, 完整回复: 我银行卡丢了,现在急着取钱! +2025-12-03 20:00:11.472 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 有的话,我觉得 +2025-12-03 20:00:15.728 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 你不会再是等你 +2025-12-03 20:00:19.699 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 我觉得如果我 +2025-12-03 20:00:22.452 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 你在我的在吗 +2025-12-03 20:00:28.076 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ?我的好的 +2025-12-03 20:00:28.178 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 1958433708096 主动断开连接 +2025-12-03 20:00:28.179 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 1958433708096 资源清理完成,当前连接数: 0 +2025-12-03 20:00:30.065 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 20:00:30.066 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 20:00:37.737 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2849732280768 +2025-12-03 20:00:37.738 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2849732280768 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 20:00:37.940 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=2849732280768,user_id=1001 +2025-12-03 20:00:37.941 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 2849732280768) +2025-12-03 20:00:43.119 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 你好,我好大家好 +2025-12-03 20:00:44.452 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 84499f57-921b-43b8-abc9-c04fa5287a6a, 完整回复: 我的银行卡丢了,现在急着取钱,怎么办啊? +2025-12-03 20:00:47.240 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,就是你能睡着 +2025-12-03 20:00:48.455 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 84499f57-921b-43b8-abc9-c04fa5287a6a, 完整回复: 我急着用钱,现在银行卡丢了,你倒是能睡着? +2025-12-03 20:00:50.015 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 2849732280768 主动断开连接 +2025-12-03 20:00:50.015 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2849732280768 资源清理完成,当前连接数: 0 +2025-12-03 20:01:29.269 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 20:01:29.269 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 20:01:34.885 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2435997483360 +2025-12-03 20:01:34.886 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2435997483360 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 20:01:34.978 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=2435997483360,user_id=1001 +2025-12-03 20:01:34.979 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 2435997483360) +2025-12-03 20:01:39.669 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 你好,我好大家好 +2025-12-03 20:01:40.942 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: ebff0ff3-06c6-42fd-84a6-bd2f82161e2c, 完整回复: 我的银行卡丢了,现在急着用钱,怎么办啊? +2025-12-03 20:01:45.724 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,给我说话 +2025-12-03 20:01:46.864 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: ebff0ff3-06c6-42fd-84a6-bd2f82161e2c, 完整回复: 我银行卡丢了,现在急着取钱,你快帮帮我! +2025-12-03 20:01:50.813 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 啊,对 +2025-12-03 20:01:51.863 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: ebff0ff3-06c6-42fd-84a6-bd2f82161e2c, 完整回复: 我,真的非常着急,能先帮我取点钱吗? +2025-12-03 20:01:57.665 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 2435997483360 主动断开连接 +2025-12-03 20:01:57.666 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2435997483360 资源清理完成,当前连接数: 0 +2025-12-03 20:02:20.750 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 20:02:20.751 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 20:02:23.724 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=1568644776288 +2025-12-03 20:02:23.725 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 1568644776288 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 20:02:23.924 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=1568644776288,user_id=1001 +2025-12-03 20:02:23.925 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 1568644776288) +2025-12-03 20:02:28.226 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 喂,你好,我说话 +2025-12-03 20:02:29.658 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 7132500b-6eda-4b1a-8ec9-ab1230ed418e, 完整回复: 李:我的银行卡丢了,现在急着取钱,怎么办啊? +2025-12-03 20:02:31.423 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 没有 +2025-12-03 20:02:32.167 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 1568644776288 主动断开连接 +2025-12-03 20:02:32.168 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 1568644776288 资源清理完成,当前连接数: 0 +2025-12-03 20:03:58.562 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 20:03:58.563 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 20:04:05.115 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=1681868964096 +2025-12-03 20:04:05.115 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 1681868964096 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 20:04:05.132 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=1681868964096,user_id=1001 +2025-12-03 20:04:05.133 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 1681868964096) +2025-12-03 20:04:09.515 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 你好你好 +2025-12-03 20:04:10.439 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 65bf2b61-ebc9-4c48-8857-9193bac812b4, 完整回复: +2025-12-03 20:04:13.402 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 啦啦啦啦啦 +2025-12-03 20:04:14.309 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 65bf2b61-ebc9-4c48-8857-9193bac812b4, 完整回复: 我的银行卡丢了,现在急着用钱,怎么办啊? +2025-12-03 20:04:16.827 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,完全不能 +2025-12-03 20:04:17.981 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 65bf2b61-ebc9-4c48-8857-9193bac812b4, 完整回复: 我的银行卡丢了,现在急着用钱,怎么办啊? +2025-12-03 20:04:18.670 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 你好,没有 +2025-12-03 20:04:19.696 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 65bf2b61-ebc9-4c48-8857-9193bac812b4, 完整回复: 你好,没有 +2025-12-03 20:04:20.809 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,行行行行行行 +2025-12-03 20:04:21.808 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 嗯 +2025-12-03 20:04:21.811 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 65bf2b61-ebc9-4c48-8857-9193bac812b4, 完整回复: 行行行,快帮我解决银行卡丢失的问题! +2025-12-03 20:04:22.562 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 1681868964096 主动断开连接 +2025-12-03 20:04:22.563 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 1681868964096 资源清理完成,当前连接数: 0 +2025-12-03 20:09:46.919 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 20:09:46.919 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 20:09:54.264 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 20:09:54.264 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 20:10:02.436 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2271755403376 +2025-12-03 20:10:02.436 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2271755403376 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 20:10:02.580 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=2271755403376,user_id=1001 +2025-12-03 20:10:02.581 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 2271755403376) +2025-12-03 20:10:05.547 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 你 +2025-12-03 20:10:06.730 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 7c52cebd-a1d1-4330-93ee-0959756385ee, 完整回复: 丢了,现在急着取钱,怎么办啊? +2025-12-03 20:10:09.514 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 你好你好 +2025-12-03 20:10:10.341 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 7c52cebd-a1d1-4330-93ee-0959756385ee, 完整回复: 你好,我的银行卡丢了,能帮我想想办法吗? +2025-12-03 20:10:11.652 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,我这个就是 +2025-12-03 20:10:12.773 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 7c52cebd-a1d1-4330-93ee-0959756385ee, 完整回复: 我李,银行卡丢了,现在急着取钱! +2025-12-03 20:10:13.900 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 2271755403376 主动断开连接 +2025-12-03 20:10:13.900 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2271755403376 资源清理完成,当前连接数: 0 +2025-12-03 20:11:05.759 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 20:11:05.759 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 20:11:13.644 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=1608561312096 +2025-12-03 20:11:13.645 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 1608561312096 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 20:11:13.852 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=1608561312096,user_id=1001 +2025-12-03 20:11:13.853 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 1608561312096) +2025-12-03 20:11:17.490 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 啊,还有两个 +2025-12-03 20:11:18.624 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 67195c15-83e5-4ce2-8c7e-a1e619488b7f, 完整回复: 我的,现在急着取钱,能不能帮 +2025-12-03 20:11:21.278 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 喂喂喂读过没读过,还有整 +2025-12-03 20:11:22.303 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 67195c15-83e5-4ce2-8c7e-a1e619488b7f, 完整回复: 我银行卡丢了,急着取钱,有没有其他办法? +2025-12-03 20:11:25.915 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 嗯,你呢 +2025-12-03 20:11:26.839 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 这个的 +2025-12-03 20:11:27.033 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 67195c15-83e5-4ce2-8c7e-a1e619488b7f, 完整回复: 我姓李,银行卡丢了,能先取钱吗? +2025-12-03 20:11:28.145 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 67195c15-83e5-4ce2-8c7e-a1e619488b7f, 完整回复: 我姓李,银行卡丢了,能先取钱吗? +2025-12-03 20:11:28.246 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 带我去 +2025-12-03 20:11:29.065 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 1608561312096 主动断开连接 +2025-12-03 20:11:29.066 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 1608561312096 资源清理完成,当前连接数: 0 +2025-12-03 20:13:08.062 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 20:13:08.062 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 20:13:13.317 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2368511197440 +2025-12-03 20:13:13.317 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2368511197440 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 20:13:13.342 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=2368511197440,user_id=1001 +2025-12-03 20:13:13.343 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 2368511197440) +2025-12-03 20:13:17.881 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 嗯嗯嗯 +2025-12-03 20:13:19.196 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 6942ed26-a58b-48f8-8488-43075dc7c69d, 完整回复: 我的银行卡丢了,我现在急着取钱,有什么办法 +2025-12-03 20:13:22.008 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,你没有 +2025-12-03 20:13:23.052 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 6942ed26-a58b-48f8-8488-43075dc7c69d, 完整回复: 我姓李,真的非常着急,能帮帮我吗? +2025-12-03 20:13:29.900 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,那我感觉就就就不是这个原因吗?但是你不会那么多吗 +2025-12-03 20:13:31.020 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 6942ed26-a58b-48f8-8488-43075dc7c69d, 完整回复: 我真没时间解释了,快帮我解决一下! +2025-12-03 20:13:34.230 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ?但是这个他从自己的评价聊过中 +2025-12-03 20:13:35.165 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 6942ed26-a58b-48f8-8488-43075dc7c69d, 完整回复: 我只问你,现在能不能先给我取出现金? +2025-12-03 20:13:36.438 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 怎么了,你们 +2025-12-03 20:13:37.016 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 2368511197440 主动断开连接 +2025-12-03 20:13:37.017 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2368511197440 资源清理完成,当前连接数: 0 +2025-12-03 20:23:06.640 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 20:23:06.640 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 20:23:12.304 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2794567549184 +2025-12-03 20:23:12.304 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2794567549184 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 20:23:12.328 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=2794567549184,user_id=1001 +2025-12-03 20:23:12.329 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 2794567549184) +2025-12-03 20:23:17.284 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 喂喂喂喂 +2025-12-03 20:23:18.751 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,我不是 +2025-12-03 20:23:19.972 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 结束呢 +2025-12-03 20:23:35.396 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 2794567549184 主动断开连接 +2025-12-03 20:23:35.398 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2794567549184 资源清理完成,当前连接数: 0 +2025-12-03 20:24:44.358 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 20:24:44.359 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 20:25:29.295 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 20:25:29.296 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 20:28:21.892 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 20:28:21.893 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 20:30:35.602 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 20:30:35.602 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 20:30:37.436 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2578057707872 +2025-12-03 20:30:37.436 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2578057707872 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 20:30:37.458 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=2578057707872,user_id=1001 +2025-12-03 20:30:37.458 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 2578057707872) +2025-12-03 20:30:40.395 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 2578057707872 主动断开连接 +2025-12-03 20:30:40.395 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2578057707872 资源清理完成,当前连接数: 0 +2025-12-03 20:31:07.226 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 20:31:07.226 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 20:31:08.771 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2559720341952 +2025-12-03 20:31:08.772 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2559720341952 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 20:31:08.865 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=2559720341952,user_id=1001 +2025-12-03 20:31:08.865 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 2559720341952) +2025-12-03 20:31:13.303 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): hellohello +2025-12-03 20:31:14.978 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 4ebd1d62-618c-4acd-a334-3bbd4e746f72, 完整回复: 我的银行卡丢了,现在急着用钱,怎么办啊? +2025-12-03 20:31:18.929 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 2559720341952 主动断开连接 +2025-12-03 20:31:18.930 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2559720341952 资源清理完成,当前连接数: 0 +2025-12-03 20:33:16.458 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 20:33:16.459 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 20:33:48.097 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2362971701648 +2025-12-03 20:33:48.098 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2362971701648 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 20:33:48.302 | ERROR | audio_ai_chat.core.websocket_handler:connect:61 - 连接 2362971701648 首个包类型错误 +2025-12-03 20:33:48.303 | ERROR | audio_ai_chat.core.websocket_handler:handle_connection:286 - 连接 2362971701648 处理异常:连接 2362971701648 首个包类型错误 +2025-12-03 20:33:48.303 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2362971701648 资源清理完成,当前连接数: 0 +2025-12-03 20:33:52.886 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2362971701840 +2025-12-03 20:33:52.886 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2362971701840 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 20:33:53.073 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=2362971701840,user_id=1001 +2025-12-03 20:33:53.074 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 2362971701840) +2025-12-03 20:33:57.278 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 所以保安 +2025-12-03 20:34:00.246 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 8fda7c09-2405-4370-97d6-796c6da856e6, 完整回复: 我的银行卡丢了,现在急着取钱,你们能帮帮我吗? +2025-12-03 20:34:02.239 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2362972209408 +2025-12-03 20:34:02.239 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2362972209408 已接受,等待身份信息(5秒超时),当前连接数: 2 +2025-12-03 20:34:02.289 | ERROR | audio_ai_chat.core.websocket_handler:connect:61 - 连接 2362972209408 首个包类型错误 +2025-12-03 20:34:02.290 | ERROR | audio_ai_chat.core.websocket_handler:handle_connection:286 - 连接 2362972209408 处理异常:连接 2362972209408 首个包类型错误 +2025-12-03 20:34:02.290 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2362972209408 资源清理完成,当前连接数: 1 +2025-12-03 20:34:02.338 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 我看一下 +2025-12-03 20:34:02.449 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2362972210944 +2025-12-03 20:34:02.450 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2362972210944 已接受,等待身份信息(5秒超时),当前连接数: 2 +2025-12-03 20:34:02.480 | ERROR | audio_ai_chat.core.websocket_handler:connect:61 - 连接 2362972210944 首个包类型错误 +2025-12-03 20:34:02.481 | ERROR | audio_ai_chat.core.websocket_handler:handle_connection:286 - 连接 2362972210944 处理异常:连接 2362972210944 首个包类型错误 +2025-12-03 20:34:02.481 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2362972210944 资源清理完成,当前连接数: 1 +2025-12-03 20:34:02.581 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2362972215696 +2025-12-03 20:34:02.582 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2362972215696 已接受,等待身份信息(5秒超时),当前连接数: 2 +2025-12-03 20:34:02.596 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=2362972215696,user_id=1001 +2025-12-03 20:34:02.597 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 2362972215696) +2025-12-03 20:34:03.375 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 8fda7c09-2405-4370-97d6-796c6da856e6, 完整回复: 快点,我真的很急! +2025-12-03 20:34:03.847 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 2362971701840 主动断开连接 +2025-12-03 20:34:03.847 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 2362972215696 主动断开连接 +2025-12-03 20:34:03.847 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2362971701840 资源清理完成,当前连接数: 1 +2025-12-03 20:34:03.848 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2362972215696 资源清理完成,当前连接数: 0 +2025-12-03 20:35:39.374 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 20:35:39.374 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 20:35:41.978 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=1650858901856 +2025-12-03 20:35:41.978 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 1650858901856 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 20:35:42.069 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=1650858901856,user_id=1001 +2025-12-03 20:35:42.069 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 1650858901856) +2025-12-03 20:35:45.433 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 喂喂喂 +2025-12-03 20:35:46.640 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 308e5aa9-5d90-4e96-83cd-8c5e7a263dda, 完整回复: 我的银行卡现在急着取钱,怎么办啊? +2025-12-03 20:35:51.579 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 1650858901856 主动断开连接 +2025-12-03 20:35:51.580 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 1650858901856 资源清理完成,当前连接数: 0 +2025-12-03 20:37:21.705 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 20:37:21.705 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 20:37:25.392 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2442869720928 +2025-12-03 20:37:25.392 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2442869720928 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 20:37:25.407 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=2442869720928,user_id=1001 +2025-12-03 20:37:25.408 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 2442869720928) +2025-12-03 20:37:30.635 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 喂喂喂 +2025-12-03 20:37:31.928 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 95e8ed04-5445-4326-89e2-d6d99aeeaf7e, 完整回复: 我的银行卡丢了,现在急着取钱,怎么办啊? +2025-12-03 20:37:33.097 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,谁不到这个 +2025-12-03 20:37:33.971 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 95e8ed04-5445-4326-89e2-d6d99aeeaf7e, 完整回复: 李,真的非常着急! +2025-12-03 20:37:36.712 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,我 +2025-12-03 20:37:37.590 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 95e8ed04-5445-4326-89e2-d6d99aeeaf7e, 完整回复: 快点帮我解决 +2025-12-03 20:37:38.201 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 如果说 +2025-12-03 20:37:39.100 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 95e8ed04-5445-4326-89e2-d6d99aeeaf7e, 完整回复: 我真了! +2025-12-03 20:37:41.684 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 嗯对对对 +2025-12-03 20:37:42.710 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 95e8ed04-5445-4326-89e2-d6d99aeeaf7e, 完整回复: 您着急,我帮您想想办法。 +2025-12-03 20:37:45.766 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 2442869720928 主动断开连接 +2025-12-03 20:37:45.767 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2442869720928 资源清理完成,当前连接数: 0 +2025-12-03 20:41:28.876 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 20:41:28.876 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 20:43:57.146 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 20:43:57.147 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 20:44:12.135 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=1926994774000 +2025-12-03 20:44:12.135 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 1926994774000 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 20:44:12.238 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=1926994774000,user_id=1001 +2025-12-03 20:44:12.238 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 1926994774000) +2025-12-03 20:44:12.258 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=1926994783984 +2025-12-03 20:44:12.259 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 1926994783984 已接受,等待身份信息(5秒超时),当前连接数: 2 +2025-12-03 20:44:12.273 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=1926994783984,user_id=1001 +2025-12-03 20:44:12.274 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 1926994783984) +2025-12-03 20:44:12.565 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=1926995220976 +2025-12-03 20:44:12.565 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 1926995220976 已接受,等待身份信息(5秒超时),当前连接数: 3 +2025-12-03 20:44:12.565 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=1926995223040 +2025-12-03 20:44:12.566 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 1926995223040 已接受,等待身份信息(5秒超时),当前连接数: 4 +2025-12-03 20:44:12.658 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=1926995223040,user_id=1001 +2025-12-03 20:44:12.659 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 1926995223040) +2025-12-03 20:44:12.659 | ERROR | audio_ai_chat.core.websocket_handler:handle_connection:286 - 连接 1926995223040 处理异常:获取ASR连接失败 +2025-12-03 20:44:12.659 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 1926995223040 资源清理完成,当前连接数: 3 +2025-12-03 20:44:13.050 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=1926995223328 +2025-12-03 20:44:13.051 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 1926995223328 已接受,等待身份信息(5秒超时),当前连接数: 4 +2025-12-03 20:44:17.575 | WARNING | audio_ai_chat.core.websocket_handler:connect:50 - 连接 1926995220976 身份校验超时 +2025-12-03 20:44:17.576 | ERROR | audio_ai_chat.core.websocket_handler:handle_connection:286 - 连接 1926995220976 处理异常:连接 1926995220976 身份校验超时 +2025-12-03 20:44:17.576 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 1926995220976 资源清理完成,当前连接数: 3 +2025-12-03 20:44:18.044 | WARNING | audio_ai_chat.core.websocket_handler:connect:50 - 连接 1926995223328 身份校验超时 +2025-12-03 20:44:18.044 | ERROR | audio_ai_chat.core.websocket_handler:handle_connection:286 - 连接 1926995223328 处理异常:连接 1926995223328 身份校验超时 +2025-12-03 20:44:18.045 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 1926995223328 资源清理完成,当前连接数: 2 +2025-12-03 20:44:33.421 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 1926994774000 主动断开连接 +2025-12-03 20:44:33.421 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 1926994783984 主动断开连接 +2025-12-03 20:44:33.422 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 1926994774000 资源清理完成,当前连接数: 1 +2025-12-03 20:44:33.422 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 1926994783984 资源清理完成,当前连接数: 0 +2025-12-03 20:44:35.795 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 20:44:35.796 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 20:44:38.857 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2203051737200 +2025-12-03 20:44:38.858 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2203051737200 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 20:44:38.969 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=2203051737200,user_id=1001 +2025-12-03 20:44:38.969 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 2203051737200) +2025-12-03 20:44:46.018 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 你前面都挺稳,这后边 g gs不稳呢 +2025-12-03 20:44:47.327 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 1010f3cf-8ff1-48b1-9c75-d16196d508f3, 完整回复: 我的,现在急着取钱,怎么办啊? +2025-12-03 20:44:48.170 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 不起 +2025-12-03 20:44:49.383 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 们,你 +2025-12-03 20:44:49.470 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 1010f3cf-8ff1-48b1-9c75-d16196d508f3, 完整回复: 那我能不能先取现金,再补办银行卡? +2025-12-03 20:44:50.637 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 1010f3cf-8ff1-48b1-9c75-d16196d508f3, 完整回复: 我姓李,真的非常着急,能帮我想想办法吗? +2025-12-03 20:44:53.257 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 啊 +2025-12-03 20:44:54.126 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 1010f3cf-8ff1-48b1-9c75-d16196d508f3, 完整回复: 我现金急用,卡丢了能先取钱吗? +2025-12-03 20:44:55.336 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,还你 +2025-12-03 20:44:56.361 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 1010f3cf-8ff1-48b1-9c75-d16196d508f3, 完整回复: 我现在必须取钱,卡丢了能 +2025-12-03 20:44:58.499 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 不是为为啥不播放 +2025-12-03 20:44:59.674 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 1010f3cf-8ff1-48b1-9c75-d16196d508f3, 完整回复: 我真急用钱,卡丢了能先取吗? +2025-12-03 20:44:59.886 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2203052344080 +2025-12-03 20:44:59.887 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2203052344080 已接受,等待身份信息(5秒超时),当前连接数: 2 +2025-12-03 20:45:01.811 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 他一个 +2025-12-03 20:45:02.872 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 1010f3cf-8ff1-48b1-9c75-d16196d508f3, 完整回复: 卡丢了,我现在必须取钱! +2025-12-03 20:45:02.904 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,但是 +2025-12-03 20:45:04.023 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 1010f3cf-8ff1-48b1-9c75-d16196d508f3, 完整回复: 卡丢了,我急用钱,能帮帮我吗? +2025-12-03 20:45:04.906 | WARNING | audio_ai_chat.core.websocket_handler:connect:50 - 连接 2203052344080 身份校验超时 +2025-12-03 20:45:04.907 | ERROR | audio_ai_chat.core.websocket_handler:handle_connection:286 - 连接 2203052344080 处理异常:连接 2203052344080 身份校验超时 +2025-12-03 20:45:04.907 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2203052344080 资源清理完成,当前连接数: 1 +2025-12-03 20:45:05.964 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 行行行 +2025-12-03 20:45:06.077 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 2203051737200 主动断开连接 +2025-12-03 20:45:06.078 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2203051737200 资源清理完成,当前连接数: 0 +2025-12-03 20:45:09.542 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 20:45:09.543 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 20:45:19.311 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2131877349792 +2025-12-03 20:45:19.311 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2131877349792 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 20:45:19.407 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=2131877349792,user_id=1001 +2025-12-03 20:45:19.407 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 2131877349792) +2025-12-03 20:45:25.399 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 播放不 +2025-12-03 20:45:26.582 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 我的银行卡丢了,现在急着取钱,怎么办啊? +2025-12-03 20:45:31.545 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 13min +2025-12-03 20:45:32.772 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 我姓李,真的非常着急,能快点帮我解决吗? +2025-12-03 20:45:34.885 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 没有,有什么问题 +2025-12-03 20:45:36.078 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 我银行卡丢了,现在急需取钱,能想想办法吗? +2025-12-03 20:45:38.327 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,那就是你保密可以吗 +2025-12-03 20:45:39.537 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 我保证保密,只希望尽快解决 +2025-12-03 20:45:41.313 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ?那是说 +2025-12-03 20:45:42.353 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 可以先挂失,再办张新卡? +2025-12-03 20:45:43.012 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 什么 +2025-12-03 20:45:44.157 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 你能快点处理吗?我真的非常 +2025-12-03 20:45:46.238 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 吃亏 +2025-12-03 20:45:47.268 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 我不想再拖了,能快点处理吗? +2025-12-03 20:45:53.888 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 没有电费,容费用没有改变,因为 +2025-12-03 20:45:54.810 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 我真的 +2025-12-03 20:45:55.848 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 我真没时间了,能赶紧帮我处理吗? +2025-12-03 20:45:56.040 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 我真的没,能快点帮我想办法吗? +2025-12-03 20:45:58.222 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 每天要进门 +2025-12-03 20:45:59.324 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 我每天都要用钱,能快点处理吗? +2025-12-03 20:46:00.781 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 这个人 +2025-12-03 20:46:01.817 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 我姓能直接找负责人处理吗? +2025-12-03 20:46:03.436 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,你不要你好,我们俩 +2025-12-03 20:46:04.359 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 回来了 +2025-12-03 20:46:04.658 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 我真的很急,能直接找领导吗? +2025-12-03 20:46:05.322 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 我银行卡丢了,现在急需取钱,能帮忙吗? +2025-12-03 20:46:05.347 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 嗯 +2025-12-03 20:46:06.401 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 能快解决吗?我真的急! +2025-12-03 20:46:08.463 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,就是过期了,在信用 +2025-12-03 20:46:11.059 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 我的卡过期了,现在急需用钱,怎么办? +2025-12-03 20:46:11.984 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,所以你把那个啥那个什么 +2025-12-03 20:46:12.975 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 你先把临时卡给我,我实在等不及了! +2025-12-03 20:46:13.751 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 体验 +2025-12-03 20:46:14.691 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 之后 +2025-12-03 20:46:15.160 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 我现在没时间体验,只想要个解决方案! +2025-12-03 20:46:15.780 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 之后我该怎么办?能告诉我步骤吗? +2025-12-03 20:46:16.825 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,尿奶奶被 +2025-12-03 20:46:19.007 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 别耽误时间了,快帮我处理! +2025-12-03 20:46:19.500 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 他给我们 +2025-12-03 20:46:20.612 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 他点帮我办新卡吗? +2025-12-03 20:46:21.565 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 我们在 +2025-12-03 20:46:22.491 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 物流卡 +2025-12-03 20:46:22.648 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 你们能快点处理我的业务吗? +2025-12-03 20:46:23.587 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 那个 +2025-12-03 20:46:23.674 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 我的丢了,能马上补办吗? +2025-12-03 20:46:25.344 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 那个卡对我很重要,能加急处理吗? +2025-12-03 20:46:25.514 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 我这个工具 +2025-12-03 20:46:26.431 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 啊嗯对 +2025-12-03 20:46:26.741 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 我的工具卡丢了,能赶紧补一张吗? +2025-12-03 20:46:27.617 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 我真的很急,能快点吗? +2025-12-03 20:46:28.019 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 哦 +2025-12-03 20:46:29.220 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 快点帮我处理,我真的等不了 +2025-12-03 20:46:30.004 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,前面两个 +2025-12-03 20:46:31.453 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 前面两位怎么还没办完?我真的很急! +2025-12-03 20:46:32.779 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 我说的是来 +2025-12-03 20:46:34.031 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 我说我急,能快点轮到我吗? +2025-12-03 20:46:36.988 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 在这边 +2025-12-03 20:46:38.134 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 我在这边等很久了,能快点吗? +2025-12-03 20:46:39.741 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,对不对 +2025-12-03 20:46:40.897 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 我急着用钱,这样处理对不对? +2025-12-03 20:46:40.942 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 嗯 +2025-12-03 20:46:42.059 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 我真的很着急,能加急处理吗? +2025-12-03 20:46:51.958 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,你就你也就会 +2025-12-03 20:46:53.111 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 你会不会快点帮我办?我急! +2025-12-03 20:46:54.466 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 有什么事 +2025-12-03 20:46:55.581 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 我卡丢了,现在必须取到钱! +2025-12-03 20:46:58.205 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,我的小孩 +2025-12-03 20:46:59.305 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 我小孩急着用钱,能快点处理吗? +2025-12-03 20:47:00.529 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,然后 +2025-12-03 20:47:01.594 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 然后我该怎么办?能告诉我吗? +2025-12-03 20:47:02.203 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 因为 +2025-12-03 20:47:03.221 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 因为太,能优先处理我吗? +2025-12-03 20:47:03.221 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 不知道 +2025-12-03 20:47:04.323 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 我真的很急,你能不能帮帮我? +2025-12-03 20:47:06.770 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 不同意,哎,这里 +2025-12-03 20:47:07.717 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 我急得没办法,必须现在解决! +2025-12-03 20:47:08.633 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 你不是 +2025-12-03 20:47:09.725 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 我不是来闹事的,只求快点解决! +2025-12-03 20:47:13.104 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 哎,你不是有这么一点 +2025-12-03 20:47:15.794 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,就这一点点吧 +2025-12-03 20:47:16.438 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 我真不是来闹的,只求快点处理! +2025-12-03 20:47:17.075 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 就这点时间了,能快点帮我吗? +2025-12-03 20:47:18.756 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,你在哪里 +2025-12-03 20:47:19.812 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 我在银行大厅,能快点帮我吗? +2025-12-03 20:47:20.257 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,你他还是 +2025-12-03 20:47:21.435 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 他还在后面拖着,我等不及了! +2025-12-03 20:47:21.677 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 这个应该是 +2025-12-03 20:47:22.865 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 最紧急的,能优先处理吗? +2025-12-03 20:47:24.287 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 我说怎么就退退了,你调了吗 +2025-12-03 20:47:25.720 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 我卡丢了,能调出账户信息先处理吗? +2025-12-03 20:47:29.350 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ?你这不连不能买 a ts吗?不是连的,是是你那个 +2025-12-03 20:47:30.842 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 16c9ab3c-94f9-46a1-bcac-bb688aee3654, 完整回复: 我卡丢了,现在必须取钱,能先帮我查账户吗? +2025-12-03 20:47:31.172 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 2131877349792 主动断开连接 +2025-12-03 20:47:31.173 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2131877349792 资源清理完成,当前连接数: 0 +2025-12-03 20:47:46.858 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2131877351232 +2025-12-03 20:47:46.858 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2131877351232 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 20:47:46.881 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=2131877351232,user_id=1001 +2025-12-03 20:47:46.882 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 2131877351232) +2025-12-03 20:47:50.818 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 给我们评价一下,是你好你好 +2025-12-03 20:47:52.174 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 9ae0862c-2956-415d-b8aa-dd737fefc4e2, 完整回复: 我的银行卡丢了,现在急着用钱,能帮帮我吗? +2025-12-03 20:47:52.378 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 嗯 +2025-12-03 20:47:53.433 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 9ae0862c-2956-415d-b8aa-dd737fefc4e2, 完整回复: 我姓李,真的非常着急,能优先处理一下吗? +2025-12-03 20:47:56.651 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 嗯,这人卡死机了 +2025-12-03 20:47:57.786 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 9ae0862c-2956-415d-b8aa-dd737fefc4e2, 完整回复: 你们能不能快点啊,我真的非常着急! +2025-12-03 20:47:59.950 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 2131877351232 主动断开连接 +2025-12-03 20:47:59.951 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2131877351232 资源清理完成,当前连接数: 0 +2025-12-03 20:48:08.364 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2131877361024 +2025-12-03 20:48:08.365 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2131877361024 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 20:48:08.422 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=2131877361024,user_id=1001 +2025-12-03 20:48:08.423 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 2131877361024) +2025-12-03 20:48:27.524 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,明天就可以。你要压力很大。明天写完了吧,我说你们作业写完了是吗 +2025-12-03 20:48:28.315 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ?咋又不说话了呢?哪个人 +2025-12-03 20:48:28.663 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 2131877361024 主动断开连接 +2025-12-03 20:48:28.664 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2131877361024 资源清理完成,当前连接数: 0 +2025-12-03 20:48:28.705 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: c2d50693-e80e-4308-95a7-3f1c28052f64, 完整回复: 我的银行卡丢了,现在急需取钱,有什么办法吗? +2025-12-03 20:48:30.516 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 13b02342-18be-4c86-b847-de3327d10fbd, 完整回复: 我的银行卡丢了,现在急着用钱,你们能帮帮我吗? +2025-12-03 20:48:33.943 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2131877360256 +2025-12-03 20:48:33.944 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2131877360256 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 20:48:33.963 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=2131877360256,user_id=1001 +2025-12-03 20:48:33.963 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 2131877360256) +2025-12-03 20:48:36.085 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 现在就是这个 +2025-12-03 20:48:37.196 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: d79f4426-1fb0-43d2-a7de-98fa683e5dec, 完整回复: 我的,现在急着取钱,怎么办啊? +2025-12-03 20:48:48.004 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 123 按了之后,你要不跟他说20 秒,他他妈自己就摁他了。对啊,我知道的不是你,他得是那个以合同为主,自己说一直发的 +2025-12-03 20:48:48.973 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: d79f4426-1fb0-43d2-a7de-98fa683e5dec, 完整回复: 我赶时间,快点帮我解决! +2025-12-03 20:48:56.433 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 。你在你在录的时候会一直静音,然后必须在那个啥,然后不给我传的话,会有这种情况,没有,一直传啊 +2025-12-03 20:48:57.950 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: d79f4426-1fb0-43d2-a7de-98fa683e5dec, 完整回复: 我真没时间了,能快点吗? +2025-12-03 20:48:57.991 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 2131877360256 主动断开连接 +2025-12-03 20:48:57.992 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2131877360256 资源清理完成,当前连接数: 0 +2025-12-03 20:50:51.492 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2131877076928 +2025-12-03 20:50:51.492 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2131877076928 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 20:50:51.693 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=2131877076928,user_id=1001 +2025-12-03 20:50:51.694 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 2131877076928) +2025-12-03 20:50:55.175 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 。不是,那你们去我去过了,到时候你一般都不算 +2025-12-03 20:50:56.288 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 你好,你好 +2025-12-03 20:50:56.464 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: f6ca30e3-0cb8-48b9-94fb-8ba4b1179610, 完整回复: 我的,现在急着取钱,你们能想想办法吗? +2025-12-03 20:50:57.489 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 5a4f0d04-4559-4fff-980e-b0733e80bd07, 完整回复: 你好,你好,请问能帮我查一下账户余额吗? +2025-12-03 20:50:59.026 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,为什么只能一次啊 +2025-12-03 20:51:00.474 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 5a4f0d04-4559-4fff-980e-b0733e80bd07, 完整回复: 我急着用钱,但银行卡丢了,能不能先 +2025-12-03 20:51:03.375 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,关闭 +2025-12-03 20:51:04.348 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 5a4f0d04-4559-4fff-980e-b0733e80bd07, 完整回复: 我还有,不能取消,必须马上处理! +2025-12-03 20:51:05.510 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 嗯啊 +2025-12-03 20:51:05.533 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 2131877076928 主动断开连接 +2025-12-03 20:51:05.534 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2131877076928 资源清理完成,当前连接数: 0 +2025-12-03 20:51:08.381 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 20:51:08.382 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 20:51:08.636 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2133230799312 +2025-12-03 20:51:08.637 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2133230799312 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 20:51:08.651 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=2133230799312,user_id=1001 +2025-12-03 20:51:08.652 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 2133230799312) +2025-12-03 20:51:09.085 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=2133230810208 +2025-12-03 20:51:09.086 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 2133230810208 已接受,等待身份信息(5秒超时),当前连接数: 2 +2025-12-03 20:51:09.100 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=2133230810208,user_id=1001 +2025-12-03 20:51:09.100 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 2133230810208) +2025-12-03 20:51:09.980 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 2133230810208 主动断开连接 +2025-12-03 20:51:09.981 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2133230810208 资源清理完成,当前连接数: 1 +2025-12-03 20:51:12.130 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 2133230799312 主动断开连接 +2025-12-03 20:51:12.131 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 2133230799312 资源清理完成,当前连接数: 0 +2025-12-03 20:51:14.338 | INFO | audio_ai_chat.core.connection:get_instance:378 - ConnectionManager 全局单例初始化成功 +2025-12-03 20:51:14.338 | INFO | audio_ai_chat.core.websocket_handler:initialize:35 - WebSocketConnectionManager 初始化成功 +2025-12-03 20:51:17.798 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:180 - 新WebSocket连接:client_id=1957878090192 +2025-12-03 20:51:17.799 | INFO | audio_ai_chat.core.websocket_handler:connect:41 - 连接 1957878090192 已接受,等待身份信息(5秒超时),当前连接数: 1 +2025-12-03 20:51:18.014 | INFO | audio_ai_chat.core.connection:create_or_reconnect_context:453 - 创建新上下文:client_id=1957878090192,user_id=1001 +2025-12-03 20:51:18.014 | INFO | audio_ai_chat.core.websocket_handler:connect:98 - 用户 1001(测试用户)身份校验通过(client_id: 1957878090192) +2025-12-03 20:51:21.192 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): hellohello +2025-12-03 20:51:22.196 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): 么了 +2025-12-03 20:51:22.424 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 24c49ade-a2a9-41a6-ae6f-67a5e919fc96, 完整回复: 你好,我的银行卡丢了,能帮我取钱吗? +2025-12-03 20:51:23.093 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 73ee6440-7817-4174-9d7f-c95b5ff8df01, 完整回复: 李阿姨,我银行卡丢了急着取钱怎么办? +2025-12-03 20:51:27.682 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ?这天的最美的宿 +2025-12-03 20:51:28.674 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 73ee6440-7817-4174-9d7f-c95b5ff8df01, 完整回复: 我银行卡丢了,急需取钱,能帮帮我吗? +2025-12-03 20:51:31.322 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,对不对 +2025-12-03 20:51:32.544 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 73ee6440-7817-4174-9d7f-c95b5ff8df01, 完整回复: 我银行卡丢了,现在急需取钱,有什么办法吗? +2025-12-03 20:51:36.537 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ?不对劲,你这音频找回来了,为什么这么慢呢 +2025-12-03 20:51:38.380 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 73ee6440-7817-4174-9d7f-c95b5ff8df01, 完整回复: 我银行卡丢了,急需取钱,你得帮我想办法! +2025-12-03 20:51:38.801 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ?你不能救病 +2025-12-03 20:51:39.932 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 73ee6440-7817-4174-9d7f-c95b5ff8df01, 完整回复: 丢了,现在必须取到钱,你得帮帮我! +2025-12-03 20:51:41.034 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:158 - 调用大模型 - 用户(): ,我不是 +2025-12-03 20:51:42.151 | INFO | audio_ai_chat.core.websocket_handler:call_llm_and_send:170 - 大模型回复完成 - 会话ID: 73ee6440-7817-4174-9d7f-c95b5ff8df01, 完整回复: 我银行卡丢了,现在急需用钱,你得解决 +2025-12-03 20:51:42.646 | INFO | audio_ai_chat.core.websocket_handler:recv_frontend_data:248 - 前端 1957878090192 主动断开连接 +2025-12-03 20:51:42.646 | INFO | audio_ai_chat.core.websocket_handler:handle_connection:317 - 连接 1957878090192 资源清理完成,当前连接数: 0 diff --git a/audio_ai_chat/test.py b/audio_ai_chat/test.py new file mode 100644 index 0000000..ac50fe5 --- /dev/null +++ b/audio_ai_chat/test.py @@ -0,0 +1,238 @@ +# audio_ai_chat/websocket/manager.py +from fastapi import WebSocket +from typing import Dict, Optional +from datetime import datetime +import base64 +from audio_ai_chat.asr.base import ASRBase, ASRResultCallback +from audio_ai_chat.asr.asr_manager import ASRManager +from audio_ai_chat.websocket.connection_context import ConnectionManager, ConnectionContext # 导入全局单例类 +from audio_ai_chat.config.logger import logger + +class WebSocketConnectionManager: + """全局唯一的WebSocket连接处理器(管理WebSocket连接生命周期)""" + def __init__(self): + self.asr_conn_map: Dict[str, Optional[object]] = {} # key=client_id,value=ASR连接 + # 不实例化新的ConnectionManager,而是使用全局单例 + self.connection_manager: Optional[ConnectionManager] = None + + async def initialize(self): + """初始化:获取ConnectionManager全局单例(在FastAPI启动时调用)""" + self.connection_manager = await ConnectionManager.get_instance() + logger.info("WebSocketConnectionManager 初始化成功(绑定全局ConnectionManager)") + + async def handle_connection(self, websocket: WebSocket): + """处理单个WebSocket连接的完整生命周期""" + # 校验ConnectionManager是否初始化 + if not self.connection_manager: + await websocket.accept() + await websocket.send_text("服务未初始化完成,请稍后重试") + await websocket.close() + logger.error("WebSocketConnectionManager 未初始化,拒绝连接") + return + + # 1. 接受连接,生成client_id(用字符串类型,避免int溢出) + await websocket.accept() + client_id = str(id(websocket)) # client_id为字符串,与ConnectionManager的key类型一致 + logger.info(f"新WebSocket连接:client_id={client_id}") + + try: + # 2. 创建连接上下文(通过全局ConnectionManager) + context = await self.connection_manager.create_connection(client_id=client_id) + if not context: + await websocket.send_text("连接上下文创建失败") + await websocket.close() + return + + # 3. 获取ASR实例 + asr_client = ASRManager.get_instance() + if not asr_client or not ASRManager.is_available(): + await websocket.send_json({ + "type": "error", + "message": "ASR服务未初始化,无法提供转写服务", + "timestamp": datetime.utcnow().isoformat() + "Z" + }) + await self.connection_manager.remove_connection(client_id=client_id) + await websocket.close() + return + + # 4. 获取ASR连接 + asr_conn = await asr_client.get_connection() + if not asr_conn: + await websocket.send_json({ + "type": "error", + "message": "ASR无空闲连接,连接失败", + "timestamp": datetime.utcnow().isoformat() + "Z" + }) + await self.connection_manager.remove_connection(client_id=client_id) + await websocket.close() + return + self.asr_conn_map[client_id] = asr_conn + + # 5. 定义ASR结果回调(绑定当前上下文) + async def asr_callback(result: Dict[str, Any]): + if not context.is_active: + logger.warning(f"连接已关闭,忽略ASR结果:client_id={client_id}") + return + # 处理ASR结果并存入上下文 + context.add_asr_result(result) + # 推送给前端 + if result.get("error"): + await websocket.send_json({ + "type": "asr_error", + "message": result["error"], + "timestamp": datetime.utcnow().isoformat() + "Z" + }) + else: + await websocket.send_json({ + "type": "asr_progress" if not result["is_final"] else "asr_final", + "text": result["text"], + "is_final": result["is_final"], + "timestamp": result.get("timestamp", datetime.utcnow().isoformat() + "Z") + }) + + # 6. 启动ASR通信 + asr_task = asyncio.create_task( + asr_client.start_communication(conn=asr_conn, callback=asr_callback) + ) + + # 7. 循环接收前端数据 + while context.is_active: + try: + # 假设前端发送JSON格式数据(区分音频/文本/用户信息) + data = await websocket.receive_json() + data_type = data.get("type") + + # 处理用户信息(登录后发送) + if data_type == "user_info": + try: + token = data.get("token") + user_id = data.get("user_id") + name = data.get("name", "匿名用户") + context.set_user_info(token=token, user_id=user_id, name=name) + await websocket.send_json({ + "type": "info", + "message": "用户信息设置成功", + "timestamp": datetime.utcnow().isoformat() + "Z" + }) + except Exception as e: + err_msg = f"用户信息设置失败:{str(e)}" + context.add_system_message(err_msg) + await websocket.send_json({ + "type": "error", + "message": err_msg, + "timestamp": datetime.utcnow().isoformat() + "Z" + }) + + # 处理Base64编码的音频数据 + elif data_type == "audio_data": + audio_base64 = data.get("audio_data") + if not audio_base64: + continue + try: + audio_data = base64.b64decode(audio_base64) + success = await asr_client.push_audio(asr_conn, audio_data) + if not success: + await websocket.send_json({ + "type": "warning", + "message": "ASR音频队列已满,部分数据丢失", + "timestamp": datetime.utcnow().isoformat() + "Z" + }) + except Exception as e: + err_msg = f"音频解码失败:{str(e)}" + logger.error(f"client_id={client_id},{err_msg}") + await websocket.send_json({ + "type": "error", + "message": err_msg, + "timestamp": datetime.utcnow().isoformat() + "Z" + }) + + # 处理纯文本输入 + elif data_type == "text_input": + text = data.get("text", "").strip() + if text: + context.add_chat_history({ + "role": "user", + "content": text, + "source": "text", + "asr_metadata": None + }) + await websocket.send_json({ + "type": "info", + "message": f"已接收文本:{text}", + "timestamp": datetime.utcnow().isoformat() + "Z" + }) + + # 处理大模型请求 + elif data_type == "request_llm": + if context.is_processing: + await websocket.send_json({ + "type": "warning", + "message": "正在处理上一个请求,请稍后再试", + "timestamp": datetime.utcnow().isoformat() + "Z" + }) + continue + # 获取对话历史 + chat_history = context.get_chat_history(limit=20) + logger.debug(f"请求大模型:client_id={client_id},历史条数={len(chat_history)}") + # 模拟大模型调用(实际替换为真实LLM调用) + context.is_processing = True + try: + # llm_response = await context.llm_session.generate(chat_history=chat_history) + llm_response = f"模拟大模型回复:已收到你的{len(chat_history)}条对话历史" + context.add_llm_result(llm_response) + await websocket.send_json({ + "type": "llm_response", + "text": llm_response, + "timestamp": datetime.utcnow().isoformat() + "Z" + }) + except Exception as e: + err_msg = f"大模型调用失败:{str(e)}" + context.add_system_message(err_msg) + await websocket.send_json({ + "type": "error", + "message": err_msg, + "timestamp": datetime.utcnow().isoformat() + "Z" + }) + finally: + context.is_processing = False + + # 未知数据类型 + else: + err_msg = f"未知数据类型:{data_type}" + await websocket.send_json({ + "type": "error", + "message": err_msg, + "timestamp": datetime.utcnow().isoformat() + "Z" + }) + + except Exception as e: + # 捕获前端发送数据异常(如断开连接) + logger.error(f"接收前端数据异常:client_id={client_id},error={str(e)}") + break + + except Exception as e: + # 其他异常 + err_msg = f"连接处理异常:{str(e)}" + logger.error(f"client_id={client_id},{err_msg}") + await websocket.send_json({ + "type": "error", + "message": err_msg, + "timestamp": datetime.utcnow().isoformat() + "Z" + }) + finally: + # 8. 资源清理 + # 取消ASR任务 + asr_task.cancel() + try: + await asr_task + except asyncio.CancelledError: + pass + # 释放ASR连接 + if client_id in self.asr_conn_map: + asr_conn = self.asr_conn_map.pop(client_id) + await asr_client.release_connection(asr_conn) + # 移除连接上下文 + await self.connection_manager.remove_connection(client_id=client_id) + # 关闭WebSocket + await websocket.close() + logger.info(f"WebSocket连接关闭:client_id={client_id}") \ No newline at end of file diff --git a/python/.idea/python.iml b/python/.idea/python.iml index b4afc04..accfc5e 100644 --- a/python/.idea/python.iml +++ b/python/.idea/python.iml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/python/__pycache__/asr_client.cpython-310.pyc b/python/__pycache__/asr_client.cpython-310.pyc index 228622c..05443d9 100644 Binary files a/python/__pycache__/asr_client.cpython-310.pyc and b/python/__pycache__/asr_client.cpython-310.pyc differ diff --git a/python/__pycache__/frontend_ws.cpython-310.pyc b/python/__pycache__/frontend_ws.cpython-310.pyc index 1bd9816..59725ad 100644 Binary files a/python/__pycache__/frontend_ws.cpython-310.pyc and b/python/__pycache__/frontend_ws.cpython-310.pyc differ diff --git a/python/__pycache__/llm_client.cpython-310.pyc b/python/__pycache__/llm_client.cpython-310.pyc index 8d9237e..4ed2a28 100644 Binary files a/python/__pycache__/llm_client.cpython-310.pyc and b/python/__pycache__/llm_client.cpython-310.pyc differ diff --git a/python/__pycache__/session_manager.cpython-310.pyc b/python/__pycache__/session_manager.cpython-310.pyc index cc63190..76cf881 100644 Binary files a/python/__pycache__/session_manager.cpython-310.pyc and b/python/__pycache__/session_manager.cpython-310.pyc differ diff --git a/python/__pycache__/tts_client.cpython-310.pyc b/python/__pycache__/tts_client.cpython-310.pyc index ae41792..31a62ac 100644 Binary files a/python/__pycache__/tts_client.cpython-310.pyc and b/python/__pycache__/tts_client.cpython-310.pyc differ diff --git a/python/__pycache__/ws_message_manager.cpython-310.pyc b/python/__pycache__/ws_message_manager.cpython-310.pyc index a8a33e0..47faf8a 100644 Binary files a/python/__pycache__/ws_message_manager.cpython-310.pyc and b/python/__pycache__/ws_message_manager.cpython-310.pyc differ diff --git a/python/protocols/__pycache__/__init__.cpython-310.pyc b/python/protocols/__pycache__/__init__.cpython-310.pyc index eab35ec..171fa20 100644 Binary files a/python/protocols/__pycache__/__init__.cpython-310.pyc and b/python/protocols/__pycache__/__init__.cpython-310.pyc differ diff --git a/python/protocols/__pycache__/protocols.cpython-310.pyc b/python/protocols/__pycache__/protocols.cpython-310.pyc index 7489c85..39e4d03 100644 Binary files a/python/protocols/__pycache__/protocols.cpython-310.pyc and b/python/protocols/__pycache__/protocols.cpython-310.pyc differ diff --git a/tra-app/.env.development b/tra-app/.env.development new file mode 100644 index 0000000..9a63819 --- /dev/null +++ b/tra-app/.env.development @@ -0,0 +1,39 @@ +# 开发环境 +ENV = 'development' +# 'development' + + +# VITE_APP_BASE_API_Url = 'https://aits.jlbank.com.cn:7001' + +VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7002' + + + +# VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7002' + + +# VITE_APP_BASE_API_Url = 'http://192.168.247.200' +# VITE_APP_BASE_API_Url = 'http://aitscdn.jlbank.com.cn:7001' +# VITE_APP_BASE_API_Url = 'http://192.168.108.129' +# dev +# VITE_APP_BASE_API_Url = 'http://25.18.122.65:7001' +# sit + +# VITE_APP_BASE_API_Url = 'http://25.18.122.91:9786' + +# UAT +# VITE_APP_BASE_API_Url = 'http://25.18.122.78:9786' + +# DEV +# app入口 + +# VITE_APP_BASE_API_Url = 'http://25.18.122.65:7001' +# VITE_APP_BASE_API_Url = 'http://25.16.122.91:9786' +# VITE_APP_BASE_API_Url = 'http://25.18.122.66:9786' + +# h5专用地址x2 +# VITE_APP_BASE_H5_API_Url_TRAAPP = 'http://25.64.32.150:9601' +# dev +#VITE_APP_BASE_H5_API_Url = 'http://25.18.122.65:7001' +# sit +# VITE_APP_BASE_H5_API_Url = 'http://25.18.122.91:9786' \ No newline at end of file diff --git a/tra-app/.env.development.rd b/tra-app/.env.development.rd new file mode 100644 index 0000000..ca382f2 --- /dev/null +++ b/tra-app/.env.development.rd @@ -0,0 +1,8 @@ +# 任东专用开发环境 +ENV = 'development.rd' +# 'development.rd' + +#VITE_APP_BASE_API_Url = 'http://25.18.122.65:7001' +VITE_APP_BASE_API_Url = 'http://25.18.122.91:9786' + +VITE_APP_BASE_H5_API_Url_TRAEXAM = 'http://25.64.32.154:9604' diff --git a/tra-app/.env.development.ty b/tra-app/.env.development.ty new file mode 100644 index 0000000..4618dba --- /dev/null +++ b/tra-app/.env.development.ty @@ -0,0 +1,9 @@ +# 田岩开发环境 +ENV = 'development.ty' +# 'development.ty' + +VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7001' + +# VITE_APP_BASE_H5_API_Url_TRAEXAM = 'http://192.168.108.129' + + diff --git a/tra-app/.env.development.yh b/tra-app/.env.development.yh new file mode 100644 index 0000000..c56d90f --- /dev/null +++ b/tra-app/.env.development.yh @@ -0,0 +1,14 @@ +# 杨航开发环境 +ENV = 'development.yh' +# 'development.yh' + +# VITE_APP_BASE_H5_API_Url = 'http://25.18.122.65:7001' + +VITE_APP_BASE_H5_API_Url = 'http://25.18.122.91:9786' +# VITE_APP_BASE_H5_API_Url = 'http://25.18.122.78:9786' +# VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7002' +# VITE_APP_BASE_H5_API_Url_TRAEXAM = 'http://25.64.32.154:9604' +# VITE_APP_BASE_H5_API_Url_TRAPRACTICE = 'http://25.64.32.156:9603' +# VITE_APP_BASE_H5_API_Url_TRAAPP = 'http://25.64.32.154:9601' +# VITE_APP_BASE_H5_API_Url_TRAAPP = 'http://25.64.32.158:9601' + diff --git a/tra-app/.env.development_h5 b/tra-app/.env.development_h5 new file mode 100644 index 0000000..175c581 --- /dev/null +++ b/tra-app/.env.development_h5 @@ -0,0 +1,5 @@ +# 开发环境 +ENV = 'development' +# 'development' + +VITE_APP_BASE_API_Url = '' \ No newline at end of file diff --git a/tra-app/.env.production b/tra-app/.env.production new file mode 100644 index 0000000..143ff54 --- /dev/null +++ b/tra-app/.env.production @@ -0,0 +1,5 @@ +# 生产环境 +ENV = 'production' + +# base api +VITE_APP_BASE_API_Url = 'https://aits.jlbank.com.cn:7001' \ No newline at end of file diff --git a/tra-app/.env.sit b/tra-app/.env.sit new file mode 100644 index 0000000..8e8ed9e --- /dev/null +++ b/tra-app/.env.sit @@ -0,0 +1,5 @@ +# SIT环境 +ENV = 'sit' + +# base api +VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7002' diff --git a/tra-app/.env.uat b/tra-app/.env.uat new file mode 100644 index 0000000..865f000 --- /dev/null +++ b/tra-app/.env.uat @@ -0,0 +1,5 @@ +# UAT环境 +ENV = 'uat' + +# base api +VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7003' \ No newline at end of file diff --git a/tra-app/.gitignore b/tra-app/.gitignore new file mode 100644 index 0000000..a908cd3 --- /dev/null +++ b/tra-app/.gitignore @@ -0,0 +1,26 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* +package-lock +package-lock.json +node_modules +.DS_Store +dist +unpackage +*.local +build +release + +# Editor directories and files +.idea +.svn +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? \ No newline at end of file diff --git a/tra-app/README.en.md b/tra-app/README.en.md new file mode 100644 index 0000000..cb85303 --- /dev/null +++ b/tra-app/README.en.md @@ -0,0 +1,9 @@ +npm install --registry=http://25.12.10.69:8081/repository/aliyun-npm/ + +npm 淘宝源下载 :npm config set registry https://registry.npmmirror.com + +npm config set registry http://25.12.10.69:8081/repository/aliyun-npm/ + + +npm install --registry=https://registry.npmmirror.com + diff --git a/tra-app/index.html b/tra-app/index.html new file mode 100644 index 0000000..fb81ddc --- /dev/null +++ b/tra-app/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + +
+ + + diff --git a/tra-app/package.json b/tra-app/package.json new file mode 100644 index 0000000..b9725b1 --- /dev/null +++ b/tra-app/package.json @@ -0,0 +1,57 @@ +{ + "name": "tra-app", + "version": "0.0.1", + "description": "", + "scripts": { + "dev:h5": "uni", + "dev:rd": "uni -p h5 --mode development.rd", + "dev:yh": "uni -p h5 --mode development.yh", + "dev:ty": "uni -p h5 --mode development.ty", + "dev:h5:sit": "uni -p h5 --mode sit", + "dev:h5:uat": "uni -p h5 --mode uat", + "dev:h5:production": "uni -p h5 --mode production", + "build:app-plus:dev": "uni build -p app-plus --mode development", + "build:app-plus:develop": "uni build -p app-plus --mode development", + "build:app-plus:sit": "uni build -p app-plus --mode sit", + "build:app-plus:uat": "uni build -p app-plus --mode uat", + "build:app-plus:prod": "uni build -p app-plus", + "build:h5:dev": "uni build -p h5 --mode development_h5", + "build:h5": "uni build" + }, + "dependencies": { + "@dcloudio/uni-app": "3.0.0-4060620250520001", + "@dcloudio/uni-app-harmony": "3.0.0-4060620250520001", + "@dcloudio/uni-app-plus": "3.0.0-4060620250520001", + "@dcloudio/uni-components": "3.0.0-4060620250520001", + "@dcloudio/uni-h5": "3.0.0-4060620250520001", + "@dcloudio/uni-mp-alipay": "3.0.0-4060620250520001", + "@dcloudio/uni-mp-baidu": "3.0.0-4060620250520001", + "@dcloudio/uni-mp-harmony": "3.0.0-4060620250520001", + "@dcloudio/uni-mp-jd": "3.0.0-4060620250520001", + "@dcloudio/uni-mp-kuaishou": "3.0.0-4060620250520001", + "@dcloudio/uni-mp-lark": "3.0.0-4060620250520001", + "@dcloudio/uni-mp-qq": "3.0.0-4060620250520001", + "@dcloudio/uni-mp-toutiao": "3.0.0-4060620250520001", + "@dcloudio/uni-mp-weixin": "3.0.0-4060620250520001", + "@dcloudio/uni-mp-xhs": "3.0.0-4060620250520001", + "@dcloudio/uni-quickapp-webview": "3.0.0-4060620250520001", + "crypto-js": "^3.1.9-1", + "dompurify": "^3.3.0", + "markdown-it": "^14.1.0", + "pinia": "^2.3.1", + "terser": "^5.42.0", + "uuid": "^11.1.0", + "vue": "^3.5.11", + "vue-i18n": "^9.1.9" + }, + "devDependencies": { + "@dcloudio/types": "^3.4.8", + "@dcloudio/uni-automator": "3.0.0-4060620250520001", + "@dcloudio/uni-cli-shared": "3.0.0-4060620250520001", + "@dcloudio/uni-stacktracey": "3.0.0-4060620250520001", + "@dcloudio/vite-plugin-uni": "3.0.0-4060620250520001", + "@vue/runtime-core": "^3.4.21", + "sass": "1.77.0", + "vite": "5.2.8" + } +} diff --git a/tra-app/shims-uni.d.ts b/tra-app/shims-uni.d.ts new file mode 100644 index 0000000..ed4adcf --- /dev/null +++ b/tra-app/shims-uni.d.ts @@ -0,0 +1,10 @@ +/// +import 'vue' + +declare module '@vue/runtime-core' { + type Hooks = App.AppInstance & Page.PageInstance; + + interface ComponentCustomOptions extends Hooks { + + } +} diff --git a/tra-app/src/App.vue b/tra-app/src/App.vue new file mode 100644 index 0000000..63d8caf --- /dev/null +++ b/tra-app/src/App.vue @@ -0,0 +1,153 @@ + + + diff --git a/tra-app/src/androidPrivacy.json b/tra-app/src/androidPrivacy.json new file mode 100644 index 0000000..3a67d53 --- /dev/null +++ b/tra-app/src/androidPrivacy.json @@ -0,0 +1,38 @@ +{ + "version": "1", + "prompt": "template", + "title": "个人信息保护提示", + "message": "欢迎使用吉AI学!
  请你务必审慎阅读、充分理解《用户隐私协议》各条款,帮助您了解我们为您提供的服务、我们如何处理个人信息以及您享有的权利。我们会严格按照相关法律法规要求,采取各种安全措施来保护您的个人信息。
  如果你同意,请点击下面按钮开始接受我们的服务。
1.为了保障软件的安全运行和账户安全,我们会申请手机您的设备信息、IP地址、WLAN MAC地址。
2.上传或拍摄图片,需要使用您的媒体影音、图片、视频、音频、相机、等权限。
3.为了实现AI问答、课程学习等APP内功能,我们需要使用您的麦克风权限。", + "buttonAccept": "同意并接受", + "buttonRefuse": "暂不同意", + "hrefLoader": "system", + "backToExit":"true", + "second": { + "title": "确认提示", + "message": "  进入应用前,你需先同意《用户隐私协议》,否则将退出应用。", + "buttonAccept": "同意并继续", + "buttonRefuse": "退出应用" + }, + "disagreeMode":{ + "support": false, + "loadNativePlugins": false, + "visitorEntry": false, + "showAlways": false + }, + "styles": { + "backgroundColor": "#fff", + "borderRadius":"5px", + "title": { + "color": "#000" + }, + "buttonAccept": { + "color": "#F91B59" + }, + "buttonRefuse": { + "color": "#333" + }, + "buttonVisitor": { + "color": "#00ffff" + } + } +} diff --git a/tra-app/src/api/analytics.js b/tra-app/src/api/analytics.js new file mode 100644 index 0000000..ddf205f --- /dev/null +++ b/tra-app/src/api/analytics.js @@ -0,0 +1,48 @@ +import request from '@/api/request' + + +// 1.本月/本年/累计 学习信息查询接口 +export const queryStudyStatistics = (data) => { + return request({ + url: '/traapp/traStudyAnalysis/queryStudyStatistics', + method: 'post', + data + }); +}; +// 课程推荐查询接口 +export const queryRecmdCrsInfo = (data) => { + return request({ + url: '/traapp/traStudyAnalysis/queryRecmdCrsInfo', + method: 'post', + data + }); +}; +// 4.维度分析查询接口 +export const queryDimesionInfo = (data) => { + return request({ + url: '/traapp/traStudyAnalysis/queryDimesionInfo', + method: 'post', + timeout: 60000, + data + }); +}; + +// 5.学习习惯查询接口 +export const queryHabitInfo = (data) => { + return request({ + url: '/traapp/traStudyAnalysis/queryHabitInfo', + method: 'post', + timeout: 60000, + data + }); +}; + +// 6.学习建议查询接口 +export const querySuggestInfo = (data) => { + return request({ + url: '/traapp/traStudyAnalysis/querySuggestInfo', + method: 'post', + timeout: 60000, + data + }); +}; diff --git a/tra-app/src/api/arraybuffer.js b/tra-app/src/api/arraybuffer.js new file mode 100644 index 0000000..fd835f8 --- /dev/null +++ b/tra-app/src/api/arraybuffer.js @@ -0,0 +1,90 @@ + +import { getToken } from '@/common/common.js' +import { get_base_url } from '@/api/request' + +const base_url = get_base_url(); +const token = getToken(); +// 基础配置 +const baseConfig = { + enableChunked: true, // 必须开启分块传输 + responseType: 'arraybuffer', // 微信小程序需用arraybuffer + timeout: 30000 // 超时时间延长 +} + +// 流式请求核心实现 +function streamRequest(options) { + const { + url, + method = 'GET', + data, + headers = {}, + onChunk, + onComplete, + onError + } = options + + // headers['Accept'] = 'text/event-stream' + headers['Content-Type'] = 'text/event-stream' + headers['summary'] = token + + // 创建请求任务 + const requestTask = uni.request({ + url, + method, + data, + header: headers, + responseType: 'arraybuffer', + ...baseConfig, + success: (res) => { + console.log(res.data) + onComplete?.(res) + }, + fail: (err) => { + onError?.(err) + } + }) + console.log(requestTask) + // 流式数据处理器 + let bufferCache = '' + requestTask.onChunkReceived((res) => { + try { + // ArrayBuffer转字符串(兼容多平台) + const uint8Array = new Uint8Array(res.data) + const chunkText = bufferCache + + String.fromCharCode.apply(null, uint8Array) + + // 处理SSE格式数据(data:开头) + const events = chunkText.split('\n\n') + bufferCache = events.pop() || '' // 缓存不完整数据 + + events.forEach(event => { + if (event.startsWith('data:')) { + onChunk?.(event.substring(5).trim()) + } + }) + } catch (e) { + onError?.(e) + } + }) + + return { + abort: () => requestTask.abort() + } +} + +// 使用示例 +export const demoUsage = (data) => { + const controller = streamRequest({ + url: `${base_url}/trastudy/intgask/askAbout`, + data: data, + onChunk: (chunk) => { + console.log('收到数据块:', chunk) + // 实时更新UI逻辑 + }, + onComplete: () => console.log('传输完成'), + onError: (err) => console.error('错误:', err) + }) + + // 需要中断时调用 + // controller.abort() +} \ No newline at end of file diff --git a/tra-app/src/api/charts.js b/tra-app/src/api/charts.js new file mode 100644 index 0000000..877adac --- /dev/null +++ b/tra-app/src/api/charts.js @@ -0,0 +1,10 @@ +import request from '@/api/request' + +// 获取list +export const leaderBoardApi = (data) => { + return request({ + url: '/traapp/myLeaderBoard/leaderBoard ', + method: 'post', + data + }); +}; \ No newline at end of file diff --git a/tra-app/src/api/common.js b/tra-app/src/api/common.js new file mode 100644 index 0000000..3a75acb --- /dev/null +++ b/tra-app/src/api/common.js @@ -0,0 +1,272 @@ +import { + get_base_url, + REQUES_ERROR_CODES, + goto_login_fun +} from '@/api/request' +import { + getToken +} from '@/common/common.js' +import common from '@/common/common'; + + +export const uploadFile = (file, data) => { + const url = '/traoss/tras3/upload'; + const base_url = get_base_url(url); + const token = getToken(); + const header = {}; + console.log('file', file); + if (token) + header['summary'] = token + return new Promise((resolve, reject) => { + uni.uploadFile({ + url: `${base_url}${url}`, + header: header, + filePath: file, + name: 'file', + formData: data, + success(result) { + try { + const res = JSON.parse(result.data); + if (res.rtnCode === '0000') { + resolve(res.body); + } else { + reject(new Error(res.msg)); + } + } catch (error) { + reject(new Error('Failed to parse response')); + } + }, + fail(error) { + console.log('error', error); + reject(new Error('Upload failed')); + } + }); + }); +}; +export const getPreviewFileUrl = (id) => { + return get_base_url() + `/traoss/tras3/show/${id}` +} +// 对象存储下载 /traoss/tras3/show/{文件ID} +export function downloadFile(id) { + const token = getToken(); + const header = { + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", + }; + if (token) + header['summary'] = token + let baseUrl = get_base_url(); + + return new Promise((resolve, reject) => { + uni.downloadFile({ + url: `${baseUrl}/traoss/tras3/show/${id}`, + header, + success(result) { + resolve(result) + }, + fail(error) { + reject(new Error(error)); + } + }); + }); +} + +// 上传音频 通用 +export const commonUploadVoiceFile = (file, url, data = {}, fileKey = 'voice') => { + const base_url = get_base_url(url); + const token = getToken(); + const header = { + "Content-Type": "multipart/form-data; charset=UTF-8", + }; + if (token) + header['summary'] = token + return new Promise((resolve, reject) => { + uni.uploadFile({ + url: `${base_url}${url}`, + header: header, + filePath: file, + name: fileKey, + formData: data, + timeout: 60000, + success(result) { + try { + console.log(result) + const res = JSON.parse(result.data); + if (res.rtnCode === '0000') { + return resolve(res); + } else if (REQUES_ERROR_CODES['NO_LOGIN'].includes(res.rtnCode)) { + goto_login_fun() + return reject('NO_LOGIN'); + } else if (res.rtnCode === '0004') { + return reject('N'); + } + return reject('Other'); + } catch (error) { + reject(new Error('Failed to parse response')); + } + }, + fail(error) { + return reject('Other'); + } + }); + }); +}; +// 上传音频 翻译文本 +export const uploadVoiceFile = (file) => { + const url = '/trastudy/traStdyInfo/audioTranscriptions'; + const base_url = get_base_url(url); + const token = getToken(); + const header = { + "Content-Type": "multipart/form-data; charset=UTF-8", + }; + if (token) + header['summary'] = token + return new Promise((resolve, reject) => { + uni.uploadFile({ + url: `${base_url}${url}`, + header: header, + filePath: file, + name: 'voice', + success(result) { + try { + const res = JSON.parse(result.data); + if (res.rtnCode === '0000') { + resolve(result); + } else { + reject(new Error(res.message)); + } + } catch (error) { + reject(new Error('Failed to parse response')); + } + }, + fail(error) { + reject(new Error('Upload failed')); + } + }); + }); +}; +// 获取音频 通用 +export function downloadVoiceFile(url) { + const token = getToken(); + const header = { + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", + }; + if (token) + header['summary'] = token + let baseUrl = get_base_url(url); + return new Promise((resolve, reject) => { + uni.downloadFile({ + url: `${baseUrl}${url}`, + header, + timeout: 180000, + success(result) { + console.log(result) + resolve(result) + }, + fail(error) { + console.log(error) + reject(new Error(error)); + } + }); + }); +} +// 获取音频 通用参数 +export function downloadVoiceParams(url) { + const token = getToken(); + const header = { + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", + }; + if (token) + header['summary'] = token + let baseUrl = get_base_url(url); + return { + url: `${baseUrl}${url}`, + header, + timeout: 180000, + } +} + + +export function getAudioByText(data) { + const token = getToken(); + const header = { + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", + }; + if (token) + header['summary'] = token + let baseUrl = get_base_url(); + + return new Promise((resolve, reject) => { + uni.request({ + url: `${baseUrl}/trastudy/traStdyInfo/readContent`, + header, + method: "POST", + data, + responseType: "arraybuffer", + success(result) { + if (result.statusCode === 200) { + resolve(result.data) + } else { + reject(new Error(result)) + } + }, + fail(error) { + reject(new Error(error)); + } + }); + }); +} + +// 对象存储预览 /traoss/tras3/show/{文件ID} +export function previewFile(id, responseType = 'arraybuffer') { + const token = getToken(); + const header = { + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", + }; + if (token) + header['summary'] = token + let baseUrl = get_base_url(); + + return new Promise((resolve, reject) => { + uni.request({ + url: `${baseUrl}/traoss/tras3/show/${id}`, + header, + responseType, + success(result) { + resolve(result) + }, + fail(error) { + reject(new Error(error)); + } + }); + }); +} +// 缩略图预览 /traoss/tras3/getCacheThumbImage/{fileId}/{width}/{height} +export function previewCacheThumbImage(id, width = 30, height = 30) { + const token = getToken(); + const header = { + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", + }; + if (token) + header['summary'] = token + let baseUrl = get_base_url(); + + return new Promise((resolve, reject) => { + uni.request({ + url: `${baseUrl}/traoss/tras3/getCacheThumbImage/${id}/${width}/${height}`, + header, + success(result) { + if (result.data.rtnCode === '0000') { + let base64 = `data:image/png;base64,` + result.data.body + resolve(base64) + } else { + reject({ + error: result.data.message + }) + } + }, + fail(error) { + reject(new Error(error)); + } + }); + }); +} \ No newline at end of file diff --git a/tra-app/src/api/competition.js b/tra-app/src/api/competition.js new file mode 100644 index 0000000..131a92a --- /dev/null +++ b/tra-app/src/api/competition.js @@ -0,0 +1,85 @@ +import request from '@/api/request' + +const module_url = '/traexam' + +// 查询竞赛列表 +export const queryTraCompetitionInfoPaging = (data) => { + return request({ + url: module_url + '/traCompetitionInfo/queryTraCompetitionInfoPaging', + method: 'post', + data + }); +}; + +// 查询试卷 +export const queryTestPapers = (data) => { + return request({ + url: module_url + '/traCompetitionInfo/queryTestPapers', + method: 'post', + data + }); +}; + +// 查询PK人员+试题 +export const queryTraCompetitionInfoByPkUser = (data) => { + return request({ + url: module_url + '/traCompetitionInfo/queryTraCompetitionInfoByPkUser', + method: 'post', + data + }); +}; + +// 循环获取简答题结果 +export const queryCompetitionPracticeAnswerResult = (data) => { + return request({ + url: module_url + '/traCompetitionInfo/queryCompetitionPracticeAnswerResult', + method: 'post', + data + }); +}; + +// 答题结束 +export const competitionAnswerEnd = (data) => { + return request({ + url: module_url + '/traCompetitionInfo/competitionAnswerEnd', + method: 'post', + data, + suppressErrors: true // 屏蔽报错信息 + }); +}; + +// 竞赛答题 +export const competitionAnswer = (data) => { + return request({ + url: module_url + '/traCompetitionInfo/competitionAnswer', + method: 'post', + data + }); +}; + +// 查询答题结果 +export const queryCompetitionAnswerResult = (data) => { + return request({ + url: module_url + '/traCompetitionInfo/queryCompetitionAnswerResult', + method: 'post', + data + }); +}; + +// 竞赛排行榜 +export const queryTraCompetitionRankingList = (data) => { + return request({ + url: module_url + '/traCompetitionInfo/queryTraCompetitionRankingList', + method: 'post', + data + }); +}; + +// 全行的机构数据 +export const queryOrgTree = (data) => { + return request({ + url: module_url + '/traCompetitionInfo/queryOrgTree', + method: 'post', + data + }); +}; diff --git a/tra-app/src/api/course.js b/tra-app/src/api/course.js new file mode 100644 index 0000000..40246c4 --- /dev/null +++ b/tra-app/src/api/course.js @@ -0,0 +1,72 @@ +import request from '@/api/request' + +const base_url = '/traapp' +const course_base_url = '/trastudy' + + +// 获取课程分类树接口 +export const getCourseTypeListApi = (data) => { + return request({ + url: base_url + '/app/traCrsCatalogInfo/courseQueryTraCrsCatalogInfoList', + method: 'post', + data + }); +}; +// 获取生产线分类树接口 +export const getPrdLineTypeListApi = (data) => { + return request({ + url: base_url + '/app/traPrdLineInfo/courseQueryTraPrdLineInfoList', + method: 'post', + data + }); +}; +// 获取标签分类树 接口 +export const getTraTagListApi = (data) => { + return request({ + url: base_url + '/app/traTagCatalogInfo/queryTraTagCatalogInfoList', + method: 'post', + data + }); +}; +// 全部模块 +export const queryCrsInfoByCatalogApi = (data) => { + return request({ + url: course_base_url + '/crsInfo/queryCrsInfoByCatalog', + method: 'post', + data + }); +}; +// 生产线模块- +export const queryCrsInfoByPrdLineApi = (data) => { + return request({ + url: course_base_url + '/crsInfo/queryCrsInfoByPrdLine', + method: 'post', + data + }); +}; +// 最新 +export const queryCrsInfoByLastnewApi = (data) => { + return request({ + url: course_base_url + '/crsInfo/queryCrsInfoByLastnew', + method: 'post', + data + }); +}; +// 最热 +export const queryCrsInfoByPopularApi = (data) => { + return request({ + url: course_base_url + '/crsInfo/queryCrsInfoByPopular', + method: 'post', + data + }); +}; +// 推荐 +export const queryCrsInfoByRecmdApi = (data) => { + return request({ + url: course_base_url + '/crsInfo/queryCrsInfoByRecmd', + method: 'post', + data + }); +}; + + diff --git a/tra-app/src/api/courseDetail.js b/tra-app/src/api/courseDetail.js new file mode 100644 index 0000000..bd0efd1 --- /dev/null +++ b/tra-app/src/api/courseDetail.js @@ -0,0 +1,72 @@ +import request from '@/api/request' + +const module_url = '/traapp' + +// AITS-A-4006--APP课程详情查询-含知识点学习状态数据 +export const getCourseDetailApi = (data) => { + return request({ + url: module_url + '/crsInfo/queryCrsInfoDetail', + method: 'post', + data + }); +}; + + +// AITS-A-4007--APP课程评价列表数据查询(20250723预计废弃) +export const qryTraCrsBbsDataByCrsNum = (data) => { + return request({ + url: module_url + '/traCrsBbsInfo/qryTraCrsBbsDataByCrsNum', + method: 'post', + data + }); +}; + +//王洋新写 +export const queryTraCrsBbsInfoByCrsId = (data) => { + return request({ + url: module_url + '/traCrsBbsInfo/queryTraCrsBbsInfoByCrsId', + method: 'post', + data + }); +}; + +// AITS-A-4008--APP课程评价下回复评论时刷新二级评论列表数据接口 +export const qryChildTraCrsBbsInfo = (data) => { + return request({ + url: module_url + '/traCrsBbsInfo/qryChildTraCrsBbsInfo', + method: 'post', + data + }); +}; + + +// AITS-A-4009--APP课程评价回复评论接口 +export const replyTraCrsBbsInfo = (data) => { + return request({ + url: module_url + '/traCrsBbsInfo/replyTraCrsBbsInfo', + method: 'post', + data + }); +}; + + +// AITS-A-4010--APP课程评价接口 +export const publishCrsEvaluation = (data) => { + return request({ + url: module_url + '/traCrsBbsInfo/publishCrsEvaluation', + method: 'post', + data + }); +}; + + +// 考试排行榜/traexam/traCrsPapers/queryTraCrsRankingList +export const queryTraCrsRankingList = (data) => { + return request({ + url: '/traexam/traCrsPapers/queryTraCrsRankingList', + method: 'post', + data + }); +}; + + diff --git a/tra-app/src/api/courseRecord.js b/tra-app/src/api/courseRecord.js new file mode 100644 index 0000000..b32117d --- /dev/null +++ b/tra-app/src/api/courseRecord.js @@ -0,0 +1,124 @@ +import request from '@/api/request' +const base_url = '/traexam' + + +/**学习*/ +// 学习列表页 +export const queryStdyRecordPaging = (data) => { + return request({ + url: base_url + '/traRecord/queryStdyRecordPaging', + method: 'post', + toastErrors: true, + data + }); +}; +// 学习单项列表 crsId +export const queryStdyBatchByCrsId = (data) => { + return request({ + url: base_url + '/traRecord/queryStdyBatchByCrsId', + method: 'post', + toastErrors: true, + data + }); +}; + +// 学习详情 通过 stdyId 查询 +export const queryTraStudyExecuteRecord = (data) => { + return request({ + url: base_url + '/traCrsPapers/queryTraStudyExecuteRecord', + method: 'post', + toastErrors: true, + data + }); +}; +/**练习*/ +// 练习列表页 +export const queryPracticeRecordPaging = (data) => { + return request({ + url: base_url + '/traRecord/queryPracticeRecordPaging', + method: 'post', + toastErrors: true, + data + }); +}; +// 练习每一项列表 +export const queryPracticeHis = (data) => { + return request({ + url: base_url + '/traRecord/queryPracticeHis', + method: 'post', + toastErrors: true, + data + }); +}; +// 练习详情 通过 exrId 查询 +export const queryPracticeRecordByExrId = (data) => { + return request({ + url: base_url + '/traCrsPractice/queryPracticeRecordByExrId', + method: 'post', + toastErrors: true, + data + }); +}; + +/**考试*/ +export const queryCrsExamRecordPaging = (data) => { + return request({ + url: base_url + '/traRecord/queryCrsExamRecordPaging', + method: 'post', + toastErrors: true, + data + }); +}; +// 考试每一项列表 +export const queryCrsExamHisByExamId = (data) => { + return request({ + url: base_url + '/traRecord/queryCrsExamHisByExamId', + method: 'post', + toastErrors: true, + data + }); +}; + + +/**考试中心考试*/ +// 考试列表页 +export const queryExamRecordPaging = (data) => { + return request({ + url: base_url + '/traRecord/queryExamRecordPaging', + method: 'post', + toastErrors: true, + data + }); +}; +// 考试每一项列表 +export const queryExamHisByExamId = (data) => { + return request({ + url: base_url + '/traRecord/queryExamHisByExamId', + method: 'post', + toastErrors: true, + data + }); +}; + + +/**竞赛*/ + +// 查询竞赛记录 +export const queryTraCompetitionRecordPaging = (data) => { + return request({ + url: base_url + '/traCompetitionInfo/queryTraCompetitionRecordPaging', + method: 'post', + toastErrors: true, + data + }); +}; +// 查询竞赛答题列表 +export const queryTraCompetitionRecordInfoPaging = (data) => { + return request({ + url: base_url + '/traCompetitionInfo/queryTraCompetitionRecordInfoPaging', + method: 'post', + toastErrors: true, + data + }); +}; + diff --git a/tra-app/src/api/dialog.js b/tra-app/src/api/dialog.js new file mode 100644 index 0000000..99b1584 --- /dev/null +++ b/tra-app/src/api/dialog.js @@ -0,0 +1,64 @@ +import request from '@/api/request' + + + +// 获取问题list +export const queryHotQuestionApi = (data) => { + return request({ + // url: '/traask/traAskchat/queryHotQuestion', + url: '/traask/traAskchat/queryHotQuestions', + method: 'post', + data + }); +}; +// 获取问答历史 +export const queryAskBatchSessionPaging = (data) => { + return request({ + url: '/traask/traAskchatSession/queryAskBatchSessionPaging', + method: 'post', + data + }); +}; +// 问答详情 /traAskchatSession/queryAskHisByBatchIdPaging +export const queryAskHisByBatchIdPaging = (data) => { + return request({ + url: '/traask/traAskchatSession/queryAskHisByBatchId', + method: 'post', + data + }); +}; +// 问答反馈 +export const insertTraAskFeedback = (data) => { + return request({ + url: '/traask/traAskFeedback/insertTraAskFeedback', + method: 'post', + data + }); +}; +// 取消反馈 取消踩 /traAskFeedback/updateDownvoteStat +export const updateDownvoteStat = (data) => { + return request({ + url: '/traask/traAskFeedback/updateDownvoteStat', + method: 'post', + data + }); +}; +// 赞 /取消赞/traAskFeedback/updateDownvoteStat +export const updateLikeStat = (data) => { + return request({ + url: '/traask/traAskFeedback/updateLikeStat', + method: 'post', + data + }); +}; + + +// example /traask/traTestchat/chatVoice +export const chatVoice = (data) => { + return request({ + url: '/traask/traTestchat/chatVoice', + method: 'post', + timeout: 120000, + data + }); +}; diff --git a/tra-app/src/api/encryptAndDecryptData.js b/tra-app/src/api/encryptAndDecryptData.js new file mode 100644 index 0000000..d994dc1 --- /dev/null +++ b/tra-app/src/api/encryptAndDecryptData.js @@ -0,0 +1,90 @@ + +import CryptoJS from 'crypto-js'// 引入AES加密库 npm install crypto-js@3.1.9-1 -save +// import { JSEncrypt } from 'jsencrypt'; // npm install jsencrypt -save + +const RsaPublicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCZf8e1tzYTSr7KciAHH2KrE3O13ftuv3rHRRz7MztYXirMdLquocTNSJ5BAoj1H3V8fvFtfmN6BkuB6XnQrbY5heOzZZQWvO4qAa0EktwkLQUwCkYJfaQNc1Iw4wZFt4VO7U8+LLo+IO7jtxmaNIBqJm8laDc1C6zw2ETZD4bQ6wIDAQAB" +const iv = CryptoJS.enc.Utf8.parse('1234567812345678'); +//定义rsa加密类 +// const crypt = new JSEncrypt(); +// crypt.setPublicKey(RsaPublicKey); // 设置公钥 + + +// 生成一个随机的128位(16字节)AES密钥 +const generateAESKey = () => { + return CryptoJS.lib.WordArray.random(16); +} +// 转化生成AES密钥成base64字符串 +const getGenerateAESKeyBase64 = (aesKey) => aesKey.toString(CryptoJS.enc.Base64) + +// 执行rsa加密 +const encryptRSA = (str) => crypt.encrypt(str) +// 执行rsa解密 +const decryptRSA = (str) => crypt.decrypt(str) + +// 执行aes加密 +export const encryptAES = (data, aesKeyBase64, iv) => { + const key = CryptoJS.enc.Utf8.parse(aesKeyBase64); + return CryptoJS.AES.encrypt(JSON.stringify(data), key, { + iv: iv, + mode: CryptoJS.mode.CBC, + padding: CryptoJS.pad.Pkcs7 + }).toString(); +} +// aes解密 +const decryptAES = (ciphertext, aesKeyBase64, iv) => { + const key = CryptoJS.enc.Utf8.parse(aesKeyBase64); + const bytes = CryptoJS.AES.decrypt(ciphertext, key, { + iv: iv, + mode: CryptoJS.mode.CBC, + padding: CryptoJS.pad.Pkcs7 + }); + return JSON.parse(bytes.toString(CryptoJS.enc.Utf8)); +} + +// 加密过程 +export const encryptData = (data={}) => { + const aesKey = generateAESKey(); // 获取随机密钥key + const aesKeyBase64 = getGenerateAESKeyBase64(aesKey) // 变成base64 + const key = encryptRSA(aesKeyBase64) + const ciphertext = encryptAES(data, aesKeyBase64, iv); + const decryptedData = decryptAES(ciphertext, aesKeyBase64, iv); + return {text:ciphertext, key:key, aesKeyBase64:aesKeyBase64} +} + +// 解密过程 +export const decryptData= (aesStr, aesKey) => { + return decryptAES(aesStr, aesKey, iv) +} + +/** + * AES加密 + * @param plainText 明文 + * @param keyInBase64Str base64编码后的key + * @returns {string} base64编码后的密文 + */ +export function encryptByAES(plainText, keyInBase64Str) { + let key = CryptoJS.enc.Base64.parse(keyInBase64Str); + let encrypted = CryptoJS.AES.encrypt(plainText, key, { + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.Pkcs7, + }); + // 这里的encrypted不是字符串,而是一个CipherParams对象 + return encrypted.ciphertext.toString(CryptoJS.enc.Base64); +} + +/** + * AES解密 + * @param cipherText 密文 + * @param keyInBase64Str base64编码后的key + * @return 明文 + */ +export function decryptByAES(cipherText, keyInBase64Str) { + let key = CryptoJS.enc.Base64.parse(keyInBase64Str); + // 返回的是一个Word Array Object,其实就是Java里的字节数组 + let decrypted = CryptoJS.AES.decrypt(cipherText, key, { + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.Pkcs7, + }); + + return decrypted.toString(CryptoJS.enc.Utf8); +} diff --git a/tra-app/src/api/examination.js b/tra-app/src/api/examination.js new file mode 100644 index 0000000..3cfb371 --- /dev/null +++ b/tra-app/src/api/examination.js @@ -0,0 +1,110 @@ +import request from '@/api/request' +import { getToken } from '@/common/common.js' +import { get_base_url } from '@/api/request' +const base_url = '/traexam' + +// 开始考试(试卷信息) +export const startTraExamPapersInfo = (data) => { + return request({ + url: base_url + '/traExamPapers/startTraExamPapersInfo', + method: 'post', + toastErrors: true, + data + }); +}; +// 提交试卷 +export const commitPaperExam = (data) => { + return request({ + url: base_url + '/traExamPapers/commitPaperExam', + method: 'post', + toastErrors: true, + data + }); +}; + +// 考试中心考试查分 +export const queryPaperExamScore = (data) => { + return request({ + url: base_url + '/traExamPapers/queryPaperExamScore', + method: 'post', + toastErrors: true, + data + }); +}; +// 根据试卷id查询详情 +export const queryTraExamPapersByPapersId = (data) => { + return request({ + url: base_url + '/traExamPapers/queryTraExamPapersByPapersId', + method: 'post', + toastErrors: true, + data + }); +}; + + +// ↑↑↑↑↑↑↑↑↑考试中心考试↑↑↑↑↑↑↑ + +// ↓↓↓↓↓↓↓↓↓课程考试↓↓↓↓↓↓↓↓ + +export const startExamByCrs = (data) => { + return request({ + url: base_url + '/traCrsPapers/startExamByCrs', + method: 'post', + toastErrors: true, + data + }); +}; +// 提交课程考试 +export const commitCrsExam = (data) => { + return request({ + url: base_url + '/traCrsPapers/commitCrsExam', + method: 'post', + toastErrors: true, + data + }); +}; +// 试卷考试查分 +export const queryCrsExamScore = (data) => { + return request({ + url: base_url + '/traCrsPapers/queryCrsExamScore', + method: 'post', + toastErrors: true, + data + }); +}; + + +// 试卷考试 排行榜 +export const queryPaperExamRankingList = (data) => { + return request({ + url: base_url + '/traExamPapers/queryPaperExamRankingList', + method: 'post', + toastErrors: true, + data + }); +}; + + + // 反馈问题 + export const saveTraQnsFeedback = (data) => { + return request({ + url: base_url + '/traCrsPapers/saveTraQnsFeedback', + method: 'post', + toastErrors: true, + data + }); + }; + + +// 考试过程中提交结果,后端用来缓存(考试中心考试和课程考试共用这个缓存接口) + export const commitPaperCache = (data) => { + return request({ + url: base_url + '/traExamPapers/commitPaperCache', + method: 'post', + toastErrors: true, + data + }); + }; + + + \ No newline at end of file diff --git a/tra-app/src/api/favorites.js b/tra-app/src/api/favorites.js new file mode 100644 index 0000000..70426dd --- /dev/null +++ b/tra-app/src/api/favorites.js @@ -0,0 +1,49 @@ +import request from '@/api/request' +const base_url = '/trastudy' + +// 课程收藏分页查询 +export const queryTraPersonalCollectionCrsPaging = (data) => { + return request({ + url: base_url + '/traPersonalCollection/queryTraPersonalCollectionCrsPaging', + method: 'post', + toastErrors: true, + data + }); +}; + +// 任务收藏分页查询 +export const queryTraPersonalCollectionTaskPaging = (data) => { + return request({ + url: base_url + '/traPersonalCollection/queryTraPersonalCollectionTaskPaging', + method: 'post', + toastErrors: true, + data + }); +}; + +// 根据收藏ID删除收藏 +export const deleteTraPersonalCollectionById = (data) => { + return request({ + url: base_url + '/traPersonalCollection/deleteTraPersonalCollectionById', + method: 'post', + toastErrors: true, + data + }); +}; + + + +/* +新增任务收藏 +入参: + collectTyp 收藏类型 必须 String 01--课程,02--任务 + collectBusId 收藏业务ID 必须 String +**/ +export const insertTraPersonalCollectionTask = (data) => { + return request({ + url: base_url + '/traPersonalCollection/insertTraPersonalCollectionTask', + method: 'post', + toastErrors: true, + data + }); +}; \ No newline at end of file diff --git a/tra-app/src/api/index.js b/tra-app/src/api/index.js new file mode 100644 index 0000000..cd5e5e8 --- /dev/null +++ b/tra-app/src/api/index.js @@ -0,0 +1,91 @@ +import request from '@/api/request' + + +// 获取首页统计数据方法(查询个人学习统计信息 ) +export const getStatisticsData = (data) => { + /** + userId 学员ID String + stdtTmLen 本年学习时长 double + accmPracticeCnt 累计训练次数 int + accmExamCnt 累计考试次数 int + **/ + return request({ + url: '/traapp/traStdyInfo/queryTraStdyStatisticsPerson', + method: 'post', + data + }); +}; + +// 最新课程查询接口 +export const queryCrsInfoByLastnew = (data) => { + /** + crsNum 课程编号 String + crsName 课程名称 String + tagId 课程分类 String + prdLineId 生产线分类 String + issuTm 发布日期 String + imgId 课程封面ID String + accmStdyCnt 累计学习次数 int + evalGrade 课程评分 Bigdecimal + tagCataList 课程标签集 List + tagId 标签ID String + tagName 标签名称 String + **/ + return request({ + url: '/traapp/mycrs/queryCrsInfoByLastnew', + method: 'post', + data + }); +}; + + +// 热门课程查询接口 +export const queryCrsInfoByPopular = (data) => { + /** + tagId 标签ID(可选填),为空时查全部 必输项:false 类型:String + 出参: + tagId 标签ID(可选填),为空时查全部 必输项:false 类型:String + 出参: + crsId 课程ID StringcrsNum 课程编号 String + crsName 课程名称 String + tagId 课程分类 String + prdLineId 生产线分类 String + issuTm 发布日期 String + imgId 课程封面ID String + accmStdyCnt 累计学习次数 int + evalGrade 课程评分 Bigdecimal + tagCataList 课程标签集 List + tagId 标签ID String + tagName 标签名称 String + **/ + return request({ + url: '/traapp/mycrs/queryCrsInfoByPopular', + method: 'post', + data + }); +}; + +// 推荐课程查询接口 +export const queryCrsInfoByRecmd = (data) => { + /** + crsNum 课程编号 String + crsName 课程名称 String + tagId 课程分类 String + prdLineId 生产线分类 String + issuTm 发布日期 String + imgId 课程封面ID String + accmStdyCnt 累计学习次数 int + evalGrade 课程评分 Bigdecimal + tagCataList 课程标签集 List + tagId 标签ID String + tagName 标签名称 String + **/ + return request({ + url: '/traapp/mycrs/queryCrsInfoByRecmd', + method: 'post', + data + }); +}; + + + diff --git a/tra-app/src/api/leaderBoard.js b/tra-app/src/api/leaderBoard.js new file mode 100644 index 0000000..c504f9a --- /dev/null +++ b/tra-app/src/api/leaderBoard.js @@ -0,0 +1,38 @@ +import request from '@/api/request' + +const base_url = '/traapp' + + +// 学习时长 +export const getLeaderBoardStdyTmApi = (data) => { + return request({ + url: base_url + '/myLeaderBoard/stdyTmLen', + method: 'post', + data + }); +}; +// 练习次数 +export const getLeaderBoardPractCntApi = (data) => { + return request({ + url: base_url + '/myLeaderBoard/practCnt', + method: 'post', + data + }); +}; +// 考试次数 +export const getLeaderBoardExamCntApi = (data) => { + return request({ + url: base_url + '/myLeaderBoard/examCnt', + method: 'post', + data + }); +}; +// 通关课程数 +export const getLeaderBoardCrsCntApi = (data) => { + return request({ + url: base_url + '/myLeaderBoard/crsCnt', + method: 'post', + data + }); +}; + diff --git a/tra-app/src/api/learningTasks.js b/tra-app/src/api/learningTasks.js new file mode 100644 index 0000000..a8fb5b3 --- /dev/null +++ b/tra-app/src/api/learningTasks.js @@ -0,0 +1,31 @@ +import request from '@/api/request' +const base_url = '/trastudy' + +// 查询所有、未完成、已完成的任务(分页) +export const queryAllTraTaskInfoByUserId = (data) => { + return request({ + url: base_url + '/traTaskInfo/queryAllTraTaskInfoByUserId', + method: 'post', + data + }); +}; + + +// 查询当前用户的课程完成情况 +export const queryAllTraTaskCrsInfo = (data) => { + return request({ + url: base_url + '/traTaskInfo/queryAllTraTaskCrsInfo', + method: 'post', + data + }); +}; + + +// 根据任务id查询这一条 +export const queryTraTaskInfoByTaskId = (data) => { + return request({ + url: base_url + '/traTaskInfo/queryTraTaskInfoByTaskId', + method: 'post', + data + }); +}; \ No newline at end of file diff --git a/tra-app/src/api/login.js b/tra-app/src/api/login.js new file mode 100644 index 0000000..ab84649 --- /dev/null +++ b/tra-app/src/api/login.js @@ -0,0 +1,239 @@ +import request from '@/api/request' +import common from "@/common/common"; +import { + queryCurrentValidMascotInfo +} from "@/api/mascot.js" + +import { + queryCurrentStatus +} from '@/api/pointsAndRank.js'; + +import { + setToken, + setUserInfo, + getUserInfo, + getToken +} from "@/common/common"; +import { + get_base_url +} from '@/api/request.js' + +// 账号登录接口 +export const useAccountLoginApp = (data) => request({ + url: '/traapp/access/userLogin', + method: 'post', + data +}).then(async ({ + rtnCode, + body +}) => { + if (rtnCode === '0000') { + setToken(body) + const res = await Promise.all([queryCurrentUser(), queryCurrentValidMascotInfo() + // queryCurrentStatus() + ]) + const userInfo = getUserInfo() + const mascotInfo = res[1] + setUserInfo({ + ...userInfo, + 'mascotInfo': mascotInfo + }) + // 积分状态 + // const pointsStatus = res[2].body + const pointsStatus = {} + common.setValue('points_data', { + badgeNum: pointsStatus['badgeNum'] ?? 0, // 徽章数 + currentPoints: pointsStatus['currentPoints'] ?? 0, // 当前积分 + currentRankAddr: pointsStatus['ossAddr'] ?? '', // 当前段位图片 + currentRankName: pointsStatus['currentRankName'] ?? '未知', // 当前段位名称 + }) + return { + mascotState: !mascotInfo.mascotId + } + } + return Promise.reject({ + rtnCode, + body + }) +}).catch(res => { + return Promise.reject(res) +}) + +/** + * 获取个人信息 + * 入参 + gender f是女 m男 + imageAddr: 图片 + loginName ID + userName 姓名 + */ +export const queryCurrentUser = (data) => request({ + url: '/traapp/access/queryCurrentUser', + method: 'post', + data +}).then(res => { + // deptId: "" + // deptName: null + // loginName: "ADMIN" // todo id + // orgId: "00E200" + // orgName: "金融科技部" + // userId: "USER001" + // userName: "ADMIN" / todo name + // userStatus: "01" + const userInfo = getUserInfo() ?? {} + setUserInfo({ + ...userInfo, + ...res.body + }) + // console.log('预加载头像'); + const base_url = get_base_url() + const token = getToken() + // 预加载头像 + // #ifdef APP-PLUS + // 更新用户信息中的头像路径 + const updateUserAvatar = (imagePath) => { + const _userInfo = getUserInfo() ?? {} + setUserInfo({ + ..._userInfo, + ...res.body, + 'imagePath': imagePath + }) + } + + // 确保目录存在 + const ensureDirectoryExists = (dirPath) => { + return new Promise((resolve) => { + plus.io.requestFileSystem(plus.io.PRIVATE_DOC, (fs) => { + fs.root.getDirectory(dirPath, { + create: true + }, resolve, resolve) + }) + }) + } + + // 生成唯一文件名 + const generateUniqueFilename = (pathName) => { + if (!pathName || typeof pathName !== 'string') { + throw new Error('无效的路径'); + } + + // 直接使用原始文件名(带扩展名) + const filename = pathName.split('/').pop(); + + if (!filename) { + throw new Error('无法从路径中提取文件名'); + } + + // 确保文件扩展名为.png (可选,根据实际需求) + const ext = filename.split('.').pop().toLowerCase(); + if (ext === 'png' || ext === 'jpg' || ext === 'jpeg' || ext === 'webp') { + return filename; + } + + // 如果没有有效扩展名,添加.png + return `${filename}.png`; + }; + + // 下载并保存头像 + const downloadAndSaveAvatar = async (fileUrl, localFilePath) => { + try { + await ensureDirectoryExists('avatar') + + return new Promise((resolve, reject) => { + console.log('开始下载头像:', fileUrl) + const dtask = plus.downloader.createDownload(fileUrl, { + filename: localFilePath + }, (d, status) => { + if (status === 200) { + console.log("保存路径:", d.filename) + resolve(d.filename) + } else { + console.error("文件下载失败:", status) + reject(new Error(`下载失败,状态码: ${status}`)) + } + }) + + dtask.start() + }) + } catch (error) { + console.error('创建目录失败:', error) + throw error + } + } + + // 检查文件是否存在 + const checkFileExists = (filePath) => { + return new Promise((resolve) => { + uni.getFileInfo({ + filePath, + success: () => resolve(true), + fail: () => resolve(false) + }) + }) + } + + // 主逻辑 + (async () => { + try { + const pathName = res.body.imageAddr + if (pathName) { + const fileUrl = base_url + pathName + '?tk=' + token + console.log('fileUrl', fileUrl); + // 生成保存路径 + const filename = generateUniqueFilename(pathName) + const localFilePath = '_doc/avatar/' + filename + + // 检查文件是否存在 + const exists = await checkFileExists(localFilePath) + + if (exists) { + // console.log('头像已存在,无需下载') + updateUserAvatar(localFilePath) + } else { + const savedPath = await downloadAndSaveAvatar(fileUrl, localFilePath) + updateUserAvatar(savedPath) + } + } + + } catch (error) { + console.error('头像预加载过程发生错误:', error) + } + })() + // #endif + + return res.body +}) + +//获取隐私协议 +export const getYsxy = (data) => request({ + url: '/traapp/dfAgtInfo/queryValidDfAgt', + method: 'post', + data +}) + +//获取服务条款 +export const getFutk = (data) => request({ + url: '/traapp/dfAgtInfo/queryValidDfAgtTerm', + method: 'post', + data +}) + +// 获取验证码(作废) +export const getLoginAppCheckCode = (data) => request({ + url: '/traapp/access/checkCode', + method: 'get', + data +}) + +// 退出登录 +export const loginOut = (data) => request({ + url: '/traapp/access/loginOut', + method: 'post', + data +}).then(() => { + setToken(null) + setUserInfo(null) + common.setValue('me_statistics_data', null) // 我的页统计数据缓存 + common.setValue('index_statistics_data', null) // 首页统计数据缓存 + common.setValue('points_data', null) // 首页统计数据缓存 +}) \ No newline at end of file diff --git a/tra-app/src/api/mascot.js b/tra-app/src/api/mascot.js new file mode 100644 index 0000000..a9e763e --- /dev/null +++ b/tra-app/src/api/mascot.js @@ -0,0 +1,112 @@ +import request from '@/api/request' +import { + get_base_url +} from '@/api/request.js' +import { getToken } from '@/common/common.js' +const app_url = '/traapp' + + +/** + * 查询吉祥物列表 + * 出参: + mascotId 吉祥物ID String + mascotName 吉祥物名称 String + mascotNameEn 吉祥物英文名称 String + mascotDesc 吉祥物简介 String + mascotNo 吉祥物编号 String + imgAddr 吉祥物图片 String + imgThumbAddr 吉祥物缩略图 String + showOrder 显示顺序 int + */ +export const queryValidTraMascotInfo = (data) => { + return request({ + url: app_url + '/traMascotInfo/queryValidTraMascotInfo', + method: 'post', + toastErrors: true, + data + }); +}; + +/** + * 设置吉祥物 + * 入参: + mascotId 必输项:true 类型:String + */ +export const setTraMascotInfo = (data) => { + return request({ + url: app_url + '/traMascotInfo/setTraMascotInfo', + method: 'post', + toastErrors: true, + data + }); +}; + +// 查询当前已经选择的吉祥物 +// 检查文件是否存在 +const checkFileExists = (filePath) => { + return new Promise((resolve) => { + uni.getFileInfo({ + filePath, + success: () => resolve(true), + fail: () => resolve(false) + }) + }) +} + +/** + * 查询当前已经选择的吉祥物 + * 出参: + mascotId 吉祥物ID String + mascotName 吉祥物名称 String + mascotNameEn 吉祥物英文名称 String + mascotDesc 吉祥物简介 String + mascotNo 吉祥物编号 String + imgAddr 吉祥物图片 String + imgThumbAddr 吉祥物缩略图 String + showOrder 显示顺序 int + */ +export const queryCurrentValidMascotInfo = (data) => { + return request({ + url: app_url + '/traMascotInfo/queryCurrentValidMascotInfo', + method: 'post', + toastErrors: true, + data + }).then(({ + body + }) => { + if (body.imgAddr) { + console.log('body.imgAddrmascotNo'); + const base_url = get_base_url() + const token = getToken() + const pathName = body.imgAddr + const fileUrl = base_url + pathName + '?tk=' + token + const localFilePath = '_doc/mascot/' + body.mascotNo + '.png' + console.log('fileUrl', fileUrl); + // 将 Promise 回调函数声明为 async + return new Promise(async (resolve, reject) => { + // 现在可以在这里使用 await 了 + const exists = await checkFileExists(localFilePath) + if (exists) { + // console.log('已存在,无需下载吉祥物') + resolve({...body, imgAddr:localFilePath}) + } else { + // console.log('开始下载吉祥物:', fileUrl) + const dtask = plus.downloader.createDownload(fileUrl, { + filename: localFilePath + }, (d, status) => { + if (status === 200) { + console.log("保存路径:", d.filename) + resolve({...body, imgAddr:d.filename}) + } else { + console.error("文件下载失败:", status) + reject(new Error(`下载失败,状态码: ${status}`)) + } + }) + + dtask.start() + } + }) + } + return body + }) +}; \ No newline at end of file diff --git a/tra-app/src/api/messageNotification.js b/tra-app/src/api/messageNotification.js new file mode 100644 index 0000000..7b7bf3c --- /dev/null +++ b/tra-app/src/api/messageNotification.js @@ -0,0 +1,62 @@ +import request from '@/api/request' +const base_url = '/trastudy' + +// 查询当前用户的所有消息通知 (分页) +export const queryTraPersonalMessageNoticePaging = (data) => { + return request({ + url: base_url + '/traPersonalMessageNotice/queryTraPersonalMessageNoticePaging', + method: 'post', + toastErrors: true, + data + }); +}; + + +// 读取单一消息 +export const readNotice = (data) => { + return request({ + url: base_url + '/traPersonalMessageNotice/readNotice', + method: 'post', + toastErrors: true, + data + }); +}; + + +// 一键读取所有消息(不展示) +export const oneTimeRead = (data) => { + return request({ + url: base_url + '/traPersonalMessageNotice/oneTimeRead', + method: 'post', + toastErrors: true, + data + }); +}; + +export const noticeMessageCount = (data) => { + return request({ + url: base_url + '/traPersonalMessageNotice/noticeMessageCount', + method: 'post', + data + }); +}; + +export const queryMessageNoticeClass = (data) => { + return request({ + url: base_url + '/traPersonalMessageNotice/queryMessageNoticeClass', + method: 'post', + data + }); +}; + + +// 按照分类一键读取消息 +export const oneTimeReadByNoticeTyp = (data) => { + return request({ + url: base_url + '/traPersonalMessageNotice/oneTimeReadByNoticeTyp', + method: 'post', + data + }); +}; + + \ No newline at end of file diff --git a/tra-app/src/api/pointsAndRank.js b/tra-app/src/api/pointsAndRank.js new file mode 100644 index 0000000..aa6a614 --- /dev/null +++ b/tra-app/src/api/pointsAndRank.js @@ -0,0 +1,129 @@ +import request from '@/api/request' +const base_url = '/traapp' + +/** + * 查询签到 + * userId 学员ID String + * checkInToday 今日是否签到 String + * days 连续签到天数 int + */ +export const queryCheckIn = (data) => { + return request({ + url: base_url + '/traPointsInfo/queryCheckIn', + method: 'post', + data + }); +}; + +/** + * 点击签到 + */ +export const addPointsByCheckIn = (data) => { + return request({ + url: base_url + '/traPointsInfo/addPointsByCheckIn', + method: 'post', + data + }); +}; + + +/** + * 每次完成任务获得弹窗奖励 + * 入参: + taskId 必输项:true 类型:String + type 必输项:true 类型:String + 出参: + userId 学员ID String + badgeName 徽章名称 String + badgeImg 徽章图片 String + num 次数 int + name 描述 String + pointsBadgeName 积分徽章名称 String + pointsBadgeImg 徽章图片 String + currentPoints 当前积分 int + changePoints 本次获得积分 int + */ +export const addPointsByTask = (data) => { + // base_url + return request({ + url: base_url + '/traPointsInfo/addPointsByTask', + method: 'post', + data + }); + // return request({ + // url: '/test/traPointsInfo/addPointsByTask', + // method: 'post', + // data + // }); +}; + +/** + * 查询积分明细 + * 出参: + detailId 明细ID String + userId 学员ID String + mattr 积分来源事项 String + mattrDesc 积分来源描述 String + changeValue 积分变化值 String + currentPoints 当前积分 int + countPoints 累计积分 int + ctTime 创建时间 String + */ +export const queryPointsDetail = (data) => { + return request({ + url: base_url + '/traPointsInfo/queryPointsDetail', + method: 'post', + data + }); +}; + + +/** + * 查询段位明细 + * 出参: + rankId 段位ID String + rankName 段位名称 String + currentRankName 当前段位名称 String + ossAddr 段位图片 String + pointsStart 段位开始区间 int + pointsEnd 段位结束区间 int + countPoints 累计积分 int + */ +export const queryRankDetail = (data) => { + return request({ + url: base_url + '/traPointsInfo/queryRankDetail', + method: 'post', + data + }); +}; + + +/** + * 查询当前状态 + * 出参: + userId 学员ID String + orgId 机构ID String + countPoints 累计积分 int + currentPoints 当前积分 int + currentRankName 当前段位 String + badgeNum 徽章数 int + */ +export const queryCurrentStatus = (data) => { + return request({ + url: base_url + '/traPointsInfo/queryCurrentStatus', + method: 'post', + data + }); +}; + + +/** + * 徽章墙 + */ +export const queryRankWall = (data) => { + return request({ + url: base_url + '/traPointsInfo/queryRankWall', + method: 'post', + data + }); +}; diff --git a/tra-app/src/api/pointsRedemption.js b/tra-app/src/api/pointsRedemption.js new file mode 100644 index 0000000..32bca0c --- /dev/null +++ b/tra-app/src/api/pointsRedemption.js @@ -0,0 +1,20 @@ +import request from '@/api/request' +const base_url = '/traapp' + +// String actId,活动id +export const queryPointsActivityDetail = (data) => { + return request({ + url: base_url + '/traPointsActivity/queryPointsActivityDetail', + method: 'post', + data + }); +}; + + +export const addPointsExchange = (data) => { + return request({ + url: base_url + '/traPointsActivity/addPointsExchange', + method: 'post', + data + }); +}; \ No newline at end of file diff --git a/tra-app/src/api/practice.js b/tra-app/src/api/practice.js new file mode 100644 index 0000000..6a121e8 --- /dev/null +++ b/tra-app/src/api/practice.js @@ -0,0 +1,49 @@ +import request from '@/api/request' +const base_url = '/traexam' + +// 1.开始练习(课程练习) +export const startPracticeByCrs = (data) => { + return request({ + url: base_url + '/traCrsPractice/startPracticeByCrs', + method: 'post', + data + }); +}; + +// 2.课程练习答题 +export const practiceAnswer = (data) => { + return request({ + url: base_url + '/traCrsPractice/practiceAnswer', + method: 'post', + data + }); +}; + +// 3.课程练习状态变更 +export const practiceCommit = (data) => { + return request({ + url: base_url + '/traCrsPractice/practiceCommit', + method: 'post', + suppressErrors:true, + data + }); +}; + +// 4.课程练习记录 +export const queryPracticeRecordByExrId = (data) => { + return request({ + url: base_url + '/traCrsPractice/queryPracticeRecordByExrId', + method: 'post', + data + }); +}; + +// 5.练习时候查询问答题结果 +export const queryCrsPracticeAnswerResult = (data) => { + return request({ + url: base_url + '/traCrsPractice/queryCrsPracticeAnswerResult', + method: 'post', + toastErrors: true, + data + }); +}; \ No newline at end of file diff --git a/tra-app/src/api/preview.js b/tra-app/src/api/preview.js new file mode 100644 index 0000000..86eca83 --- /dev/null +++ b/tra-app/src/api/preview.js @@ -0,0 +1,20 @@ +import request from '@/api/request' +const base_url = '/trastudy' + + +export const queryTraStdyInfoPreviewPaging = (data) => { + return request({ + url: base_url + '/traStdyInfoPreview/queryTraStdyInfoPreviewPaging', + method: 'post', + toastErrors: true, + data + }); +}; +export const queryTraListPreviewPaging = (data) => { + return request({ + url: '/trapractice/traPartnerInfo/queryTraPartnerPreviewPaging', + method: 'post', + toastErrors: true, + data + }); +}; diff --git a/tra-app/src/api/request.js b/tra-app/src/api/request.js new file mode 100644 index 0000000..e9a3016 --- /dev/null +++ b/tra-app/src/api/request.js @@ -0,0 +1,154 @@ +import { + getToken +} from '@/common/common.js' +import common from '@/common/common.js' + +const ENV = import.meta.env +// function getBrowserInfo() { +// const userAgent = navigator.userAgent; +// let browserName = '未知浏览器'; +// let version = '未知版本'; + +// // 检测 Chrome +// if (userAgent.indexOf('Chrome') > -1 && userAgent.indexOf('Edg') === -1) { +// browserName = 'Chrome'; +// version = userAgent.match(/Chrome\/(\d+\.\d+)/)[1]; +// } + +// return { +// browser: browserName, +// version: version, +// userAgent: userAgent +// }; +// } + + +export const get_base_url = (url = '') => { + if (['development', 'development.rd', 'development.yh', 'development.ty'].includes(ENV.MODE)) { // 开发环境 + // #ifdef H5 + if (url) { + let _str = (url.split('/')[0] || url.split('/')[1])?.toUpperCase() + return ENV[`VITE_APP_BASE_H5_API_Url_${_str}`] || ENV.VITE_APP_BASE_H5_API_Url || ENV + .VITE_APP_BASE_API_Url + } else { + return ENV.VITE_APP_BASE_H5_API_Url || ENV.VITE_APP_BASE_API_Url + } + // #endif + // #ifdef APP-PLUS + return ENV.VITE_APP_BASE_API_Url + // #endif + } else { // 其他环境,包括生产环境,sit环境,uat环境 + + return ENV.VITE_APP_BASE_API_Url + // return ENV.VITE_APP_BASE_API_Url + + } +} + +// 请求错误码 +export const REQUES_ERROR_CODES = { + NO_LOGIN: ['QQ0005', '0005'] //登录 +}; + +// 去登录弹窗状态 +let no_login_show_modal_state = false +// 去登录方法 +export const goto_login_fun = (from = 'http') => { + if (no_login_show_modal_state) return; + no_login_show_modal_state = true + let page_route = '' + const pages = getCurrentPages(); + if (pages.length >= 1) { + const page = pages[pages.length - 1]; + page_route = page.route + } + console.log('当前页路由', page_route); + if (page_route === 'pages/login/login') return; + common.hideLoading() + common.show('未登录', '现在去登录', false).then(() => { + // common.navigateTo('/pages/login/login') + if (from === 'socket' && page_route !== '') { + common.redirectTo('/pages/login/login?path=' + page_route) + } else { + common.redirectTo('/pages/login/login') + // common.navigateTo('/pages/login/login') + } + + }).finally(() => { + no_login_show_modal_state = false + }) +} + +export default function request({ + url, + method, + data, + meta, + isStream, + suppressErrors = false, + toastErrors = true, + header = {}, + timeout = 6000, + baseUrl = '' +}) { + const base_url = get_base_url(url); + + const headers_base = { + 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', + ...(header || {}) + } + + let token = getToken() + if (token) { + headers_base['summary'] = token // 让每个请求携带令牌 + } + let that = this + + return new Promise((resolve, reject) => { + uni.request({ + url: `${baseUrl || base_url}${url}`, + data: data, + method: method, + header: headers_base, + timeout: timeout, + success: (response) => { + if (isStream) { // 流式传输,直接返回 + return resolve(response); + } + // suppressErrors + // 模拟拦截器功能 + const res = response.data + if (res?.rtnCode === '0000') { + return resolve(res) + } else if (res?.rtnCode === '1001') { // 为前后端约定不公共拦截的码值(小树20250903定) + return reject(res) + } else if (res?.rtnCode === '0002' && res?.message) { + if (toastErrors) { + common.msg(res.message) + } + return reject(response) + } + //到这里下面全是异常的处理 + if (suppressErrors) { // 静默的接口,报错也不处理,因为下面有公共处理 + return reject(response) + } + if (!res) { + common.msg("系统异常.") + return reject() + } + // 0005 QQ0005 登录已过期 + if (REQUES_ERROR_CODES['NO_LOGIN'].includes(res.rtnCode)) { + goto_login_fun() + } + if (res?.rtnCode === '9999') { + common.msg("系统异常.") + } + return reject(res) + }, + fail(res) { + console.log('请求错误', res, url, base_url); + return reject(res) + } + }); + }) +}; \ No newline at end of file diff --git a/tra-app/src/api/search.js b/tra-app/src/api/search.js new file mode 100644 index 0000000..17666ef --- /dev/null +++ b/tra-app/src/api/search.js @@ -0,0 +1,12 @@ +import request from '@/api/request' +const base_url = '/traapp' + +// 搜索 +export const appSearchCrsApi = (data) => { + return request({ + url: base_url + '/traSearchHis/appSearchCrs', + method: 'post', + toastErrors: true, + data + }); +}; diff --git a/tra-app/src/api/socket.js b/tra-app/src/api/socket.js new file mode 100644 index 0000000..0869f8d --- /dev/null +++ b/tra-app/src/api/socket.js @@ -0,0 +1,242 @@ +import { goto_login_fun } from '@/api/request' +export default class WebSocketUtil { + /** + * WebSocket工具类,用于管理WebSocket连接 + * @param {string} url - WebSocket服务器地址 + * @param {Object} options - 配置选项 + * @param {number} [options.maxReconnectCount=5] - 最大重连次数 + * @param {number} [options.reconnectInterval=3000] - 重连间隔时间(ms) + * @param {number} [options.heartbeatInterval=30000] - 心跳间隔时间(ms) + * @param {string|Object} [options.heartbeatMsg='ping'] - 心跳消息 + */ + constructor(url, options = {}) { + this.url = url; + this.options = options; + this.socketTask = null; // WebSocket任务实例 + this.reconnectTimer = null; // 重连计时器 + this.reconnectCount = 0; // 当前重连次数 + this.maxReconnectCount = options.maxReconnectCount || 5; // 最大重连次数 + this.reconnectInterval = options.reconnectInterval || 3000; // 重连间隔(ms) + this.heartbeatTimer = null; // 心跳计时器 + this.heartbeatInterval = options.heartbeatInterval || 30000; // 心跳间隔(ms) + this.heartbeatMsg = options.heartbeatMsg || 'ping'; // 心跳消息 + this.isClose = false; // 手动关闭 + this.callbacks = { + open: [], // 连接打开回调 + message: [], // 消息接收回调 + close: [], // 连接关闭回调 + error: [] // 错误处理回调 + }; + this.init(true); // 初始化WebSocket连接 + } + + /** + * 初始化WebSocket连接 + */ + init(flag = false) { + if(flag) this.reconnectCount = 0 + this.isClose = false; // 手动关闭 + // 创建WebSocket连接 + this.socketTask = uni.connectSocket({ + url: this.url, + header: this.options.header || {}, + protocols: this.options.protocols || [], + success: () => { + // console.log('WebSocket连接创建成功'); + }, + fail: (err) => { + // console.error('WebSocket连接创建失败', err); + this.triggerEvent('error', err); + this.tryReconnect(); // 连接失败时尝试重连 + } + }); + + // 监听WebSocket连接打开事件 + this.socketTask.onOpen(() => { + // console.log('WebSocket连接已打开'); + this.reconnectCount = 0; // 重置重连计数 + clearInterval(this.reconnectTimer); // 清除重连计时器 + // this.startHeartbeat(); // 启动心跳机制 + this.triggerEvent('open'); // 触发连接打开事件 + }); + + // 监听WebSocket消息接收事件 + this.socketTask.onMessage((res) => { + if(res.data){ + const { rtnCode } = JSON.parse(res.data) + // console.log('收到WebSocket消息', JSON.parse(res.data) ); + if(['0005', 'QQ0005'].includes(rtnCode)) { + clearInterval(this.reconnectTimer); + this.reconnectTimer = null + this.close() + goto_login_fun('socket'); + return + } + }else{ + clearInterval(this.reconnectTimer); // 清除重连计时器 + this.reconnectTimer = null + goto_login_fun('socket'); + return + } + this.triggerEvent('message', res); // 触发消息接收事件 + }); + + // 监听WebSocket连接关闭事件 + this.socketTask.onClose((res) => { + // console.log('WebSocket连接已关闭', res); + clearInterval(this.heartbeatTimer); // 清除心跳计时器 + this.triggerEvent('close', res); // 触发连接关闭事件 + this.tryReconnect(); // 尝试重连 + }); + + // 监听WebSocket错误事件 + this.socketTask.onError((err) => { + console.error('WebSocket发生错误', err); + clearInterval(this.heartbeatTimer); // 清除心跳计时器 + this.triggerEvent('error', err); // 触发错误事件 + this.tryReconnect(); // 尝试重连 + }); + } + + /** + * 注册事件回调 + * @param {string} event - 事件名称(open/message/close/error) + * @param {Function} callback - 回调函数 + * @returns {WebSocketUtil} - 返回当前实例,支持链式调用 + */ + on(event, callback) { + if (this.callbacks[event]) { + this.callbacks[event].push(callback); + } + return this; + } + + /** + * 移除事件回调 + * @param {string} event - 事件名称(open/message/close/error) + * @param {Function} callback - 要移除的回调函数 + * @returns {WebSocketUtil} - 返回当前实例,支持链式调用 + */ + off(event, callback) { + if (this.callbacks[event]) { + this.callbacks[event] = this.callbacks[event].filter(cb => cb !== callback); + } + return this; + } + + /** + * 触发事件回调 + * @param {string} event - 事件名称 + * @param {any} [data] - 传递给回调函数的数据 + */ + triggerEvent(event, data) { + if (this.callbacks[event]) { + this.callbacks[event].forEach(callback => callback(data)); + } + } + + /** + * 通过WebSocket发送消息 + * @param {string|Object} message - 要发送的消息,可以是字符串或对象 + * @returns {WebSocketUtil} - 返回当前实例,支持链式调用 + */ + send(message, num = 1) { + if (this.socketTask && this.getStatus() === 1) { + // 发送消息,对象会自动转换为JSON字符串 + this.socketTask.send({ + data: typeof message === 'string' ? message : JSON.stringify(message), + success: () => { + // console.log('WebSocket消息发送成功'); + }, + fail: (err) => { + console.error('WebSocket消息发送失败', err); + this.triggerEvent('error', err); + } + }); + } else { + console.error('WebSocket连接未打开,无法发送消息'); + this.triggerEvent('error', new Error('WebSocket连接未打开')); + if(num === 10) return + setTimeout(()=>{ + this.send(message, num + 1) + },500) + } + return this; + } + + /** + * 关闭WebSocket连接 + * @param {number} [code=1000] - 关闭码 + * @param {string} [reason=''] - 关闭原因 + * @returns {WebSocketUtil} - 返回当前实例,支持链式调用 + */ + close(code = 1000, reason = '') { + this.isClose = true + if (this.socketTask) { + clearInterval(this.heartbeatTimer); // 清除心跳计时器 + clearInterval(this.reconnectTimer); // 清除重连计时器 + // 关闭WebSocket连接 + this.socketTask.close({ + code, + reason, + success: () => { + console.log('WebSocket连接正在关闭'); + }, + fail: (err) => { + console.error('WebSocket关闭失败', err); + this.triggerEvent('error', err); + } + }); + } + return this; + } + + /** + * 获取WebSocket连接状态 + * @returns {number} - 连接状态:0-连接中,1-已连接,2-连接关闭中,3-已关闭,-1-未知 + */ + getStatus() { + if (this.socketTask) { + try { + return this.socketTask.readyState; + } catch (e) { + console.error('获取WebSocket状态失败', e); + return -1; + } + } + return -1; + } + + /** + * 尝试重新连接WebSocket + */ + tryReconnect() { + if(this.isClose) return + if (this.reconnectCount < this.maxReconnectCount) { + clearInterval(this.reconnectTimer); + this.reconnectTimer = setTimeout(() => { + this.reconnectCount++; + console.log(`尝试重新连接WebSocket (${this.reconnectCount}/${this.maxReconnectCount})`); + this.init(); // 重新初始化WebSocket连接 + }, this.reconnectInterval); + } else { + console.error('达到最大重连次数,停止尝试'); + this.triggerEvent('error', new Error('达到最大重连次数')); + this.reconnectTimer = null + this.close() + goto_login_fun('socket') + } + } + + /** + * 启动心跳机制 + */ + startHeartbeat() { + clearInterval(this.heartbeatTimer); + this.heartbeatTimer = setInterval(() => { + if (this.getStatus() === 1) { + this.send(this.heartbeatMsg); // 发送心跳消息 + } + }, this.heartbeatInterval); + } +} \ No newline at end of file diff --git a/tra-app/src/api/sparring.js b/tra-app/src/api/sparring.js new file mode 100644 index 0000000..2b9c8f0 --- /dev/null +++ b/tra-app/src/api/sparring.js @@ -0,0 +1,92 @@ +import request from '@/api/request' + +const base_url = '/trapractice' + +// 获取分类树接口 +export const queryTraPartnerCatelogList = (data) => { + return request({ + url: base_url + '/traPartnerCatelog/queryTraPartnerCatelogList', + method: 'post', + data + }); +}; +// 获取标签分类树 接口 +export const getTraTagListApi = (data) => { + return request({ + url: base_url + '/traPartnerTag/queryTraTagCatalogInfoList', + method: 'post', + data + }); +}; +// 全部模块 +export const queryCrsInfoByCatalogApi = (data) => { + return request({ + url: base_url + '/traPartnerInfo/queryTraPartnerInfoPaging', + method: 'post', + data + }); +}; +// id 查详情/trapractice/traPartnerInfo/queryTraPartnerInfoById +export const queryTraPartnerInfoById = (data) => { + return request({ + url: base_url + '/traPartnerInfo/queryTraPartnerInfoById', + method: 'post', + data + }); +}; +// 陪练角色列表 traId +export const queryTraPartnerCharacterInfoList = (data) => { + return request({ + url: base_url + '/traPartnerCharacterInfo/queryTraPartnerCharacterInfoList', + method: 'post', + data + }); +}; +// 陪练角色详情 + 对话详情 /traPartnerCharacterInfo/queryTraPartnerCharacterInfoById +export const queryTraPartnerCharacterInfoById = (data) => { + return request({ + url: base_url + '/traPartnerCharacterInfo/queryTraPartnerCharacterInfoById', + method: 'post', + data + }); +}; +// 结束陪练 /trapractice/traPartnerChatReport/partnerChatReportTrigger?execId= +export const partnerChatReportTrigger = (data) => { + return request({ + url: base_url + '/traPartnerChatReport/partnerChatReportTrigger', + method: 'post', + data + }); +}; +// 问答提示/trapractice/traPartnerChat/partnerChatPrompt +export const partnerChatPrompt = (data) => { + return request({ + url: base_url + '/traPartnerChat/partnerChatPrompt', + method: 'post', + data + }); +}; +// 获取报告traPartnerChatReport/partnerChatReport +export const partnerChatReport = (data) => { + return request({ + url: base_url + '/traPartnerChatReport/partnerChatReport', + method: 'post', + data + }); +}; +// 反馈 +export const insertTraPartnerFeedback = (data) => { + return request({ + url: base_url + '/traPartnerFeedback/insertTraPartnerFeedback', + method: 'post', + data + }); +}; +// 查询是否可以进入trapractice/traPartnerInfo/checkTraPartnerInfoById +export const checkTraPartnerInfoById = (data) => { + return request({ + url: base_url + '/traPartnerInfo/checkTraPartnerInfoById', + method: 'post', + data + }); +}; \ No newline at end of file diff --git a/tra-app/src/api/study.js b/tra-app/src/api/study.js new file mode 100644 index 0000000..eec4f21 --- /dev/null +++ b/tra-app/src/api/study.js @@ -0,0 +1,206 @@ +import request from '@/api/request' +import { getToken } from '@/common/common.js' +import { get_base_url } from '@/api/request' +const base_url = '/trastudy' + +// 获取课程学习信息接口 +export const getCourseStudyInfoApi = (data) => { + return request({ + url: base_url + '/traStdyInfo/queryTraStdyProgressAndTeacher', + method: 'post', + toastErrors: true, + data + }); +}; +// 获取课程学习 知识点 接口 +export const queryTraStdyBatchWaitParagraphInfoApi = (data) => { + return request({ + url: base_url + '/traStdyInfo/queryTraStdyBatchWaitParagraphInfo', + method: 'post', + toastErrors: true, + data + }); +}; +// 重新开始 删除学习信息 +export const deleteTraStdyInfoByIdApi = (data) => { + return request({ + url: base_url + '/traStdyInfo/deleteTraStdyInfoById', + method: 'post', + toastErrors: true, + data + }); +}; + +// 重新学习 +export const afreshStudyCourseApi = (data) => { + return request({ + url: base_url + '/traStdyInfo/afreshStudyCourse', + method: 'post', + toastErrors: true, + data + }); +}; +// 学习段落完成 +export const studyParagraphOverApi = (data) => { + return request({ + url: base_url + '/traStdyInfo/studyParagraphOver', + method: 'post', + toastErrors: true, + data + }); +}; +// 回答问题 /traStdyInfo/studyAnswerTextUp +export const studyAnswerTextUpApi = (data) => { + return request({ + url: base_url + '/traStdyInfo/studyAnswerTextUp', + method: 'post', + toastErrors: true, + data + }); +}; +// 获取问题信息/traStdyInfo/queryTraStdyBatchWaitQuestionInfo +export const queryTraStdyBatchWaitQuestionInfoApi = (data) => { + return request({ + url: base_url + '/traStdyInfo/queryTraStdyBatchWaitQuestionInfo', + method: 'post', + toastErrors: true, + data + }); +}; +// 上传语音翻译i文本 /traStdyInfo/audioTranscriptions +export const audioTranscriptionsApi = (data) => { + return request({ + url: base_url + '/traStdyInfo/audioTranscriptions', + method: 'post', + toastErrors: true, + data + }); +}; + +// 撤回消息 +export const withdrawalAnswerApi = (data) => { + return request({ + url: base_url + '/traStdyInfo/withdrawalAnswer', + method: 'post', + toastErrors: true, + data + }); +}; +// 异步完成回答 +// 入参: +// qstLogId 必输项:true 类型:String +// 出参: +// qstLogId 问题日志ID String +// qnsId 问题ID String +// processStat 状态 String +// message 处理消息 String + +export const waitMakeEvaluateApi = (data) => { + return request({ + url: base_url + '/traStdyInfo/waitMakeEvaluate', + method: 'post', + toastErrors: true, + data + }); +}; +// 查询问题评价 +// /traStdyInfo/queryQuestionEvalate +export const queryQuestionEvalateApi = (data) => { + return request({ + url: base_url + '/traStdyInfo/queryQuestionEvalate', + method: 'post', + toastErrors: true, + data + }); +}; + + +// /trastudy/intgask/askAbout +// 流式获取文本 +export const getAskAboutRawApi = (data) => { + // return request({ + // baseUrl:'http://25.18.122.76:3030', + // url: '/api/v1/prediction/a402cf58-9f64-479b-a9f8-c527300a7905', + // isStream: true, + // header: { + // 'Accept':'text/event-stream', + // 'content-type': 'application/json; charset=UTF-8', + // 'Authorization': 'Bearer PjadGPFSHDg6F8ZRekROe-QxS2fPXlWRsmvP5Mp0vxA' + // }, + // method: 'post', + // toastErrors: false, + // data + // }); + return request({ + url: base_url + '/intgask/askAbout', + header: { + 'Accept':'text/event-stream', + 'content-type': 'text/event-stream; charset=UTF-8', + }, + responseType: 'arrayBuffer', + method: 'get', + isStream: true, + timeout: 30000, + toastErrors: false, + data + }) +}; + +// 学习开始 +export const studyStartApi = (data) => { + return request({ + url: base_url + '/traStdychat/studyStart', + method: 'post', + toastErrors: true, + data + }); +}; +// 学习结束 +export const studyEndApi = (data) => { + return request({ + url: base_url + '/traStdychat/studyEnd', + method: 'post', + toastErrors: true, + data + }); +}; + +// 学习 对话 +export const textChartApi = (data) => { + return request({ + url: base_url + '/traStdychat/textChart', + method: 'post', + toastErrors: true, + data + }); +}; + +// 预览 对话 +export const textChartPreviewApi = (data) => { + return request({ + url: base_url + '/traStdyInfoPreview/previewCrsInfo', + method: 'post', + toastErrors: true, + data + }); +}; + + +export const previewAfreshStudyCourse = (data) => { + return request({ + url: base_url + '/traStdyInfo/previewAfreshStudyCourse', + method: 'post', + toastErrors: true, + data + }); +}; + +// 设置学习顾问音色/traStdyTeacher/setStudyTeacher +export const setStudyTeacherApi = (data) => { + return request({ + url: base_url + '/traStdyTeacher/setStudyTeacher', + method: 'post', + toastErrors: true, + data + }); +}; \ No newline at end of file diff --git a/tra-app/src/api/talk.js b/tra-app/src/api/talk.js new file mode 100644 index 0000000..357ea49 --- /dev/null +++ b/tra-app/src/api/talk.js @@ -0,0 +1,11 @@ +import request from '@/api/request' + +export const prepareApi = (data) => { + return request({ + url: '/trapractice/traPartnerVoiceCall/voiceCallPrepare' , + method: 'post', + toastErrors: true, + data, + baseUrl: 'http://25.64.32.157:9603' + }); +}; \ No newline at end of file diff --git a/tra-app/src/api/testingHall.js b/tra-app/src/api/testingHall.js new file mode 100644 index 0000000..01062cb --- /dev/null +++ b/tra-app/src/api/testingHall.js @@ -0,0 +1,13 @@ +import request from '@/api/request' + +const module_url = '/traexam' + +// 1.查询考试列表分页 +export const queryTraExamPapersPage = (data) => { + return request({ + url: module_url + '/traExamPapers/queryTraExamPapersPage', + method: 'post', + data + }); +}; + diff --git a/tra-app/src/api/traRecord.js b/tra-app/src/api/traRecord.js new file mode 100644 index 0000000..761429b --- /dev/null +++ b/tra-app/src/api/traRecord.js @@ -0,0 +1,58 @@ +import request from '@/api/request' +const base_url = '/trapractice' + + +/**练习*/ +// 练习列表页 +export const queryPracticeRecordPaging = (data) => { + return request({ + url: base_url + '/traPartnerExecuteHis/queryTraPartnerExecutePaging', + method: 'post', + toastErrors: true, + data:{ + traType:'01', + ...data + } + }); +}; +// 练习每一项列表 +export const queryPracticeHis = (data) => { + return request({ + url: base_url + '/traPartnerExecuteHis/queryTraPartnerExecuteHisPaging', + method: 'post', + toastErrors: true, + data + }); +}; +// 练习详情 通过 exrId 查询 +export const queryPracticeRecordByExrId = (data) => { + return request({ + url: base_url + '/traCrsPractice/queryPracticeRecordByExrId', + method: 'post', + toastErrors: true, + data + }); +}; + +/**考试*/ +export const queryCrsExamRecordPaging = (data) => { + return request({ + url: base_url + '/traPartnerExecuteHis/queryTraPartnerExecutePaging', + method: 'post', + toastErrors: true, + data:{ + traType:'02', + ...data + } + }); +}; +// 考试每一项列表 +export const queryCrsExamHisByExamId = (data) => { + return request({ + url: base_url + '/traPartnerExecuteHis/queryTraPartnerExecuteHisPaging', + method: 'post', + toastErrors: true, + data + }); +}; + diff --git a/tra-app/src/api/user.js b/tra-app/src/api/user.js new file mode 100644 index 0000000..b4a180b --- /dev/null +++ b/tra-app/src/api/user.js @@ -0,0 +1,57 @@ +import request from '@/api/request' + + +// 我的-统计信息 +export const queryTraStdyStatisticsSelf = (data) => { + /** + userId 学员ID String + stdtTmLen 累计学习时长 double + accmTaskCnt 累计完成任务 int + accmPoint 累计获得积分 int + **/ + return request({ + url: '/traapp/traStdyInfo/queryTraStdyStatisticsSelf', + method: 'post', + data + }); +}; + +// 更新性别 +export const updateDfSysUserExtandInfoGender = (data) => { + /** + + **/ + return request({ + url: '/traapp/dfSysUserInfo/updateDfSysUserExtandInfoGender ', + method: 'post', + data + }); +}; + + // 更新照片 + export const updateDfSysUserExtandInfoImageAddr = (data) => { + return request({ + url: '/traapp/dfSysUserInfo/updateDfSysUserExtandInfoImageAddr', + method: 'post', + data + }); + }; + +// 设置匿名 是Y 否N +export const updateTraUserAnony = (data) => { + return request({ + url: '/traapp/traUserAnony/updateTraUserAnony', + method: 'post', + data + }); +}; + +// 查询是否匿名 是Y 否N +export const queryTraUserAnonyByUserId = (data) => { + return request({ + url: '/traapp/traUserAnony/queryTraUserAnonyByUserId', + method: 'post', + data + }); +}; + diff --git a/tra-app/src/api/wrongQuestionRecord.js b/tra-app/src/api/wrongQuestionRecord.js new file mode 100644 index 0000000..305c8f3 --- /dev/null +++ b/tra-app/src/api/wrongQuestionRecord.js @@ -0,0 +1,54 @@ +import request from '@/api/request' +const base_url = '/traexam' + +// 错题本列表页 +export const queryTraMistakesCollectionPaging = (data) => { + return request({ + url: base_url + '/traMistakesCollection/queryTraMistakesCollectionPaging', + method: 'post', + toastErrors: true, + data + }); +}; + +// 错题订正 +export const mistakesCorrect = (data) => { + return request({ + url: base_url + '/traMistakesCollection/mistakesCorrect', + method: 'post', + toastErrors: true, + data + }); +}; + + +// 错题本id集合 +export const queryTraMistakesCollection = (data) => { + return request({ + url: base_url + '/traMistakesCollection/queryTraMistakesCollection', + method: 'post', + toastErrors: true, + data + }); +}; + +// 根据ID查题 +export const queryMistakesAnswerByMisId = (data) => { + return request({ + url: base_url + '/traMistakesCollection/queryMistakesAnswerByMisId ', + method: 'post', + toastErrors: false, + data + }); +}; + +// 根据ID查题 +export const queryMistakesCorrectByMisId = (data) => { + return request({ + url: base_url + '/traMistakesCollection/queryMistakesCorrectByMisId', + method: 'post', + toastErrors: false, + data + }); +}; + diff --git a/tra-app/src/common/common.js b/tra-app/src/common/common.js new file mode 100644 index 0000000..15175f2 --- /dev/null +++ b/tra-app/src/common/common.js @@ -0,0 +1,385 @@ +import { + get_base_url +} from '@/api/request'; +// #ifdef APP-PLUS +import showDialog from '@/common/dialogMessage' +import common from '@/common/common' +// #endif +function msg(title, duration = 1000) { + uni.showToast({ + title: title, + duration: duration, + icon: 'none', + mask: true, + }); +} + +function show(title, msg, showCancel = true, cancelText = '取消', confirmText = '确定') { + return new Promise((resolve, reject) => { + // 创建通用参数对象 + const dialogOptions = { + title, + content: msg, + showCancel, + cancelText, + confirmText, + success: (res) => { + // 统一处理成功回调,解析为布尔值 + resolve(!!res.confirm); + } + }; + // #ifdef APP-PLUS + if (getPhoneEnvBool('AND')) { + // 安卓环境使用showDialog,补充cancel回调 + showDialog({ + ...dialogOptions, + cancel: () => { + console.log('用户点击取消'); + resolve(false); // 取消时也返回Promise结果 + } + }); + } else { + // 其他环境使用uni.showModal,补充fail回调 + uni.showModal({ + ...dialogOptions, + fail: (res) => { + reject(res); + } + }); + } + // #endif + // #ifdef H5 + uni.showModal({ + ...dialogOptions, + fail: (res) => { + reject(res); + } + }); + // #endif + }); +} + +const loading = (title = '加载中', mask = true) => uni.showLoading({ + title: title, + mask: mask +}) +const hideLoading = () => uni.hideLoading() + +// 根据姓名的长度生成对应的带星号格式 +export const formatName = (name) => { + + // 处理空值或非字符串情况 + if (!name || typeof name !== 'string') { + return ''; + } + + const familyName = name.charAt(0); // 取姓氏(第一个字符) + const givenNameLength = name.length - 1; // 名字部分的长度 + + // 根据名字长度计算星号数量,最多4个 + const starCount = Math.min(givenNameLength, 3); + const stars = '*'.repeat(starCount); + + return familyName + stars; +} + +// 防抖工具函数 +function debounce(fn, wait = 300) { + let isLocked = false; // 锁定状态标记 + return function(...args) { + // 如果处于锁定状态,直接返回不执行 + if (isLocked) { + return; + } + // 执行函数 + fn.apply(this, args); + + // 锁定,防止再次执行 + isLocked = true; + + // 等待指定时间后解锁 + setTimeout(() => { + isLocked = false; + }, wait); + }; +} + +// 保留当前页面,跳转到应用内的某个页面,使用uni.navigateBack可以返回到原页面。 +const navigateTo = debounce(function(url, data = {}) { + uni.navigateTo({ + url: url, + ...data + }); +}) + +// 关闭所有页面,打开到应用内的某个页面。 +const reLaunch = url => { + uni.reLaunch({ + url: url + }); +} +// 跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面。 +const switchTab = url => { + uni.switchTab({ + url: url + }); +} +// 关闭当前页面,返回上一页面或多级页面。可通过 getCurrentPages() 获取当前的页面栈,决定需要返回几层。 +const navigateBack = (delta = 1, fun = () => {}, data = {}) => { + uni.navigateBack({ + delta: delta, + ...data, + success() { + fun() + } + }) +} + +//关闭当前页面,跳转到应用内的某个页面。 +const redirectTo = (url, obj = {}) => uni.redirectTo({ + url: url, + ...obj +}); + +// 时间转换时间戳 +const dateToTime = (dateString) => { + let date = new Date(dateString); + let timestamp = date.getTime(); + return timestamp +} + +// 数字格式化 +const formatDate2 = (timestamp, format = 'yyyy-MM-dd HH:mm:ss') => { + if (!timestamp) + return '' + let date + try { + if (timestamp) { + if (typeof timestamp == 'number' && timestamp <= 9999999999) { + timestamp *= 1000 + } + date = new Date(timestamp) + } else { + date = new Date() + } + } catch (e) { + return '' + } + let day = date.getDate() + let month = date.getMonth() + 1 + let year = date.getFullYear() + let hours = date.getHours() + let minutes = date.getMinutes() + let seconds = date.getSeconds() + + // 替换日期格式中的占位符 + return format + .replace('yyyy', year) + .replace('MM', month.toString().padStart(2, '0')) + .replace('dd', day.toString().padStart(2, '0')) + .replace('HH', hours.toString().padStart(2, '0')) + .replace('mm', minutes.toString().padStart(2, '0')) + .replace('ss', seconds.toString().padStart(2, '0')) +} + + + +// 获取token +export const getToken = () => uni.getStorageSync('token') +export const setToken = data => uni.setStorageSync('token', data) +export const setUserInfo = data => uni.setStorageSync('userInfo', data) +export const getUserInfo = (key = '') => { + const userInfo = uni.getStorageSync('userInfo') + return '' === key ? userInfo : userInfo[key]; +} +// 数字转换,把传入的秒转换为HH:mm:ss格式 +export const formatSeconds = (seconds) => { + // 处理非数字或负数情况 + if (typeof seconds !== 'number' || isNaN(seconds) || seconds < 0) { + return '00:00:00'; + } + + // 计算小时、分钟和剩余秒数 + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const remainingSeconds = Math.floor(seconds % 60); + + // 补零函数:将数字转为两位数字符串 + const padZero = (num) => num.toString().padStart(2, '0'); + + // 拼接成00:00:00格式 + return `${padZero(hours)}:${padZero(minutes)}:${padZero(remainingSeconds)}`; +}; + +/** + * 判断当前运行平台是否匹配指定标识 + * @param {string} flag - 平台标识:'IOS' 表示苹果iOS平台,'AND' 表示安卓平台 + * @returns {boolean} 如果当前平台匹配指定标识返回true,否则返回false + */ +export const getPhoneEnvBool = (flag) => { + // 获取当前设备的平台信息 + + const { + platform + } = uni.getSystemInfoSync(); + + // 平台与标识的映射关系 + const platformMap = { + ios: 'IOS', + android: 'AND', + }; + + // 根据当前平台返回匹配结果 + return platformMap[platform] === flag; +}; + + +// 将对象和字符串相互转换 +export const objToJson = obj => encodeURLComponent('page_data', obj) +export const jsonToObj = () => uni.getStorageSync('page_data') + +// 将对象存入缓存 +export const setPageCache = (name, obj) => { + uni.setStorageSync(name, obj) +} + +// 将对象取出并且删除缓存 +export const getPageCache = name => { + const cache = uni.getStorageSync(name) + uni.removeStorageSync(name) + return cache +} + +function setValue(key, value) { + uni.setStorageSync(key, value); +} + +function getValue(key, defaultValue = null) { + return uni.getStorageSync(key) ?? defaultValue +} + +function isLogin() { + return !!getToken(); +} + +// 封装键盘高度变化处理 +export function setupKeyboardHeightListener(updateCallback) { + // #ifndef APP-PLUS + return () => {}; + // #endif + // #ifdef APP-PLUS + // 获取系统信息 + const systemInfo = uni.getSystemInfoSync(); + // 计算安全区域底部高度 + const safeAreaBottomHeight = systemInfo.screenHeight - systemInfo.safeArea.bottom; + + // 定义处理函数 + const handleKeyboardHeightChange = (res) => { + let keyboardHeight = res.height; + + // 仅在iOS端补偿安全区域高度 + if (systemInfo.platform === 'ios') { + keyboardHeight = Math.max(0, keyboardHeight - safeAreaBottomHeight); // 避免负数 + } + + // 通过回调函数更新外部的响应式变量 + updateCallback(keyboardHeight); + }; + + // 监听键盘高度变化 + uni.onKeyboardHeightChange(handleKeyboardHeightChange); + + // 返回取消监听的函数,方便组件卸载时清理 + return () => { + uni.offKeyboardHeightChange(handleKeyboardHeightChange); + }; + // #endif + +} + +let isDownloading = false +export const previewFileFnc = ({ + fileId, + getFileSrc, + fileName +}) => { + let userInfo = getUserInfo() + let userParams = '/' + (userInfo?.loginName ?? '') + navigateTo('/pages/index/preview?fileId=' + fileId + '&fileApiSrc=' + getFileSrc); + // if (getPhoneEnvBool('AND')) { + // navigateTo('/pages/index/preview?fileId=' + fileId + '&fileApiSrc=' + getFileSrc); + // } else { + // if (isDownloading) return + // const base_url = get_base_url(getFileSrc); + // let fileUrl = base_url + getFileSrc + fileId + userParams; + // const localFilePath = '_doc/pdf_view/' + fileName + '.pdf'; + // isDownloading = true + // downloadFileFnc(fileUrl, localFilePath).then((res) => { + // uni.openDocument({ + // filePath: res, + // showMenu: true, + // success: function() { + // console.log('打开文档成功'); + // } + // }) + // }).finally(() => { + // setTimeout(() => { + // isDownloading = false + // }, 1000) + // }); + // } +} +const downloadFileFnc = async (fileUrl, localFilePath) => { + try { + //#ifdef APP-PLUS + return new Promise((resolve, reject) => { + console.log('开始下载:', fileUrl); + const dtask = plus.downloader.createDownload( + fileUrl, { + filename: localFilePath + }, + (d, status) => { + if (status === 200) { + console.log('保存路径:', d.filename); + resolve(d.filename); + } else { + console.error('文件下载失败:', status); + reject(new Error(`下载失败,状态码: ${status}`)); + } + } + ); + dtask.start(); + }); + // #endif + //#ifdef H5 + return new Promise((resolve, reject) => { + resolve(); + }); + // #endif + } catch (error) { + console.error('创建目录失败:', error); + throw error; + } +}; + + +export default { + getPageCache, + setPageCache, + objToJson, + jsonToObj, + hideLoading, + loading, + msg, + show, + redirectTo, + navigateTo, + navigateBack, + formatDate2, + setValue, + getValue, + isLogin, + reLaunch, + switchTab, + dateToTime, +} \ No newline at end of file diff --git a/tra-app/src/common/dialogMessage.js b/tra-app/src/common/dialogMessage.js new file mode 100644 index 0000000..b2451f0 --- /dev/null +++ b/tra-app/src/common/dialogMessage.js @@ -0,0 +1,225 @@ +export default function showDialog(options) { + + const view = new plus.nativeObj.View('customModal', { + top: '0', + left: '0', + height: '100%', + width: '100%', + backgroundColor: 'rgba(0,0,0,0.1)', + position: 'fixed' + }); + + const screenWidth = plus.screen.resolutionWidth; + const screenHeight = plus.screen.resolutionHeight; + const borderRadius = 10; + const contentPadding = 12; // 内容左右内边距 + const lineHeight = 17; // 单行文本行高 + const maxLines = 2; // 最大行数 + const fontSize = 14; // 字体大小 + + // 判断内容和标题是否存在 + const hasContent = options.content && options.content.trim() !== ''; + const hasTitle = options.title && options.title.trim() !== ''; + + // 弹窗宽度计算 + const modalWidth = Math.min(0.65 * screenWidth, 500); + const modalLeft = (screenWidth - modalWidth) / 2; + + // 内容区域可用宽度 + const contentAvailableWidth = modalWidth - 2 * contentPadding - 10; + + // 动态计算各部分高度 + const titleHeight = hasTitle ? 20 : 0; + const titleToContentSpacing = (hasTitle && hasContent) ? 10 : 0; + const contentHeight = hasContent ? (lineHeight * maxLines) : 0; + const contentToBtnSpacing = 10; + const btnHeight = 46; + + // 弹窗总高度 + let modalHeight = contentPadding + 6 + titleHeight + titleToContentSpacing + contentHeight + contentToBtnSpacing + btnHeight; + modalHeight = options.showCancel ? Math.min(modalHeight, 300) : Math.min(modalHeight, 250); + + // 位置计算 + const modalTop = (screenHeight - modalHeight) / 2; + const contentAreaTop = modalTop + contentPadding + 6; + const titleTop = hasTitle ? contentAreaTop : 0; + + + let contentTop; + if (hasContent) { + contentTop = hasTitle ? (titleTop + titleHeight + titleToContentSpacing) : contentAreaTop; + } else { + contentTop = hasTitle ? (titleTop + titleHeight) : contentAreaTop; + } + + const btnTop = hasContent + ? (contentTop + contentHeight + contentToBtnSpacing) + : (hasTitle + ? (titleTop + titleHeight + contentToBtnSpacing) + : (contentAreaTop + (modalHeight - btnHeight - contentPadding))); // 既无标题也无内容时 + + // 绘制弹窗背景 + view.draw([{ + tag: 'rect', + id: 'bg', + position: { top: modalTop + 'px', left: modalLeft + 'px', width: modalWidth + 'px', height: modalHeight + 'px' }, + rectStyles: { color: '#fff', radius: borderRadius + 'px' } + }]); + + // 绘制标题和内容 + const elements = []; + + // 标题(仅当存在时) + if (hasTitle) { + elements.push({ + tag: 'font', + id: 'title', + text: options.title, + position: { top: titleTop + 'px', left: modalLeft + 'px', width: modalWidth + 'px', height: titleHeight + 'px' }, + textStyles: { + color: '#000', + size: '18px', + align: 'center', + fontWeight: 'bold', + verticalAlign: 'middle' + } + }); + } + if (hasContent) { + const text = options.content; + const maxLines = 2; // 最大显示行数 + const lineHeightPx = lineHeight; + const leftPos = modalLeft + contentPadding; + const textWidth = contentAvailableWidth; + const fontSizePx = fontSize; + + // 按 \n 分割段落 + const paragraphs = text.split('\n'); + let lines = []; + + // 遍历每个段落,计算换行 + for (let para of paragraphs) { + if (lines.length >= maxLines) break; + + // 估算每行能容纳的字符数(中文按 1.2 倍宽度计算) + const avgCharWidth = fontSizePx * (isChinese(para) ? 1.2 : 0.6); + const maxCharsPerLine = Math.floor(textWidth / avgCharWidth); + + let currentLine = ''; + for (let i = 0; i < para.length; i++) { + const char = para[i]; + currentLine += char; + + // 如果当前行字符数超过限制,换行 + if (currentLine.length >= maxCharsPerLine) { + lines.push(currentLine); + currentLine = ''; + + if (lines.length >= maxLines) break; + } + } + + // 添加剩余部分 + if (currentLine && lines.length < maxLines) { + lines.push(currentLine); + } + } + + // 如果超出最大行数,最后一行加 "..." + if (lines.length > maxLines) { + lines = lines.slice(0, maxLines); + const lastLine = lines[maxLines - 1]; + lines[maxLines - 1] = lastLine.substring(0, lastLine.length - 3) + '...'; + } + + // 绘制每一行文本(确保行高正确) + lines.forEach((line, index) => { + const topPos = contentTop + index * lineHeightPx; + + elements.push({ + tag: 'font', + id: 'content_line_' + index, + text: line, + position: { + top: topPos + 'px', + left: leftPos + 'px', + width: textWidth + 'px', + height: lineHeightPx + 'px' + }, + textStyles: { + color: '#111', + size: fontSizePx + 'px', + align: 'center', + verticalAlign: 'top', + lineHeight: lineHeightPx + 'px' + } + }); + }); + } + + // 判断是否主要是中文 + function isChinese(text) { + return /[\u4e00-\u9fa5]/.test(text); + } + + // 分割线 + elements.push({ + tag: 'rect', + id: 'divider', + position: { top: btnTop + 'px', left: modalLeft + 'px', width: modalWidth + 'px', height: '0.5px' }, + rectStyles: { color: '#b3b3b3' } + }); + + view.draw(elements); + + // 绘制按钮(不变) + if (options.showCancel) { + const btnWidth = Math.floor((modalWidth - 2 * borderRadius) / 2); + view.draw([ + { tag: 'rect', id: 'cancelBtn', position: { top: (btnTop + 2) + 'px', left: (modalLeft + borderRadius) + 'px', width: btnWidth + 'px', height: (btnHeight - 3) + 'px' }, rectStyles: { color: '#fff' } }, + { tag: 'font', id: 'cancelText', text: options.cancelText || '取消', position: { top: btnTop + 'px', left: (modalLeft + borderRadius) + 'px', width: btnWidth + 'px', height: btnHeight + 'px' }, textStyles: { color: '#007AFF', size: '16px', align: 'center', verticalAlign: 'middle' } }, + { tag: 'rect', id: 'btnDivider', position: { top: btnTop + 'px', left: (modalLeft + borderRadius + btnWidth) + 'px', width: '0.5px', height: btnHeight + 'px' }, rectStyles: { color: '#b3b3b3' } }, + { tag: 'rect', id: 'confirmBtn', position: { top: (btnTop + 2) + 'px', left: (modalLeft + borderRadius + btnWidth + 1) + 'px', width: btnWidth + 'px', height: (btnHeight - 3) + 'px' }, rectStyles: { color: '#fff' } }, + { tag: 'font', id: 'confirmText', text: options.confirmText || '确定', position: { top: btnTop + 'px', left: (modalLeft + borderRadius + btnWidth + 1) + 'px', width: btnWidth + 'px', height: btnHeight + 'px' }, textStyles: { color: '#007AFF', size: '16px', align: 'center', verticalAlign: 'middle' } } + ]); + } else { + view.draw([ + { tag: 'rect', id: 'confirmBtn', position: { top: (btnTop + 2) + 'px', left: (modalLeft + borderRadius) + 'px', width: (modalWidth - 2 * borderRadius) + 'px', height: (btnHeight - 3) + 'px' }, rectStyles: { color: '#fff' } }, + { tag: 'font', id: 'confirmText', text: options.confirmText || '确定', position: { top: btnTop + 'px', left: (modalLeft + borderRadius) + 'px', width: (modalWidth - 2 * borderRadius) + 'px', height: btnHeight + 'px' }, textStyles: { color: '#007AFF', size: '16px', align: 'center', verticalAlign: 'middle' } } + ]); + } + + // 点击事件(不变) + view.addEventListener("click", function(e) { + const x = e.clientX; + const y = e.clientY; + if (options.showCancel) { + const btnWidth = Math.floor((modalWidth - 2 * borderRadius) / 2); + const cancelBtnLeft = modalLeft + borderRadius; + if (y > btnTop && y < btnTop + btnHeight) { + if (x >= cancelBtnLeft && x < cancelBtnLeft + btnWidth) { + options.cancel?.(); + view.close(); + } else if (x >= cancelBtnLeft + btnWidth + 1 && x < cancelBtnLeft + 2 * btnWidth + 1) { + options.success?.({ confirm: true }); + view.close(); + } + } + } else { + const btnLeft = modalLeft + borderRadius; + if (y > btnTop && y < btnTop + btnHeight && x >= btnLeft && x < btnLeft + (modalWidth - 2 * borderRadius)) { + options.success?.({ confirm: true }); + view.close(); + } + } + }, false); + + view.show(); + + return { + close: () => { + view.close(); + } + }; + +} \ No newline at end of file diff --git a/tra-app/src/common/imgSvg.ts b/tra-app/src/common/imgSvg.ts new file mode 100644 index 0000000..ae7f695 --- /dev/null +++ b/tra-app/src/common/imgSvg.ts @@ -0,0 +1,127 @@ +export const optionSuccessIcon = (color : string) : string => { + const svgXml = ` + + `.trim(); + + return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`; +}; +export const optionErrorIcon = (color : string = '#F5212D') : string => { + const svgXml = ` + + `.trim(); + return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`; +}; + +export const examinationSheetIcon = () : string => { + const svgXml = ` + + `.trim(); + return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`; +}; + +export const markIcon = (color : string = '#ABABAB') : string => { + const svgXml = ` + + `.trim(); + return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`; +}; + +export const feedbackIcon = () : string => { + const svgXml = ` + + `.trim(); + return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`; +}; + +// 通过考试 +export const passTheExamIcon = (color : string) : string => { + const svgXml = ` + +`.trim(); + return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`; +}; + +// 完成练习 +export const completeTheExercisesIcon = (color : string) : string => { + const svgXml = ` + +`.trim(); + return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`; +}; + +// 锁定 +export const lockIcon = (color : string) : string => { + const svgXml = ` + +`.trim(); + return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`; +}; + + +// 练习退出图标 +export const practiceExitIcon = () : string => { + const svgXml = ` + +`.trim(); + return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`; +}; + + +// Pk页规则图标 +export const pkRules = () : string => { + const svgXml = ` + +`.trim(); + return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`; +}; + +// Pk页排行榜图标 +export const pkRankingList = () : string => { + const svgXml = ` + +`.trim(); + return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`; +}; + +// Pk页匿名关 +export const pkAnonymousClose = () : string => { + const svgXml = ` + +`.trim(); + return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`; +}; + +// Pk页匿名开 +export const pkAnonymousOpen = () : string => { + const svgXml = ` + +`.trim(); + return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`; +}; + + +// 重置页图标 +export const resetIcon = () : string => { + const svgXml = ` + +`.trim(); + return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`; +}; + + +// pk页切换图标 +export const switchIcon = () : string => { + const svgXml = ` + +`.trim(); + return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`; +}; + + +// 左右切换箭头 +export const arrowRightIcon = (color : string = '#F5212D') : string => { + const svgXml = ` + +`.trim(); + return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`; +}; \ No newline at end of file diff --git a/tra-app/src/common/mascot.js b/tra-app/src/common/mascot.js new file mode 100644 index 0000000..1adf039 --- /dev/null +++ b/tra-app/src/common/mascot.js @@ -0,0 +1,28 @@ +// 设置科技鹿 +import { + getUserInfo +} from "@/common/common.js" +import common from "@/common/common.js" +export const setMascot = () => { + const mascotState = false + if (!mascotState) { + try { + const userInfo = getUserInfo() + const mascotInfo = userInfo?.mascotInfo + if (mascotInfo) { + uni.setTabBarStyle({ + midButton: { + iconPath: "/static/images/icon/fawn-" + (mascotInfo?.mascotNo || '1') + ".png", + width: "86px", + height: "86px", + iconWidth: "90px" + }, + success: () => { + common.setValue('mascotState', true) + } + }) + } + } catch (e) {} + } + return mascotState +} \ No newline at end of file diff --git a/tra-app/src/common/permission.js b/tra-app/src/common/permission.js new file mode 100644 index 0000000..6716429 --- /dev/null +++ b/tra-app/src/common/permission.js @@ -0,0 +1,272 @@ +/** + * 本模块封装了Android、iOS的应用权限判断、打开应用权限设置界面、以及位置系统服务是否开启 + */ + +var isIos +// #ifdef APP-PLUS +isIos = (plus.os.name == "iOS") +// #endif + +// 判断推送权限是否开启 +function judgeIosPermissionPush() { + var result = false; + var UIApplication = plus.ios.import("UIApplication"); + var app = UIApplication.sharedApplication(); + var enabledTypes = 0; + if (app.currentUserNotificationSettings) { + var settings = app.currentUserNotificationSettings(); + enabledTypes = settings.plusGetAttribute("types"); + console.log("enabledTypes1:" + enabledTypes); + if (enabledTypes == 0) { + console.log("推送权限没有开启"); + } else { + result = true; + console.log("已经开启推送功能!") + } + plus.ios.deleteObject(settings); + } else { + enabledTypes = app.enabledRemoteNotificationTypes(); + if (enabledTypes == 0) { + console.log("推送权限没有开启!"); + } else { + result = true; + console.log("已经开启推送功能!") + } + console.log("enabledTypes2:" + enabledTypes); + } + plus.ios.deleteObject(app); + plus.ios.deleteObject(UIApplication); + return result; +} + +// 判断定位权限是否开启 +function judgeIosPermissionLocation() { + var result = false; + var cllocationManger = plus.ios.import("CLLocationManager"); + var status = cllocationManger.authorizationStatus(); + result = (status != 2) + console.log("定位权限开启:" + result); + // 以下代码判断了手机设备的定位是否关闭,推荐另行使用方法 checkSystemEnableLocation + /* var enable = cllocationManger.locationServicesEnabled(); + var status = cllocationManger.authorizationStatus(); + console.log("enable:" + enable); + console.log("status:" + status); + if (enable && status != 2) { + result = true; + console.log("手机定位服务已开启且已授予定位权限"); + } else { + console.log("手机系统的定位没有打开或未给予定位权限"); + } */ + plus.ios.deleteObject(cllocationManger); + return result; +} + +// 判断麦克风权限是否开启 +function judgeIosPermissionRecord() { + var result = false; + var avaudiosession = plus.ios.import("AVAudioSession"); + var avaudio = avaudiosession.sharedInstance(); + var permissionStatus = avaudio.recordPermission(); + console.log("permissionStatus:" + permissionStatus); + if (permissionStatus == 1684369017 || permissionStatus == 1970168948) { + console.log("麦克风权限没有开启"); + } else { + result = true; + console.log("麦克风权限已经开启"); + } + plus.ios.deleteObject(avaudiosession); + return result; +} + +// 判断相机权限是否开启 +function judgeIosPermissionCamera() { + var result = false; + var AVCaptureDevice = plus.ios.import("AVCaptureDevice"); + var authStatus = AVCaptureDevice.authorizationStatusForMediaType('vide'); + console.log("authStatus:" + authStatus); + if (authStatus == 3) { + result = true; + console.log("相机权限已经开启"); + } else { + console.log("相机权限没有开启"); + } + plus.ios.deleteObject(AVCaptureDevice); + return result; +} + +// 判断相册权限是否开启 +function judgeIosPermissionPhotoLibrary() { + var result = false; + var PHPhotoLibrary = plus.ios.import("PHPhotoLibrary"); + var authStatus = PHPhotoLibrary.authorizationStatus(); + console.log("authStatus:" + authStatus); + if (authStatus == 3) { + result = true; + console.log("相册权限已经开启"); + } else { + console.log("相册权限没有开启"); + } + plus.ios.deleteObject(PHPhotoLibrary); + return result; +} + +// 判断通讯录权限是否开启 +function judgeIosPermissionContact() { + var result = false; + var CNContactStore = plus.ios.import("CNContactStore"); + var cnAuthStatus = CNContactStore.authorizationStatusForEntityType(0); + if (cnAuthStatus == 3) { + result = true; + console.log("通讯录权限已经开启"); + } else { + console.log("通讯录权限没有开启"); + } + plus.ios.deleteObject(CNContactStore); + return result; +} + +// 判断日历权限是否开启 +function judgeIosPermissionCalendar() { + var result = false; + var EKEventStore = plus.ios.import("EKEventStore"); + var ekAuthStatus = EKEventStore.authorizationStatusForEntityType(0); + if (ekAuthStatus == 3) { + result = true; + console.log("日历权限已经开启"); + } else { + console.log("日历权限没有开启"); + } + plus.ios.deleteObject(EKEventStore); + return result; +} + +// 判断备忘录权限是否开启 +function judgeIosPermissionMemo() { + var result = false; + var EKEventStore = plus.ios.import("EKEventStore"); + var ekAuthStatus = EKEventStore.authorizationStatusForEntityType(1); + if (ekAuthStatus == 3) { + result = true; + console.log("备忘录权限已经开启"); + } else { + console.log("备忘录权限没有开启"); + } + plus.ios.deleteObject(EKEventStore); + return result; +} + +// Android权限查询 +function requestAndroidPermission(permissionID) { + return new Promise((resolve, reject) => { + plus.android.requestPermissions( + [permissionID], // 理论上支持多个权限同时查询,但实际上本函数封装只处理了一个权限的情况。有需要的可自行扩展封装 + function(resultObj) { + var result = 0; + for (var i = 0; i < resultObj.granted.length; i++) { + var grantedPermission = resultObj.granted[i]; + console.log('已获取的权限:' + grantedPermission); + result = 1 + } + for (var i = 0; i < resultObj.deniedPresent.length; i++) { + var deniedPresentPermission = resultObj.deniedPresent[i]; + console.log('拒绝本次申请的权限:' + deniedPresentPermission); + result = 0 + } + for (var i = 0; i < resultObj.deniedAlways.length; i++) { + var deniedAlwaysPermission = resultObj.deniedAlways[i]; + console.log('永久拒绝申请的权限:' + deniedAlwaysPermission); + result = -1 + } + resolve(result); + // 若所需权限被拒绝,则打开APP设置界面,可以在APP设置界面打开相应权限 + // if (result != 1) { + // gotoAppPermissionSetting() + // } + }, + function(error) { + console.log('申请权限错误:' + error.code + " = " + error.message); + resolve({ + code: error.code, + message: error.message + }); + } + ); + }); +} + +// 使用一个方法,根据参数判断权限 +function judgeIosPermission(permissionID) { + if (permissionID == "location") { + return judgeIosPermissionLocation() + } else if (permissionID == "camera") { + return judgeIosPermissionCamera() + } else if (permissionID == "photoLibrary") { + return judgeIosPermissionPhotoLibrary() + } else if (permissionID == "record") { + return judgeIosPermissionRecord() + } else if (permissionID == "push") { + return judgeIosPermissionPush() + } else if (permissionID == "contact") { + return judgeIosPermissionContact() + } else if (permissionID == "calendar") { + return judgeIosPermissionCalendar() + } else if (permissionID == "memo") { + return judgeIosPermissionMemo() + } + return false; +} + +// 跳转到**应用**的权限页面 +function gotoAppPermissionSetting() { + if (isIos) { + var UIApplication = plus.ios.import("UIApplication"); + var application2 = UIApplication.sharedApplication(); + var NSURL2 = plus.ios.import("NSURL"); + // var setting2 = NSURL2.URLWithString("prefs:root=LOCATION_SERVICES"); + var setting2 = NSURL2.URLWithString("app-settings:"); + application2.openURL(setting2); + + plus.ios.deleteObject(setting2); + plus.ios.deleteObject(NSURL2); + plus.ios.deleteObject(application2); + } else { + // console.log(plus.device.vendor); + var Intent = plus.android.importClass("android.content.Intent"); + var Settings = plus.android.importClass("android.provider.Settings"); + var Uri = plus.android.importClass("android.net.Uri"); + var mainActivity = plus.android.runtimeMainActivity(); + var intent = new Intent(); + intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + var uri = Uri.fromParts("package", mainActivity.getPackageName(), null); + intent.setData(uri); + mainActivity.startActivity(intent); + } +} + +// 检查系统的设备服务是否开启 +// var checkSystemEnableLocation = async function () { +function checkSystemEnableLocation() { + if (isIos) { + var result = false; + var cllocationManger = plus.ios.import("CLLocationManager"); + var result = cllocationManger.locationServicesEnabled(); + console.log("系统定位开启:" + result); + plus.ios.deleteObject(cllocationManger); + return result; + } else { + var context = plus.android.importClass("android.content.Context"); + var locationManager = plus.android.importClass("android.location.LocationManager"); + var main = plus.android.runtimeMainActivity(); + var mainSvr = main.getSystemService(context.LOCATION_SERVICE); + var result = mainSvr.isProviderEnabled(locationManager.GPS_PROVIDER); + console.log("系统定位开启:" + result); + return result + } +} + +export default ({ + judgeIosPermission: judgeIosPermission, + requestAndroidPermission: requestAndroidPermission, + checkSystemEnableLocation: checkSystemEnableLocation, + gotoAppPermissionSetting: gotoAppPermissionSetting +}) diff --git a/tra-app/src/common/recordPermission.js b/tra-app/src/common/recordPermission.js new file mode 100644 index 0000000..3e17d4e --- /dev/null +++ b/tra-app/src/common/recordPermission.js @@ -0,0 +1,94 @@ +import { + getPhoneEnvBool +} from '@/common/common.js' + +import common from '@/common/common.js' +const IsAndEnv = getPhoneEnvBool('AND') +import permission from '@/common/permission.js' + +/** + * 录音权限工具类 + * 统一处理安卓和iOS的录音权限状态:同意、拒绝、未设置 + */ +const RecordPermission = { + hasRecordPermission: false, + /** + * 获取当前录音权限状态 + * @returns {Promise<'granted'|'denied'|'undetermined'>} 权限状态 + */ + getPermissionState() { + // #ifdef APP-PLUS + const authSetting = uni.getAppAuthorizeSetting(); + const state = authSetting.microphoneAuthorized; + switch (state) { + case 'authorized': + return 'authorized'; + case 'denied': + return 'denied'; + case 'not determined': + return 'undetermined'; + case 'config error': + return 'undetermined'; + default: + return 'undetermined'; + } + // #endif + // 非APP环境默认返回以获取方便h5调试 + return 'undetermined'; + }, + + /** + * 检查并请求录音权限 + * @returns {Promise} 是否获得录音权限 + */ + requestPermission() { + if (this.hasRecordPermission) { + console.log('直接通过'); + return Promise.resolve('authorized'); + } + return new Promise(async (resolve, reject) => { + const state = this.getPermissionState(); + // 已同意权限 + console.log('权限状态', state); + if (state === 'authorized') { + this.hasRecordPermission = true; // 缓存权限状态 + resolve('authorized') + }; + if (state === 'denied') { + const result = await common.show( + '提示信息', + '当前页面需要使用录音权限,是否前往开启?' + ) + if (result) { + // 前往权限设置页面 + uni.openAppAuthorizeSetting(); + reject('open_app_authorize_setting') + } else { + // 用户取消,返回上一页 + reject('cancel') + } + } + + if (state === 'undetermined') { + // #ifdef APP-PLUS + if (IsAndEnv) { + // 安卓需要再次请求权限 + const permResult = await permission.requestAndroidPermission( + 'android.permission.RECORD_AUDIO'); + if(permResult === 1) { + resolve('need_request'); + } else { + reject('permResultx') + } + } else { + resolve('need_request'); + } + // #endif + } + }); + }, + + +}; + +export default RecordPermission; \ No newline at end of file diff --git a/tra-app/src/common/regex.js b/tra-app/src/common/regex.js new file mode 100644 index 0000000..0d4ce6c --- /dev/null +++ b/tra-app/src/common/regex.js @@ -0,0 +1,174 @@ +// 定义正则表达式 +const regexes = { + email: /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/, + phone: /^(?:(?:\+|00)86)?1\d{10}$/, + url: /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/, + ip: /^((?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$/, + uuid: /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/, + required: /^.+$/, + is_number: /^\d+(\.\d+)?$/, + is_chinese: /^[\u4e00-\u9fa5]+$/, + is_realname: /^(?:[\u4e00-\u9fa5·]{2,50})$/, + is_english: /^[A-Za-z]+$/, + is_id_card: /^[1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|10|11|12)(?:0[1-9]|[1-2]\d|30|31)\d{3}[\dXx]$/, + is_bank_card: /^[1-9]\d{9,29}$/, + is_car_id: /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z][A-HJ-NP-Z0-9]{4,5}[A-HJ-NP-Z0-9挂学警港澳]$/, + is_username: /^.{5,18}$/, + is_password: /^.{4,18}$/, + is_email: /^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/ +}; + +// 邮箱 +export const checkEmail = (rule, sId, callback) => { + let _idRe = regexes.is_usis_emailername + if (!_idRe.test(sId)) { + callback(new Error('邮箱格式不正确')); + } + callback(); +} + +// 用户名 +export const checkUserName = (rule, sId, callback) => { + let _idRe = regexes.is_username + if (!_idRe.test(sId)) { + callback(new Error('用户名格式不正确')); + } + callback(); +} +// 密码 +export const checkPassword = (rule, sId, callback) => { + let _idRe = regexes.is_password + if (!_idRe.test(sId)) { + callback(new Error('密码格式不正确')); + } + callback(); +} + +// 车牌号 +export const checkCarId = (rule, sId, callback) => { + let _idRe = regexes.is_car_id + if (!_idRe.test(sId)) { + callback(new Error('车牌号格式不正确')); + } + callback(); +} +// 银行卡 +export const checkBankCard = (rule, sId, callback) => { + let _idRe = regexes.is_bank_card + if (!_idRe.test(sId)) { + callback(new Error('银行卡号格式不正确')); + } + callback(); +} + +// 验证手机号 +export const checkPhone = (rule, sId, callback) => { + let _idRe = regexes.phone + if (!_idRe.test(sId)) { + callback(new Error('手机号不正确')); + } + callback(); +} + +// 验证姓名 +export const checkName = (rule, sId, callback) => { + let _idRe = regexes.is_chinese + if (!_idRe.test(sId)) { + callback(new Error('姓名不正确')); + } + callback(); +} +export const checkRealName = (rule, sId, callback) => { + let _idRe = regexes.is_realname + if (!_idRe.test(sId)) { + callback(new Error('姓名不正确')); + } + callback(); +} + +// 验证身份证号 +export const checkIdCard = (rule, sId, callback) => { + if (!sId) { + return callback(new Error('身份证号不能为空')); + } + let _idRe = + /^\d{6}((((((19|20)\d{2})(0[13-9]|1[012])(0[1-9]|[12]\d|30))|(((19|20)\d{2})(0[13578]|1[02])31)|((19|20)\d{2})02(0[1-9]|1\d|2[0-8])|((((19|20)([13579][26]|[2468][048]|0[48]))|(2000))0229))\d{3})|((((\d{2})(0[13-9]|1[012])(0[1-9]|[12]\d|30))|((\d{2})(0[13578]|1[02])31)|((\d{2})02(0[1-9]|1\d|2[0-8]))|(([13579][26]|[2468][048]|0[048])0229))\d{2}))(\d|X|x)$/ + if (!_idRe.test(sId)) { + callback(new Error('⾝份证长度或格式错误')); + } + //⾝份证城市 + const aCity = { + 11: "北京", + 12: "天津", + 13: "河北", + 14: "山西", + 15: "内蒙古", + 21: "辽宁", + 22: "吉林", + 23: "黑龙江", + 31: "上海", + 32: "江苏", + 33: "浙江", + 34: "安徽", + 35: "福建", + 36: "江西", + 37: "山东", + 41: "河南", + 42: "湖北", + 43: "湖南", + 44: "广东", + 45: "广西", + 46: "海南", + 50: "重庆", + 51: "四川", + 52: "贵州", + 53: "云南", + 54: "西藏", + 61: "陕西", + 62: "甘肃", + 63: "青海", + 64: "宁夏", + 65: "新疆", + 71: "台湾", + 81: "香港", + 82: "澳门", + 91: "国外" + }; + if (!aCity[parseInt(sId.substr(0, 2))]) { + callback(new Error('身份证地区有误')); + } + // 出生期验证 + const sBirthday = (sId.substr(6, 4) + "-" + Number(sId.substr(10, 2)) + "-" + Number(sId.substr(12, 2))) + .replace(/-/g, "/") + const d = new Date(sBirthday) + if (sBirthday != (d.getFullYear() + "/" + (d.getMonth() + 1) + "/" + d.getDate())) { + callback(new Error('出生日期有误')); + } + // ⾝份证号码校验 + if (sId.length == 18) { + let sum = 0 + const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2], + codes = "10X98765432" + for (let i = 0; i < sId.length - 1; i++) { + sum += sId[i] * weights[i]; + } + const last = codes[sum % 11]; //计算出来的最后⼀位⾝份证号码 + if (sId[sId.length - 1] != last) { + callback(new Error('身份证号校验码有误')); + } + callback(); + } +}; + +export const validator = { + checkName, + checkIdCard, + checkPhone, + checkCarId, + checkBankCard, + checkUserName, + checkPassword, + checkEmail +} + +export default regexes; \ No newline at end of file diff --git a/tra-app/src/components/avatar-cropper/avatar-cropper.vue b/tra-app/src/components/avatar-cropper/avatar-cropper.vue new file mode 100644 index 0000000..920a424 --- /dev/null +++ b/tra-app/src/components/avatar-cropper/avatar-cropper.vue @@ -0,0 +1,1238 @@ + + + + + + diff --git a/tra-app/src/components/avatar-cropper/rotate.svg b/tra-app/src/components/avatar-cropper/rotate.svg new file mode 100644 index 0000000..0143706 --- /dev/null +++ b/tra-app/src/components/avatar-cropper/rotate.svg @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/tra-app/src/components/cached-avatar/cached-avatar.vue b/tra-app/src/components/cached-avatar/cached-avatar.vue new file mode 100644 index 0000000..ff1a20b --- /dev/null +++ b/tra-app/src/components/cached-avatar/cached-avatar.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/tra-app/src/components/course-item/course-item.vue b/tra-app/src/components/course-item/course-item.vue new file mode 100644 index 0000000..c0ecfb0 --- /dev/null +++ b/tra-app/src/components/course-item/course-item.vue @@ -0,0 +1,175 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/components/examination/characters-subject.vue b/tra-app/src/components/examination/characters-subject.vue new file mode 100644 index 0000000..240788d --- /dev/null +++ b/tra-app/src/components/examination/characters-subject.vue @@ -0,0 +1,187 @@ + + + + diff --git a/tra-app/src/components/examination/es-analysis.vue b/tra-app/src/components/examination/es-analysis.vue new file mode 100644 index 0000000..a2de879 --- /dev/null +++ b/tra-app/src/components/examination/es-analysis.vue @@ -0,0 +1,148 @@ + + + + + diff --git a/tra-app/src/components/examination/question.vue b/tra-app/src/components/examination/question.vue new file mode 100644 index 0000000..a9ab2d4 --- /dev/null +++ b/tra-app/src/components/examination/question.vue @@ -0,0 +1,399 @@ + + + + + diff --git a/tra-app/src/components/examination/radio-subject-item.vue b/tra-app/src/components/examination/radio-subject-item.vue new file mode 100644 index 0000000..edd889e --- /dev/null +++ b/tra-app/src/components/examination/radio-subject-item.vue @@ -0,0 +1,124 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/components/examination/radio-subject.vue b/tra-app/src/components/examination/radio-subject.vue new file mode 100644 index 0000000..2aaeddf --- /dev/null +++ b/tra-app/src/components/examination/radio-subject.vue @@ -0,0 +1,214 @@ + + + + \ No newline at end of file diff --git a/tra-app/src/components/examination/subject.vue b/tra-app/src/components/examination/subject.vue new file mode 100644 index 0000000..f193d26 --- /dev/null +++ b/tra-app/src/components/examination/subject.vue @@ -0,0 +1,541 @@ + + + + diff --git a/tra-app/src/components/examination/touchBtn.vue b/tra-app/src/components/examination/touchBtn.vue new file mode 100644 index 0000000..f1088e4 --- /dev/null +++ b/tra-app/src/components/examination/touchBtn.vue @@ -0,0 +1,468 @@ + + + + + diff --git a/tra-app/src/components/examination/useAudio.js b/tra-app/src/components/examination/useAudio.js new file mode 100644 index 0000000..1f9e677 --- /dev/null +++ b/tra-app/src/components/examination/useAudio.js @@ -0,0 +1,225 @@ +import { + computed, + reactive, + nextTick, + ref +} from 'vue'; + + +// const innerAudioContext = uni.createInnerAudioContext(); +// // 当前播放状态 +const audioStopFun = ref(null) + + +export const useAudio = () => { + + const audioInstance = uni.createInnerAudioContext(); + const callbacks = reactive({ + onEnded: null, // 播放完成回调 + onError: null, // 播放错误回调 + onLoaded: null, // 音频加载完成回调(可获取时长) + onStop: null + }) + let isInitialized = false; + const isPlaying = ref(false) + // 加载状态 + const isLoading = ref(false) + // 当前播放的音频地址 + const currentSrc = ref('') + const errorMsg = ref('') + + // 初始化音频实例(确保全局唯一) + const initAudio = () => { + console.log('初始化音频实例(确保全局唯一)'); + // if (isInitialized) return; + // isInitialized = true; + bindAudioEvents(); + } + + + + const bindAudioEvents = () => { + if (!audioInstance) return; + // 播放开始事件 + audioInstance.onPlay(() => { + isPlaying.value = true; + isLoading.value = false; + errorMsg.value = ''; + }); + + audioInstance.onStop(() => { + console.log('音频停止事件'); + isPlaying.value = false; + isLoading.value = false; + // 清空地址避免残留 + + // 触发自定义回调 + if (callbacks.onStop && typeof callbacks.onStop === 'function') { + callbacks.onStop(); + } + }); + + // 播放结束事件 + audioInstance.onEnded(() => { + console.log('播放结束事件'); + + // 清空地址避免残留 + // if (audioInstance) { + // audioInstance.src = ''; + // } + // 触发自定义回调 + if (callbacks.onEnded && typeof callbacks.onEnded === 'function') { + callbacks.onEnded(); + } + }); + + // 音频可以播放时触发(此时通常能获取到时长) + audioInstance.onCanplay(() => { + // 尝试获取时长(部分环境需要延迟一点) + // console.log('尝试获取时长(部分环境需要延迟一点)'); + isLoading.value = false; + setTimeout(() => { + const duration = audioInstance.duration || 0; + console.log('尝试获duration', duration); + if (duration > 0) { + // 触发加载完成回调(此时已获取时长) + if (callbacks.onLoaded && typeof callbacks.onLoaded === 'function') { + callbacks.onLoaded({ + duration: duration + }); + } + } + }, 100); // 短暂延迟确保时长已加载 + }); + + // 错误事件 + audioInstance.onError((err) => { + isPlaying.value = false; + isLoading.value = false; + console.error('音频错误:', err); + if (callbacks.onError && typeof callbacks.onError === 'function') { + callbacks.onError(err); // 把错误信息传给外部 + } + }); + } + + // 新增:预设置音频地址并加载(此时不播放,仅获取时长) + const setAudioSrc = (src) => { + console.log('预设置音频地址并加载', src); + if (!src || src === currentSrc.value) return; // 地址不变则不重复加载 + + isLoading.value = true; + currentSrc.value = src; + errorMsg.value = ''; + + try { + // 先停止当前播放并清空旧地址 + // audioInstance.stop(); + // audioInstance.src = ''; + // 设置新地址并加载(不自动播放) + audioInstance.src = src; + // audioInstance.load(); // 手动触发加载 + } catch (err) { + isLoading.value = false; + errorMsg.value = `设置音频失败: ${err.message}`; + } + } + + // 修改:播放当前已设置的音频(需先调用setAudioSrc) + const playAudio = async (src) => { + console.log('播放当前已设置的音频', currentSrc.value); + if (!currentSrc.value) { + errorMsg.value = '请先设置音频地址'; + return; + } + // 已在播放则暂停 + console.log('已在播放则暂停', isPlaying.value); + const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms)); + + if (isPlaying.value) { + pauseAudio(); + return; + } + + console.log('当前是是否暂停或停止状态', audioStopFun.value?.paused); + if (!audioStopFun.value?.paused) { // 当前是是否暂停或停止状态,true 表示暂停或停止,false 表示正在播放 + audioStopFun.value?.stop(); + wait(100) + } + + // 执行播放 + try { + // 哪个函数的实例正在播放,就把他挂在全局 + audioStopFun.value = audioInstance + audioInstance.play(); + isPlaying.value = true; + isLoading.value = false; + } catch (err) { + isLoading.value = false; + errorMsg.value = `播放失败: ${err.message}`; + } + } + + // 暂停播放 + const pauseAudio = () => { + if (audioInstance && isPlaying.value) { + console.log('暂停播放'); + audioInstance.pause(); + isPlaying.value = false; + } + } + + // 停止播放并重置状态 + const stopAudio = () => { + + if (audioInstance && isPlaying.value) { + audioInstance.stop(); + // todo + } + } + + // 移除所有回调(组件卸载时调用) + const removeCallbacks = () => { + callbacks.onEnded = null; + callbacks.onError = null; + callbacks.onLoaded = null; + callbacks.onStop = null; + } + // 注册播放完成回调 + const onAudioEnded = (callback) => { + if (typeof callback === 'function') { + callbacks.onEnded = callback; + } + } + // 注册播放错误回调 + const onAudioError = (callback) => { + if (typeof callback === 'function') { + callbacks.onError = callback; + } + } + // 新增:注册音频加载完成回调(用于获取时长) + const onAudioLoaded = (callback) => { + if (typeof callback === 'function') { + callbacks.onLoaded = callback; + } + } + const onAudioStop = (callback) => { + if (typeof callback === 'function') { + callbacks.onStop = callback; + } + } + initAudio(); + + return { + isPlaying, + playAudio, + pauseAudio, + stopAudio, + setAudioSrc, + removeCallbacks, + onAudioEnded, + onAudioError, + onAudioLoaded, + onAudioStop + } +} \ No newline at end of file diff --git a/tra-app/src/components/file-preview/file-preview.vue b/tra-app/src/components/file-preview/file-preview.vue new file mode 100644 index 0000000..d4db270 --- /dev/null +++ b/tra-app/src/components/file-preview/file-preview.vue @@ -0,0 +1,111 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/components/image-preview/image-preview.vue b/tra-app/src/components/image-preview/image-preview.vue new file mode 100644 index 0000000..c8fb927 --- /dev/null +++ b/tra-app/src/components/image-preview/image-preview.vue @@ -0,0 +1,191 @@ + + + + + diff --git a/tra-app/src/components/image-preview/preview-detail-ios.vue b/tra-app/src/components/image-preview/preview-detail-ios.vue new file mode 100644 index 0000000..70f348d --- /dev/null +++ b/tra-app/src/components/image-preview/preview-detail-ios.vue @@ -0,0 +1,301 @@ + + + + + diff --git a/tra-app/src/components/image-preview/preview-detail.vue b/tra-app/src/components/image-preview/preview-detail.vue new file mode 100644 index 0000000..80ea81a --- /dev/null +++ b/tra-app/src/components/image-preview/preview-detail.vue @@ -0,0 +1,296 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/components/image-preview/video-preview.vue b/tra-app/src/components/image-preview/video-preview.vue new file mode 100644 index 0000000..bfd3d5d --- /dev/null +++ b/tra-app/src/components/image-preview/video-preview.vue @@ -0,0 +1,200 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/components/list-no-data/list-no-data.vue b/tra-app/src/components/list-no-data/list-no-data.vue new file mode 100644 index 0000000..4a466fa --- /dev/null +++ b/tra-app/src/components/list-no-data/list-no-data.vue @@ -0,0 +1,26 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/components/loading.vue b/tra-app/src/components/loading.vue new file mode 100644 index 0000000..3bf15e4 --- /dev/null +++ b/tra-app/src/components/loading.vue @@ -0,0 +1,352 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/components/markdown-preview/markdown-preview.vue b/tra-app/src/components/markdown-preview/markdown-preview.vue new file mode 100644 index 0000000..0ce9ed5 --- /dev/null +++ b/tra-app/src/components/markdown-preview/markdown-preview.vue @@ -0,0 +1,78 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/components/my-skeleton/my-skeleton.vue b/tra-app/src/components/my-skeleton/my-skeleton.vue new file mode 100644 index 0000000..ec320ff --- /dev/null +++ b/tra-app/src/components/my-skeleton/my-skeleton.vue @@ -0,0 +1,121 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/components/nav-bar/nav-bar.vue b/tra-app/src/components/nav-bar/nav-bar.vue new file mode 100644 index 0000000..1896bdb --- /dev/null +++ b/tra-app/src/components/nav-bar/nav-bar.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/tra-app/src/components/picture-upload/picture-upload.vue b/tra-app/src/components/picture-upload/picture-upload.vue new file mode 100644 index 0000000..a01f55b --- /dev/null +++ b/tra-app/src/components/picture-upload/picture-upload.vue @@ -0,0 +1,200 @@ + + + + \ No newline at end of file diff --git a/tra-app/src/components/points-and-badge/badge.vue b/tra-app/src/components/points-and-badge/badge.vue new file mode 100644 index 0000000..20ada5e --- /dev/null +++ b/tra-app/src/components/points-and-badge/badge.vue @@ -0,0 +1,319 @@ + + + + + diff --git a/tra-app/src/components/points-and-badge/check-in.vue b/tra-app/src/components/points-and-badge/check-in.vue new file mode 100644 index 0000000..3596321 --- /dev/null +++ b/tra-app/src/components/points-and-badge/check-in.vue @@ -0,0 +1,484 @@ + + + + + diff --git a/tra-app/src/components/points-and-badge/points.vue b/tra-app/src/components/points-and-badge/points.vue new file mode 100644 index 0000000..a28d797 --- /dev/null +++ b/tra-app/src/components/points-and-badge/points.vue @@ -0,0 +1,191 @@ + + + + + diff --git a/tra-app/src/components/points-and-badge/usePointsAndBadge.js b/tra-app/src/components/points-and-badge/usePointsAndBadge.js new file mode 100644 index 0000000..a702c17 --- /dev/null +++ b/tra-app/src/components/points-and-badge/usePointsAndBadge.js @@ -0,0 +1,121 @@ +/** + 完成课程学习 首次完成1分 每天最多获取5分 完成课程学习 crsId 02 完成调用 杨航 + 完成课程练习 首次完成1分 每天最多获取5分 完成课程练习 crsId 04 完成练习调用 田岩 已完成 + 通过课程考试 首次完成1分 每天最多获取5分 通过课程考试 crsId 03 通过调用 田岩 已完成 + 完成考试中心考试 首次完成1分 每天最多获取5分 完成考试中心考试 papersId 05 完成调用 田岩 已完成 + 完成竞赛PK 每次1分 每天最多获取5分 完成竞赛PK 11 完成调用 田岩 已完成 + 获取AI问答 次数徽章 06 建立ws完成调用 杨航 + 通过AI陪练练习 每次1分 每天最多获取5分 通过AI陪练练习 07 通过调用 杨航 + 通过AI陪练考试 每次1分 每天最多获取5分 通过AI陪练考试 08 通过调用 杨航 + 签到 每日1积分, + 第七天5积分, + 第30天20积分, + 断签从第一天计算。 签到完成 田岩 + */ +import { + addPointsByTask +} from '@/api/pointsAndRank.js' +import { + ref, + nextTick +} from 'vue' +import { + pop_up_time +} from '@/enum'; + +export default function usePointsAndBadge() { + const badgeRef = ref(null); + const pointsRef = ref(null); + /** + * 处理积分和徽章弹窗逻辑(提取的独立方法) + * @param {number} points - 积分值 + * @param {Array} pointsBadges - 积分关联的徽章列表 + * @param {Function} complete - 完成回调函数 + * @param {any} res - 接口返回数据 + */ + const handlePointsBadgePopup = (points, pointsBadges, complete = () => {}, res = {}) => { + // AI问答仅弹徽章弹窗 + if (points?.length > 0) { + // 普通场景:先弹积分弹窗,再弹徽章弹窗 + pointsRef.value?.open(points, pointsBadges, (status, pointsList, badgesList) => { + pointsRef.value.close(); + // 有徽章则弹徽章弹窗,否则直接完成 + if (badgesList?.length > 0) { + nextTick(() => { + setTimeout(() => { + badgeRef.value.open(pointsList, badgesList, () => { + badgeRef.value.close(); + complete(true, '积分获取成功', res); + }); + }, pop_up_time); + }); + } else { + complete(true, '积分获取成功', res); + } + }); + } else if (pointsBadges?.length > 0) { + badgeRef.value.open([], pointsBadges, () => { + badgeRef.value.close(); + complete(true, '积分获取成功', res); + }); + }else{ + complete(false, '积分接口返回数据异常', res); + } + }; + + /** + * 积分和徽章处理主方法 + * @param {string} type - 任务类型(04:课程练习,06:AI问答,其他:普通任务) + * @param {string|null} taskId - 任务ID + * @param {Function} complete - 完成回调(status, message, data) + */ + const pointAndBadgesRun = async (type, taskId = '', complete = () => {}) => { + + try { + console.log('调用积分接口'); + const res = await addPointsByTask({ + type, + taskId + }); + + // 防御性处理:确保res.body存在 + if (!res?.body) { + complete(false, '积分接口返回数据异常', res); + return; + } + const { + points, + pointsBadges, + message = '' + } = res.body; + + // 分支逻辑:按类型处理 + switch (type) { + case '04': // 04:课程练习,自定义返回逻辑,不弹弹窗 + complete(true, '', res.body); // 补充回调,避免调用方等待 + return; + case '06': // 06:AI问答,仅弹徽章弹窗 + if (pointsBadges?.length > 0) { + handlePointsBadgePopup(points, pointsBadges, complete, res); + } else { + complete(true, '', res); + } + return; + // 其他类型:弹积分+徽章弹窗 + default: + handlePointsBadgePopup(points, pointsBadges, complete, res); + return; + } + } catch (error) { + // 细化错误信息:区分网络错误/接口错误 + const errorMsg = error?.message || '积分获取失败'; + complete(false, `积分获取失败:${errorMsg}`, error); + } + }; + return { + pointAndBadgesRun, + handlePointsBadgePopup, + badgeRef, + pointsRef + }; + } \ No newline at end of file diff --git a/tra-app/src/components/search-input/search-input.vue b/tra-app/src/components/search-input/search-input.vue new file mode 100644 index 0000000..c9fdc07 --- /dev/null +++ b/tra-app/src/components/search-input/search-input.vue @@ -0,0 +1,68 @@ + + + + diff --git a/tra-app/src/components/selection-institutions/index.vue b/tra-app/src/components/selection-institutions/index.vue new file mode 100644 index 0000000..64a5150 --- /dev/null +++ b/tra-app/src/components/selection-institutions/index.vue @@ -0,0 +1,206 @@ + + + + + diff --git a/tra-app/src/components/selection-institutions/treeSelector.vue b/tra-app/src/components/selection-institutions/treeSelector.vue new file mode 100644 index 0000000..2e65b6f --- /dev/null +++ b/tra-app/src/components/selection-institutions/treeSelector.vue @@ -0,0 +1,129 @@ + + + + + diff --git a/tra-app/src/components/tabbar-shadow/tabbar-shadow.vue b/tra-app/src/components/tabbar-shadow/tabbar-shadow.vue new file mode 100644 index 0000000..55624c9 --- /dev/null +++ b/tra-app/src/components/tabbar-shadow/tabbar-shadow.vue @@ -0,0 +1,26 @@ + + + + \ No newline at end of file diff --git a/tra-app/src/components/topic-display/topic-display.vue b/tra-app/src/components/topic-display/topic-display.vue new file mode 100644 index 0000000..7dd55be --- /dev/null +++ b/tra-app/src/components/topic-display/topic-display.vue @@ -0,0 +1,238 @@ + + + + + diff --git a/tra-app/src/components/watermark/watermark.vue b/tra-app/src/components/watermark/watermark.vue new file mode 100644 index 0000000..7a120c9 --- /dev/null +++ b/tra-app/src/components/watermark/watermark.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/tra-app/src/composables/useCheckIn.js b/tra-app/src/composables/useCheckIn.js new file mode 100644 index 0000000..67a8d5b --- /dev/null +++ b/tra-app/src/composables/useCheckIn.js @@ -0,0 +1,226 @@ +import { + queryCheckIn, + addPointsByCheckIn +} from '@/api/pointsAndRank.js'; +import common from '@/common/common'; +import { + getUserInfo +} from '@/common/common'; +import { + ref +} from 'vue'; +export default function useCheckIn() { + const checkInRef = ref(null) + // ========== 核心工具方法 ========== + /** + * 获取当前用户的签到存储key(区分不同用户) + */ + const getKey = () => { + const userInfo = getUserInfo(); + const userId = userInfo?.userId || ''; // 修复原代码userId重复赋值问题 + if (!userId) { + console.warn('用户ID为空,无法获取签到状态'); + return ''; + } + return `${userId}_user_sign_in_status`; + }; + + /** + * 获取今日日期(YYYY-MM-DD) + */ + const getTodayDate = () => { + const now = new Date(); + return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`; + }; + + /** + * 读取本地存储的签到状态 + * @returns {Object} { isSigned: boolean, signDate: string } + */ + const getLocalSignStatus = () => { + const key = getKey(); + if (!key) return { + signDate: '', + checkInDays: 0, + PopupDay: '' + }; + try { + // 读取本地存储的签到信息(兼容JSON格式) + const rawData = common.getValue(key); + if (!rawData) return { + signDate: '', + checkInDays: 0, + popupDay: '' + }; + + // 兼容存储的是字符串或JSON对象的情况 + const signStatus = rawData; + return { + signDate: signStatus.signDate || '', + checkInDays: signStatus.checkInDays || 0, + popupDay: signStatus.popupDay || '', + + }; + } catch (e) { + console.error('读取本地签到状态失败', e); + return { + signDate: '', + checkInDays: 0, + popupDay: '' + }; + } + }; + + /** + * 更新本地签到状态(支持按需更新字段) + * @param {boolean|undefined} isSigned - 是否签到(可选,传入则更新) + * @param {number|undefined} checkInDays - 签到天数(可选,传入则更新) + * @param {string|undefined} popupDay - 弹窗关闭日期(可选,传入则更新) + */ + const setLocalSignStatus = (isSigned, checkInDays, popupDay) => { + const key = getKey(); + if (!key) return; + + // 1. 读取本地原有数据(若无则初始化空对象) + let oldSignStatus = {}; + try { + oldSignStatus = common.getValue(key) || {}; // 兼容common.getValue返回null/undefined的情况 + } catch (e) { + console.warn('读取原有签到状态失败,使用空对象初始化', e); + oldSignStatus = {}; + } + + // 2. 初始化新状态:先继承原有数据 + const newSignStatus = { ...oldSignStatus }; + + // 3. 按需更新字段(仅当参数传入时覆盖) + // 处理isSigned:传入则更新,且为true时自动设置signDate + if (isSigned !== undefined) { + // 保持原有逻辑:isSigned为true时,signDate设为今日 + newSignStatus.signDate = isSigned ? getTodayDate() : oldSignStatus.signDate; + } + + // 处理checkInDays:传入则更新,未传入保留原有值 + if (checkInDays !== undefined) { + newSignStatus.checkInDays = checkInDays; + } + + // 处理popupDay:传入非空则更新,未传入/空值保留原有值 + if (popupDay !== undefined) { + newSignStatus.popupDay = popupDay? getTodayDate() : oldSignStatus.popupDay; + } + + // 4. 重新存入本地(覆盖原有数据) + try { + console.log('存入的数据(原有+更新)', newSignStatus); + common.setValue(key, newSignStatus); + } catch (e) { + console.error('更新本地签到状态失败', e); + } + }; + + + + // ========== 对外暴露的核心方法 ========== + /** + * 初始化签到状态(页面加载时调用,同步本地与后端状态) + * @returns {Promise<{ isSigned: boolean, checkInDays?: number }>} 签到状态 + 连续签到天数 + */ + const initCheckInStatus = async (complete = () => {}) => { + const today = getTodayDate(); + const localStatus = getLocalSignStatus(); + console.log('localStatus', localStatus); + // 1. 本地已有今日签到记录 → 直接返回本地状态 + if (localStatus.signDate === today) { + + complete(true, localStatus.checkInDays, localStatus.popupDay === today) + return true; + } + // 2. 本地记录过期/无记录 → 请求接口获取最新状态 + try { + const res = await queryCheckIn(); // 调用查询签到状态接口 + const { + body + } = res || {}; + console.log('bodbodyy', body); + // // 接口返回格式适配(根据实际后端返回调整) + const isSigned = body?.checkInToday === 'Y'; + const checkInDays = body?.days || 0; + setLocalSignStatus(isSigned, checkInDays); + // if (!isSigned) { // 没签到执行签到弹窗 + // console.log('没签到执行签到弹窗'); + // //如果今日点击过x那么久不弹了 + // // if(localStatus.popupDay === today) return; + // complete(isSigned, checkInDays, localStatus.popupDay === today) + // } + complete(isSigned, checkInDays, localStatus.popupDay === today) + + } catch (e) { + console.error('请求签到状态接口失败', e); + } + }; + + /** + * 判断今日是否已签到(简化版,供快速判断) + * @returns {boolean} + */ + const getTodayCheckInStatus = () => { + const localStatus = getLocalSignStatus(); + const today = getTodayDate(); + // 日期匹配且已签到,才返回true + return localStatus.signDate === today; + }; + + + + /** + * 执行签到操作(核心方法) + * @param {Function} successCb - 签到成功回调 + * @param {Function} failCb - 签到失败回调 + * @returns {Promise} 签到是否成功 + */ + const checkInFun = async ({ + status, + days=0, + successCb=() =>{}, + failCb=() =>{} + }) => { + if(!status) { // 点击了关闭 + console.log('点击了关闭'); + + setLocalSignStatus(undefined, undefined, true); + return; + } + console.log('请求接口', days); + addPointsByCheckIn({ + days + }) + .then((res) => { + console.log('接口成功回调', res); + setLocalSignStatus(true, days + 1); + successCb?.({ + days: days + 1, + res + }); + }).catch((error) => { + failCb?.(error) + }) + }; + + /** + * 重置签到状态(可选,用于特殊场景) + */ + const resetCheckInStatus = () => { + const key = getKey(); + if (key) common.removeValue(key); // 需确保common有remove方法,无则补充 + }; + + // ========== 返回对外暴露的方法 ========== + return { + checkInRef, + initCheckInStatus, // 初始化签到状态(页面加载时调用) + checkInFun, // 执行签到 + getLocalSignStatus, // 获取签到缓存信息 + getTodayCheckInStatus // 获取今天签到状态 + }; +} \ No newline at end of file diff --git a/tra-app/src/composables/useExamSubject.js b/tra-app/src/composables/useExamSubject.js new file mode 100644 index 0000000..eff364d --- /dev/null +++ b/tra-app/src/composables/useExamSubject.js @@ -0,0 +1,10 @@ +import RecordPermission from '@/common/recordPermission.js' + +export const initRecord = () => RecordPermission.requestPermission(); + + +export default function useIndexList() { + return { + + }; +} \ No newline at end of file diff --git a/tra-app/src/composables/useIndexList.js b/tra-app/src/composables/useIndexList.js new file mode 100644 index 0000000..29ef764 --- /dev/null +++ b/tra-app/src/composables/useIndexList.js @@ -0,0 +1,33 @@ +import { + computed, + reactive, + nextTick, + ref +} from 'vue'; + + +export default function useIndexList() { + const tabAct = ref(0); + const listItemRefs = ref([]); + const tabChange = (item) => { + tabAct.value = item.index; + }; + + const swiperChange = (item) => { + tabAct.value = item.detail.current; + }; + + const search = (value) => { + console.log('search', value); + listItemRefs.value.forEach((item, index) => { + item?.search(value, tabAct.value === index); + }); + }; + return { + tabAct, + listItemRefs, + tabChange, + swiperChange, + search, + }; +} \ No newline at end of file diff --git a/tra-app/src/composables/useUpdateAvatar.js b/tra-app/src/composables/useUpdateAvatar.js new file mode 100644 index 0000000..d1f55e4 --- /dev/null +++ b/tra-app/src/composables/useUpdateAvatar.js @@ -0,0 +1,83 @@ +import { + reactive, + ref, + computed, + nextTick +} from 'vue'; +import { + uploadFile +} from '@/api/common'; +import { + queryCurrentUser +} from '@/api/login.js'; +import common from '@/common/common'; +import { + updateDfSysUserExtandInfoImageAddr +} from '@/api/user.js'; + +export default function useUpdateAvatar({ + success = () => {} +}) { + const avatarCropperUrl = ref(''); + const avatarCropperStatus = ref(true); + // 点击修改头像 + const click_update_avatar = () => { + uni.chooseImage({ + count: 1, + sizeType: ['compressed'], + success: (resPicture) => { + uni.getFileInfo({ + filePath: resPicture.tempFilePaths[0], + success: function(resSize) { + if (resSize.size > 10 * 1024 * 1024) return common.msg( + '图片过大,请重新选择'); + avatarCropperUrl.value = resPicture.tempFilePaths[0]; + avatarCropperStatus.value = true; + }, + fail: function(err) { + common.msg('获取图片失败,请选择其他图片'); + } + }); + } + }); + }; + // 选择头像取消 + const avatarOnCancel = (value) => { + avatarCropperStatus.value = false; + }; + // 选择头像确定 + const avatarOnConfirm = (file) => { + avatarCropperStatus.value = false; + nextTick(async () => { + common.loading('更新头像中'); + try { + const data = { + bizScen: 'S3007', + thumbnailFlag: true, + thumbImgSize: 100 + }; + // 上传图片 + const res = await uploadFile(file.tempFilePath, data); + // 更新个人信息 + await updateDfSysUserExtandInfoImageAddr({ + imageAddr: res.thumbFileId + }); + // 更新个人信息 + const userData = await queryCurrentUser(); + common.msg('修改成功'); + success(userData) + + } catch { + common.msg('修改失败'); + } + common.hideLoading(); + }); + }; + return { + avatarCropperUrl, + avatarCropperStatus, + click_update_avatar, + avatarOnCancel, + avatarOnConfirm, + }; +} \ No newline at end of file diff --git a/tra-app/src/composables/useZpaging.js b/tra-app/src/composables/useZpaging.js new file mode 100644 index 0000000..e748245 --- /dev/null +++ b/tra-app/src/composables/useZpaging.js @@ -0,0 +1,65 @@ +import { + computed, + reactive, + nextTick, + ref +} from 'vue'; + +export default function useZpaging({ + fetchFunction, // 必传:接口请求函数 + searchKey = '' // 可选:搜索参数名,默认'searchText' +}) { + const total = ref(-1); + const data_list = ref([]); + const searchText = ref(''); + const pagingRef = ref(null); + const queryList = async (pageNo, pageSize) => { + try { + const requestParams = { + page: pageNo, + limit: pageSize + }; + // 只有当searchKey有效(非空字符串/非undefined)且searchText有值时,才添加搜索参数 + if (searchKey && searchKey.trim() && searchText.value) { + requestParams[searchKey] = searchText.value; + } + const res = await fetchFunction(requestParams); + + total.value = res.total??res.body.length + pagingRef.value.completeByTotal(res.body, res.total); + } catch (err) { + console.error('分页查询失败:', err); + pagingRef.value?.complete(false); + } + }; + //刷新列表要求 + const reload = () => { + pagingRef.value?.reload() + }; + // 清空分页数据,pageNo恢复为默认值。 + const clear = () => { + total.value = -1 + pagingRef.value?.clear(); + } + + // 搜索 + const search = (value, state=true) => { + console.log('搜索', value, state); + searchText.value = value; + if (state) { + reload() + } else { + clear() + } + }; + return { + pagingRef, + queryList, + reload, + search, + clear, + searchText, + total, + data_list + }; +} \ No newline at end of file diff --git a/tra-app/src/enum.js b/tra-app/src/enum.js new file mode 100644 index 0000000..f98cec9 --- /dev/null +++ b/tra-app/src/enum.js @@ -0,0 +1,51 @@ +export const MAX_UPLOAD_PICTURE_SIZE = 2048 // 最大图片上传限制 +export const Max_Upload_Document_Size = 102400 // 最大文档上传限制 +export const TextareaInputLimitCharacter = 160 // 全局多行文本输入框限制字数 +// 公共tab页的图片 +export const LineBg = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAABL0lEQVQ4T6XSv0uCURTG8e9jgpRNDS1BU1sELUFLQVM1tgQFtcR7A8m1LQqaLGhOkQKH/o5aKiiHlgiE0K1oCYLAMk9ov/RF8e313u2ew4dzzzmi2+NsA9gH3hCr6srzbAJxDkS/nbvwYNJilMkDow1FXYUHneWAlV/MqN3ZcKCzHWDb164MGa3/H/Qshdj0YbdUmORIL8HBNRsgShZjoQkznvioY/e19wCgCccSsAcM+bBnjHmyuvx5bw8mrJ93FoEkYrzFej0g5kjrpjH2BTrro8oIYhgYQ0xhTCPibfb0ggjLHKrojwvPEogDIBZgycsYKQrscqpKq3zh7BXo7YBVgRMibLWqqvnLzh6BwTZgESNHD8edoL+hOJupT9CII0oYJcQ14oy0CgHa0JTyCeBLVNeWygE5AAAAAElFTkSuQmCC"; +// 性别 +export const GENDER = [{ + value: "m", + label: "男性", + }, + { + value: "f", + label: "女性", + } +] +// 试题题型 +export const SUBJECT_TYPE = [{ + value: "SN", + label: "单选题", + }, + { + value: "MU", + label: "多选题", + }, + { + value: "JD", + label: "判断题", + }, + { + value: "ES", + label: "问答题", + } +] +export const defaultAvatar = '/static/images/me/default_user_avatar.png' +export const defaultAnonymousAvatar = '/static/images/competition/competition_default_avatar.png' +export const styleButton = { + style_button_1: { + color: '#FFFFFF', + fontSize: '30rpx', + backgroundColor: '#0066FF', + borderRadius: '8rpx' + }, + style_button_2: { + color: '#0066FF', + fontSize: '30rpx', + backgroundColor: '#E2EDFF', + borderRadius: '8rpx' + } +} +export const pop_up_time = 500; \ No newline at end of file diff --git a/tra-app/src/index.html b/tra-app/src/index.html new file mode 100644 index 0000000..c3ff205 --- /dev/null +++ b/tra-app/src/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + +
+ + + diff --git a/tra-app/src/main.js b/tra-app/src/main.js new file mode 100644 index 0000000..5874ef8 --- /dev/null +++ b/tra-app/src/main.js @@ -0,0 +1,26 @@ +import App from './App' +import { createSSRApp } from 'vue' +import pinia from './store.js' +import CommonLoading from './components/loading.vue' + +uni.$zp = { + config: { + 'default-page-size': 20, + 'show-loading-more-when-reload': true, // 列表刷新时自动显示加载更多view,且为加载中状态 + 'show-scrollbar': false, // 控制是否出现滚动条 + 'refresher-refreshing-scrollable': false, // 自定义下拉刷新刷新中状态是否允许列表滚动 + "min-delay":300,// 触发@query后最小延迟处理的时间,单位为毫秒 + 'empty-view-img': '/static/images/common/default_img.png', + 'empty-view-center': false, // 空数据图片是否垂直居中,默认为是,若设置为否即为从空数据容器顶部开始显示 + //'auto-hide-empty-view-when-pull':false,// 加载中时是否自动隐藏空数据图 + } +} + +export function createApp() { + const app = createSSRApp(App) + app.use(pinia) + app.component('CommonLoading',CommonLoading) + return { + app + } +} diff --git a/tra-app/src/manifest.json b/tra-app/src/manifest.json new file mode 100644 index 0000000..0a4b6c0 --- /dev/null +++ b/tra-app/src/manifest.json @@ -0,0 +1,103 @@ +{ + "name" : "tra-app", + "appid" : "__UNI__EDE0E52", + "description" : "", + "versionName" : "0.0.3", + "versionCode" : 3, + "transformPx" : false, + /* 5+App特有相关 */ + "app-plus" : { + "usingComponents" : true, + "nvueStyleCompiler" : "uni-app", + "compilerVersion" : 3, + "renderer" : "auto", + "orientation" : [ "portrait-primary" ], + "splashscreen" : { + "alwaysShowBeforeRender" : true, + "waiting" : false, + "autoclose" : true, + "delay" : 0 + }, + "safearea" : { + "background" : "#CCCCCC", + "bottom" : { + "offset" : "none|auto" + }, + "left" : { + "offset" : "none|auto" + }, + "right" : { + "offset" : "none|auto" + } + }, + /* 模块配置 */ + "modules" : { + "Camera" : {}, + "Barcode" : {}, + "Record" : {}, + "VideoPlayer" : {}, + "LivePusher" : {} + }, + /* 应用发布信息 */ + "distribute" : { + /* android打包配置 */ + "android" : { + "permissions" : [ + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ] + }, + /* ios打包配置 */ + "ios" : { + "dSYMs" : false + }, + /* SDK配置 */ + "sdkConfigs" : {}, + "splashscreen" : { + "androidStyle" : "common", + "android" : { + "hdpi" : "src/static/images/login/open.9.png", + "xhdpi" : "src/static/images/login/open.9.png", + "xxhdpi" : "src/static/images/login/open.9.png" + }, + "useOriginalMsgbox" : true + } + } + }, + /* 快应用特有相关 */ + "quickapp" : {}, + /* 小程序特有相关 */ + "mp-weixin" : { + "appid" : "", + "setting" : { + "urlCheck" : false + }, + "usingComponents" : true + }, + "mp-alipay" : { + "usingComponents" : true + }, + "mp-baidu" : { + "usingComponents" : true + }, + "mp-toutiao" : { + "usingComponents" : true + }, + "uniStatistics" : { + "enable" : false + }, + "vueVersion" : "3" +} diff --git a/tra-app/src/pages.json b/tra-app/src/pages.json new file mode 100644 index 0000000..62ee516 --- /dev/null +++ b/tra-app/src/pages.json @@ -0,0 +1,599 @@ +{ + "pages": [ + { + "path": "pages/index/dialog", + "style": { + "navigationBarTitleText": "AI问答", + "app-plus": { + "titleNView": false, + "animationType": "fade-in" + } + } + }, + { + "path": "pages/index/index", + "style": { + "navigationBarTitleText": "首页", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/courseDetail/index", + "style": { + "navigationBarTitleText": "课程详情", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/courseDetail/submit", + "style": { + "navigationBarTitleText": "提交评价", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/setting/index", + "style": { + "navigationBarTitleText": "设置", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/welcome/index", + "style": { + "navigationBarTitleText": "欢迎", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/test/index", + "style": { + "navigationBarTitleText": "测试", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/study/index", + "style": { + "navigationBarTitleText": "学习", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/scene/index", + "style": { + "navigationBarTitleText": "场景", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/me/index", + "style": { + "navigationBarTitleText": "我的", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/login/login", + "style": { + "navigationBarTitleText": "登录", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/other/web_view", + "style": { + "navigationBarTitleText": "", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/login/ysxy", + "style": { + "navigationBarTitleText": "", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/search/search", + "style": { + "navigationBarTitleText": "", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/mascot/mascot_select_list", + "style": { + "navigationBarTitleText": "吉祥物列表", + "disableSwipeBack": true, + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/preview/index", + "style": { + "navigationBarTitleText": "", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/favorites/index", + "style": { + "navigationBarTitleText": "", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/messageNotification/index", + "style": { + "navigationBarTitleText": "消息", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/messageNotification/list", + "style": { + "navigationBarTitleText": "消息列表", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/testingHall/index", + "style": { + "navigationBarTitleText": "考试中心", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/testingHall/details", + "style": { + "navigationBarTitleText": "考试详情", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/wrongQuestionRecord/index", + "style": { + "navigationBarTitleText": "错题本", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/wrongQuestionRecord/correct", + "style": { + "navigationBarTitleText": "错题本做题页", + "disableSwipeBack": true, + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/examination/index", + "style": { + "navigationBarTitleText": "考试", + "disableSwipeBack": true, + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/examination/result", + "style": { + "navigationBarTitleText": "考试结果", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/practice/index", + "style": { + "navigationBarTitleText": "练习", + "disableSwipeBack": true, + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/learningTasks/index", + "style": { + "navigationBarTitleText": "学习任务", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/learningTasks/details", + "style": { + "navigationBarTitleText": "学习详情", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/examRanking/index", + "style": { + "navigationBarTitleText": "考试排行榜页", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/courseRecord/index", + "style": { + "navigationBarTitleText": "课程记录页", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/examinationRecord/index", + "style": { + "navigationBarTitleText": "考试记录页", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/courseRecord/courseStudyRecordDetail", + "style": { + "navigationBarTitleText": "课程记录学习记录详情", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/courseRecord/coursePracticeRecordDetail", + "style": { + "navigationBarTitleText": "课程记录练习记录详情", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/wrongQuestionRecord/result", + "style": { + "navigationBarTitleText": "答题本答题完成结果页", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/practice/result", + "style": { + "navigationBarTitleText": "练习完成结果页", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/competition/index", + "style": { + "navigationBarTitleText": "竞赛列表页", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/competition/matching", + "style": { + "navigationBarTitleText": "匹配页面", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/competition/pk", + "style": { + "navigationBarTitleText": "pk页面", + "disableSwipeBack": true, + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/competition/result", + "style": { + "navigationBarTitleText": "pk结果页面", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/competition/ranking", + "style": { + "navigationBarTitleText": "pk排行榜", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/pointsAndRank/points", + "style": { + "navigationBarTitleText": "积分变动明细", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/pointsAndRank/rank", + "style": { + "navigationBarTitleText": "段位列表", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/pointsAndRank/badge", + "style": { + "navigationBarTitleText": "徽章墙", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/competitionRecord/index", + "style": { + "navigationBarTitleText": "竞赛记录", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/pointsRedemption/index", + "style": { + "navigationBarTitleText": "积分兑换", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/test/testChat", + "style": { + "navigationBarTitleText": "", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/course/index", + "style": { + "navigationBarTitleText": "课程中心", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/course/study", + "style": { + "navigationBarTitleText": "课程学习", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/charts/index", + "style": { + "navigationBarTitleText": "", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/index/preview", + "style": { + "navigationBarTitleText": "", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/search/result", + "style": { + "navigationBarTitleText": "", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/example/dialog", + "style": { + "navigationBarTitleText": "AI问答演示", + "app-plus": { + "titleNView": false, + "animationType": "fade-in" + } + } + }, + { + "path": "pages/sparring/index", + "style": { + "navigationBarTitleText": "AI陪练", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/sparring/detail", + "style": { + "navigationBarTitleText": "AI陪练详情", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/sparring/tip", + "style": { + "navigationBarTitleText": "AI陪练提示", + "disableSwipeBack": true, + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/sparring/chooseRole", + "style": { + "navigationBarTitleText": "AI陪练选择角色", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/sparring/talk", + "style": { + "navigationBarTitleText": "AI陪练通话", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/sparring/result", + "style": { + "navigationBarTitleText": "AI陪练报告", + "disableSwipeBack": true, + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/analytics/study", + "style": { + "navigationBarTitleText": "学习分析", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/traRecord/index", + "style": { + "navigationBarTitleText": "陪练记录页", + "app-plus": { + "titleNView": false + } + } + } + ], + "tabBar": { + "list": [ + { + "text": "首页", + "pagePath": "pages/index/index" + }, + { + "text": "学习", + "pagePath": "pages/course/index" + }, + { + "text": "场景", + "pagePath": "pages/sparring/index" + }, + { + "text": "我的", + "pagePath": "pages/me/index" + } + ], + "color": "#666666", + "selectedColor": "#0066FF", + "fontSize": "16px", + "borderStyle": "white", + "borderColor": "#ffffff", + "height": "70px", + "spacing": "10px", + "midButton": { + "iconPath": "/static/images/icon/fawn-1.png", + "width": "86px", + "height": "86px", + "iconWidth": "90px" + } + }, + "globalStyle": { + "navigationBarTextStyle": "black", + "navigationBarTitleText": "AI智能培训", + "navigationBarBackgroundColor": "#F8F8F8", + "backgroundColor": "#F8F8F8", + "navigationStyle": "custom", + "app-plus": { + "titleView": false, + "bounce": "none", + "softinputNavBar": "none" + } + }, + "uniIdRouter": {} +} \ No newline at end of file diff --git a/tra-app/src/pages/analytics/study.vue b/tra-app/src/pages/analytics/study.vue new file mode 100644 index 0000000..9c52ab1 --- /dev/null +++ b/tra-app/src/pages/analytics/study.vue @@ -0,0 +1,628 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/charts/index.vue b/tra-app/src/pages/charts/index.vue new file mode 100644 index 0000000..06e2207 --- /dev/null +++ b/tra-app/src/pages/charts/index.vue @@ -0,0 +1,598 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/charts/index_old.vue b/tra-app/src/pages/charts/index_old.vue new file mode 100644 index 0000000..64fd530 --- /dev/null +++ b/tra-app/src/pages/charts/index_old.vue @@ -0,0 +1,845 @@ + + + + + diff --git a/tra-app/src/pages/competition/index.vue b/tra-app/src/pages/competition/index.vue new file mode 100644 index 0000000..d8e40b4 --- /dev/null +++ b/tra-app/src/pages/competition/index.vue @@ -0,0 +1,196 @@ + + + + + diff --git a/tra-app/src/pages/competition/js/matching.ts b/tra-app/src/pages/competition/js/matching.ts new file mode 100644 index 0000000..213b1e2 --- /dev/null +++ b/tra-app/src/pages/competition/js/matching.ts @@ -0,0 +1,64 @@ +/** + * 生成随机匹配时间 + * 特点:同一时间段生成的时间会比较接近,时间范围可配置 + */ +export const MatchTimeGenerator = (() => { + // 可配置的时间范围(毫秒) + const MIN_TIME = 2000; // 最小时间 + const MAX_TIME = 6000; // 最大时间 + + // 时间窗口:100秒内生成的时间会基于同一基准(可根据需要调整) + const TIME_WINDOW = 100000; + // 同一窗口内的最大偏差(±1.5秒,可根据需要调整) + const MAX_DEVIATION = 1500; + + // 记录上一次生成的基准时间和时间戳 + let lastBaseTime = 0; + let lastTimestamp = 0; + + return { + /** + * 生成匹配时间(毫秒) + * @returns {number} 介于MIN_TIME和MAX_TIME之间的随机数(毫秒) + */ + generate() : number { + const now = Date.now(); + let baseTime : number; + + // 如果超过时间窗口或首次生成,重新计算基准时间 + if (now - lastTimestamp > TIME_WINDOW || lastBaseTime === 0) { + // 生成MIN_TIME到MAX_TIME之间的基准时间(毫秒) + const timeRange = MAX_TIME - MIN_TIME; + baseTime = MIN_TIME + Math.floor(Math.random() * timeRange); + lastBaseTime = baseTime; + lastTimestamp = now; + } else { + // 在同一时间窗口内,基于上次基准时间生成偏差 + baseTime = lastBaseTime; + } + + // 生成偏差值(-MAX_DEVIATION 到 MAX_DEVIATION) + const deviation = Math.floor(Math.random() * (MAX_DEVIATION * 2 + 1)) - MAX_DEVIATION; + let matchTime = baseTime + deviation; + + // 确保结果在配置的范围内 + matchTime = Math.max(MIN_TIME, Math.min(MAX_TIME, matchTime)); + + return matchTime; + }, + + /** + * 生成匹配时间(秒) + * @returns {number} 介于MIN_TIME/1000和MAX_TIME/1000之间的随机数(秒,保留一位小数) + */ + generateInSeconds() : number { + return Math.round(this.generate() / 100) / 10; + } + }; +})(); + +export const formatTime = (seconds : number) => { + const m = Math.floor(seconds / 60); + const s = seconds % 60; + return [m.toString().padStart(2, '0'), s.toString().padStart(2, '0')].join(':'); +}; \ No newline at end of file diff --git a/tra-app/src/pages/competition/js/random_avatar.ts b/tra-app/src/pages/competition/js/random_avatar.ts new file mode 100644 index 0000000..e0875f6 --- /dev/null +++ b/tra-app/src/pages/competition/js/random_avatar.ts @@ -0,0 +1,69 @@ +import AvatarW1 from '@/static/images/competition/avatar/f_1.jpg' +import AvatarW2 from '@/static/images/competition/avatar/f_2.jpg' +import AvatarW3 from '@/static/images/competition/avatar/f_3.jpg' +import AvatarW4 from '@/static/images/competition/avatar/f_4.jpg' +import AvatarM1 from '@/static/images/competition/avatar/m_1.jpg' +import AvatarM2 from '@/static/images/competition/avatar/m_2.jpg' +import AvatarM3 from '@/static/images/competition/avatar/m_3.jpg' +import AvatarM4 from '@/static/images/competition/avatar/m_4.jpg' +import AvatarS1 from '@/static/images/competition/avatar/s_1.jpg' +import AvatarS2 from '@/static/images/competition/avatar/s_2.jpg' +import AvatarS3 from '@/static/images/competition/avatar/s_3.jpg' +import AvatarS4 from '@/static/images/competition/avatar/s_4.jpg' + +// 定义头像类型 +type AvatarType = 'f' | 'm' | 's'; + +// 按类型分组 +const avatarsByType = { + f: [AvatarW1, AvatarW2, AvatarW3, AvatarW4], + m: [AvatarM1, AvatarM2, AvatarM3, AvatarM4], + s: [AvatarS1, AvatarS2, AvatarS3, AvatarS4] +}; + +/** + * 随机获取头像 + * @param type1 第一个头像类型(可选) + * @param type2 第二个头像类型(可选,传入则返回2个头像) + * @returns 单个头像路径(当只传type1或都不传时)或两个头像路径的数组(当传入type2时) + */ +export default function getRandomAvatars( + type1 ?: AvatarType, + type2 ?: AvatarType +) { + // 随机获取指定类型的头像 + const getRandomAvatar = (type ?: AvatarType) => { + // 如果指定了类型且有效,则从该类型中选择 + if (type && avatarsByType[type]) { + const avatars = avatarsByType[type]; + return avatars[Math.floor(Math.random() * avatars.length)]; + } + + // 否则从所有头像中随机选择 + const allAvatars = [ + ...avatarsByType.f, + ...avatarsByType.m, + ...avatarsByType.s + ]; + return allAvatars[Math.floor(Math.random() * allAvatars.length)]; + }; + + // 只传type1或都不传时,返回1个头像 + if (type2 === undefined) { + return getRandomAvatar(type1); + } + + // 传入type2时,返回2个头像 + const avatar1 = getRandomAvatar(type1); + let avatar2; + + // 当两个类型相同时,确保头像不重复 + if (type1 && type1 === type2) { + const sameTypeAvatars = avatarsByType[type1].filter(avatar => avatar !== avatar1); + avatar2 = sameTypeAvatars[Math.floor(Math.random() * sameTypeAvatars.length)]; + } else { + avatar2 = getRandomAvatar(type2); + } + + return [avatar1, avatar2]; +} \ No newline at end of file diff --git a/tra-app/src/pages/competition/js/tool.js b/tra-app/src/pages/competition/js/tool.js new file mode 100644 index 0000000..4ea1d19 --- /dev/null +++ b/tra-app/src/pages/competition/js/tool.js @@ -0,0 +1,20 @@ +export function formatSecondsToMS(seconds) { + + // 尝试将输入转换为数字(处理字符串类型的数字) + const num = Number(seconds); + + // 处理无效输入(转换失败、非数字、负数) + if (isNaN(num) || num < 0) { + return '0-秒'; + } + + // 计算分钟(不进位,直接取整) + const minutes = Math.floor(num / 60); + // 计算剩余秒数(取整) + const remainingSeconds = Math.floor(num % 60); + if (minutes === 0) { + return `${remainingSeconds}秒`; + } + // 拼接结果 + return `${minutes}分${remainingSeconds}秒`; +} \ No newline at end of file diff --git a/tra-app/src/pages/competition/matching.vue b/tra-app/src/pages/competition/matching.vue new file mode 100644 index 0000000..d367d5a --- /dev/null +++ b/tra-app/src/pages/competition/matching.vue @@ -0,0 +1,351 @@ + + + + + diff --git a/tra-app/src/pages/competition/pk.vue b/tra-app/src/pages/competition/pk.vue new file mode 100644 index 0000000..d7807f7 --- /dev/null +++ b/tra-app/src/pages/competition/pk.vue @@ -0,0 +1,817 @@ + + + + + diff --git a/tra-app/src/pages/competition/ranking.vue b/tra-app/src/pages/competition/ranking.vue new file mode 100644 index 0000000..c439531 --- /dev/null +++ b/tra-app/src/pages/competition/ranking.vue @@ -0,0 +1,411 @@ + + + + + diff --git a/tra-app/src/pages/competition/result.vue b/tra-app/src/pages/competition/result.vue new file mode 100644 index 0000000..d607719 --- /dev/null +++ b/tra-app/src/pages/competition/result.vue @@ -0,0 +1,655 @@ + + + + diff --git a/tra-app/src/pages/competition/style/matching-loading.scss b/tra-app/src/pages/competition/style/matching-loading.scss new file mode 100644 index 0000000..1f95855 --- /dev/null +++ b/tra-app/src/pages/competition/style/matching-loading.scss @@ -0,0 +1,37 @@ +.match-time-div { + font-weight: 400; + font-size: 34rpx; + color: #ffffff; + line-height: 34rpx; + font-style: normal; + margin-bottom: 20rpx; + transition: opacity 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + opacity: 1; /* 初始显示状态 */ + transform: translateY(0); /* 初始位置 */ + &.hidden { + opacity: 0; /* 完全透明 */ + transform: translateY(40rpx); /* 轻微上移增强淡出效果 */ + pointer-events: none; /* 隐藏后不响应事件 */ + height: 0; /* 可选:完全隐藏高度 */ + overflow: hidden; /* 可选:隐藏溢出内容 */ + } +} +.match-msg-div { + width: 100%; + height: 8vh; + font-weight: normal; + font-size: 72rpx; + color: #ffffff; + display: flex; + justify-content: center; + align-items: flex-end; + transition: opacity 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + opacity: 1; /* 初始显示状态 */ + transform: translateY(0); /* 初始位置 */ + &.hidden { + opacity: 0; /* 完全透明 */ + transform: translateY(-40rpx); /* 轻微上移增强淡出效果 */ + pointer-events: none; /* 隐藏后不响应事件 */ + overflow: hidden; /* 可选:隐藏溢出内容 */ + } +} diff --git a/tra-app/src/pages/competition/style/matching-prelude.scss b/tra-app/src/pages/competition/style/matching-prelude.scss new file mode 100644 index 0000000..809d12a --- /dev/null +++ b/tra-app/src/pages/competition/style/matching-prelude.scss @@ -0,0 +1,348 @@ +.select-div { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + transition: opacity 0.3s ease-out; + opacity: 1; /* 初始显示状态 */ + &.hidden { + opacity: 0; /* 完全透明 */ + pointer-events: none; /* 隐藏后不响应事件 */ + height: 0; /* 可选:完全隐藏高度 */ + overflow: hidden; /* 可选:隐藏溢出内容 */ + } +} +.select-institution-msg-div { + width: calc(100% - 228rpx); + font-weight: 400; + font-size: 22rpx; + color: #ffffff; + line-height: 32rpx; + text-align: left; + font-style: normal; + opacity: 0.8; + padding-top: 18rpx; + padding-bottom: 18rpx; +} + +.select-institution-div { + width: calc(100% - 228rpx); + height: 86rpx; + border: 1rpx solid transparent; + position: relative; + font-weight: 400; + font-size: 32rpx; + line-height: 86rpx; + text-align: center; + color: #ffffff; + background-color: rgba(0, 0, 0, 0.4); + z-index: 1; + overflow: hidden; + padding: 0 60rpx 0 20rpx; + white-space: nowrap; + /* 强制不换行 */ + overflow: hidden; + /* 超出部分隐藏 */ + text-overflow: ellipsis; + .img { + // background-color: red; + padding: 16rpx; + position: absolute; + top: 50%; + transform: translateY(-50%); + right: 10rpx; + width: 30rpx; + height: 30rpx; + } +} + +/* 边框高光效果 */ +.select-institution-div::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border: 4rpx solid transparent; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.8), transparent) border-box; + -webkit-mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + + /* 初始位置在左侧外部 */ + background-size: 200% 100%; + + /* 动画:高光从左到右流动 */ + animation: highlightFlow 4s linear infinite; +} + +/* 定义高光流动动画 */ +@keyframes highlightFlow { + 0% { + background-position: -100% 0; + } + 100% { + background-position: 100% 0; + } +} + +/* 环绕版本带呼吸效果 */ +.select-institution-div.full-flow::before { + background: linear-gradient( + 90deg, + transparent, + rgba(255, 255, 255, 0.8), + transparent, + transparent, + transparent, + transparent, + transparent, + transparent + ); + background-size: 300% 300%; + animation: fullBorderFlow 2s linear infinite, breathing 1s ease-in-out infinite; +} + +@keyframes fullBorderFlow { + 0% { + background-position: 0 0; + } + 25% { + background-position: 100% 0; + } + 50% { + background-position: 100% 100%; + } + 75% { + background-position: 0 100%; + } + 100% { + background-position: 0 0; + } +} + +.title-div { + width: calc(100% - 80rpx); + margin: 40rpx; + font-size: 32rpx; + font-weight: 500; + color: #ffffff; + line-height: 44rpx; + text-shadow: 0px 0px 19px rgba(0, 0, 0, 0.11); + text-align: center; + font-family: none; + min-height: 88rpx; + margin: calc(var(--status-bar-height) + 40rpx); + + /* 添加过渡动画 */ + transition: opacity 0.3s ease-out, transform 0.3s ease-out; + opacity: 1; /* 初始显示状态 */ + transform: translateY(0); /* 初始位置 */ + &.hidden { + opacity: 0; /* 完全透明 */ + transform: translateY(-10rpx); /* 轻微上移增强淡出效果 */ + pointer-events: none; /* 隐藏后不响应事件 */ + height: 0; /* 可选:完全隐藏高度 */ + overflow: hidden; /* 可选:隐藏溢出内容 */ + } +} + +.avatar-div { + margin-top: 20rpx; + .avatar-img-div { + width: 204rpx; + height: 204rpx; + border: 4rpx solid #ffffff; + border-radius: 50%; + position: relative; + overflow: visible; + /* 修复伪元素显示问题 */ + &.hidden::before { + content: ''; + position: absolute; + /* 使用固定尺寸确保计算正确 */ + width: 230rpx; + height: 230rpx; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + /* 确保在z轴上处于正确层级 */ + z-index: 1; + /* 背景图设置 */ + background-image: url('@/static/images/competition/avatar_surround.png'); + background-size: contain; /* 改用contain确保完整显示 */ + background-repeat: no-repeat; + background-position: center; + /* 旋转动画 */ + animation: variableRotate 2s linear infinite; + } + .img { + width: 100%; + height: 100%; + border-radius: 50%; + position: relative; + z-index: 2; + overflow: hidden; + } + } + + @keyframes variableRotate { + 0% { + transform: translate(-50%, -50%) rotate(0deg); + } + 100% { + transform: translate(-50%, -50%) rotate(360deg); + } + } + + .avatar-name-div { + font-weight: 600; + font-size: 46rpx; + color: #ffffff; + line-height: 64rpx; + text-align: center; + font-style: normal; + padding-top: 16rpx; + padding-bottom: 16rpx; + } +} +.button-div { + .start_button { + width: 569rpx; + height: 92rpx; + background: linear-gradient(#009fff 0%, #0066ff 100%); + border-radius: 58rpx; + font-weight: 500; + font-size: 30rpx; + color: #ffffff; + line-height: 92rpx; + text-align: center; + margin-top: 250rpx; + } + .cancel_button { + width: 246rpx; + height: 92rpx; + border-radius: 58rpx; + font-weight: 500; + font-size: 30rpx; + color: #ffffff; + line-height: 92rpx; + text-align: center; + background: rgba(0, 0, 0, 0.5); + background: linear-gradient(rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.5) 100%); + border-radius: 58rpx; + border: 2rpx solid #dbdbdb; + margin-bottom: 250rpx; /* 向上移动50rpx,可根据需要调整 */ + } + .button-transition { + /* 添加过渡动画属性 */ + transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); + /* 确保边框变化也有过渡效果 */ + box-sizing: border-box; + } +} + +.top-button-div { + display: flex; + justify-content: space-between; + width: 100%; + height: 70rpx; + padding-top: calc(16rpx + var(--status-bar-height)); + font-weight: 500; + position: relative; + margin-right: 10rpx; + .top-icon-div { + display: flex; + align-items: center; + margin-right: 30rpx; + font-weight: 400; + font-size: 26rpx; + color: #ffffff; + text-align: left; + .img1, + .img3 { + width: 40rpx; + height: 40rpx; + margin-right: 6rpx; + } + .img2 { + width: 30rpx; + height: 30rpx; + margin-right: 6rpx; + } + } + .back_div { + position: absolute; + left: 26rpx; + height: 34rpx; + top: calc(10rpx + var(--status-bar-height)); + .back-icon { + position: relative; + width: 50rpx; + height: 50rpx; + cursor: pointer; + display: flex; + align-items: center; + } + + .back-icon::before, + .back-icon::after { + content: ''; + position: absolute; + transition: all 0.3s ease; + } + + .back-icon::after { + top: 25%; + left: 25%; + width: 40%; + height: 40%; + border-top: 4rpx solid #ffffff; + border-left: 4rpx solid #ffffff; + transform: rotate(-45deg); + } + } +} + +.popup { + z-index: 1800; + padding: 0 38rpx; + + .sheet_popup_div { + width: 100%; + height: 100%; + background-color: #ffffff; + padding: 10rpx 34rpx; + border-radius: 34rpx 34rpx 0px 0px; + position: relative; + + .title_div { + width: 100%; + padding: 30rpx 0; + font-family: PingFangSC; + font-weight: 600; + font-size: 30rpx; + color: #000000; + font-style: normal; + border-bottom: 2rpx solid #efefef; + + } + .title_back_icon{ + position: absolute; + right: 20rpx; + top: 20rpx; + padding: 10rpx; + .img{ + width: 34rpx; + height: 34rpx; + } + } + .note_div { + padding: 20rpx 0 80rpx 0; + word-wrap: break-word; /* 旧版浏览器兼容,允许长单词换行到下一行 */ + word-break: break-all; /* 强制断词,哪怕是连续字母也会换行(适合小程序) */ + white-space: normal; /* 恢复默认换行(避免被其他样式覆盖) */ + } + } +} diff --git a/tra-app/src/pages/competition/style/matching-success.scss b/tra-app/src/pages/competition/style/matching-success.scss new file mode 100644 index 0000000..da8f009 --- /dev/null +++ b/tra-app/src/pages/competition/style/matching-success.scss @@ -0,0 +1,202 @@ +// 坠落动画配置变量 - 方便调整效果 +$fall-duration: 0.26s; // 动画总时长 +$initial-scale: 2.8; // 初始大小倍数 +$impact-scale: 0.9; // 撞击瞬间的压缩倍数 +$final-scale: 1; // 最终稳定大小 +$primary-color: #ffe271; // 文字颜色 + +.matching-success-text { + margin-top: 10%; + padding-left: 1rem; + // 基础样式 + font-size: 74rpx; + font-weight: bold; + color: #ffe271; + text-align: center; + // 动画属性 + animation: fallDown $fall-duration ease-out forwards; + + // 坠落动画关键帧 + @keyframes fallDown { + 0% { + // 初始状态:高处坠落的起点 + transform: scale($initial-scale); + opacity: 0.8; + } + + 80% { + // 撞击瞬间:略微压缩,增强冲击力 + transform: scale($impact-scale); + opacity: 1; + } + + 100% { + // 稳定状态:恢复正常大小 + transform: scale($final-scale); + opacity: 1; + } + } + + // 增强版:带震动效果的撞击(可选) + &.with-vibration { + animation: fallWithVibration $fall-duration cubic-bezier(0.21, 0.98, 0.6, 0.99) forwards; + + @keyframes fallWithVibration { + 0% { + transform: scale($initial-scale) rotate(0deg); + opacity: 0.8; + } + + 70% { + transform: scale($impact-scale) rotate(2deg); + opacity: 1; + } + + 85% { + transform: scale($final-scale * 1.05) rotate(-1deg); + opacity: 1; + } + + 100% { + transform: scale($final-scale) rotate(0deg); + opacity: 1; + } + } + } +} + +// 可通过修改变量快速调整效果 +// 示例:更重的坠落效果 +.heavy-impact { + $fall-duration: 0.3s; + $initial-scale: 2.5; + $impact-scale: 0.8; + + animation: fallDown $fall-duration ease-out forwards; +} + +.matching-success-msg { + margin-top: 20%; + font-family: Alibaba-PuHuiTi, Alibaba-PuHuiTi; + font-weight: normal; + font-size: 74rpx; + color: #ffffff; + font-weight: 700; + margin-bottom: 10%; +} + +/**匹配成功样式*/ +.matching-success-animation { + display: flex; + width: 100%; + justify-content: space-between; + height: 540rpx; + position: relative; + // overflow: hidden; + .left { + position: absolute; + width: calc(59vw); + left: -10rpx; + top: 0%; + z-index: 3; + } + .right { + position: absolute; + right: -10rpx; + top: 18vw; + width: calc(59vw); + z-index: 2; + } + .img { + width: 100%; + } + // 左侧元素回弹动画 + .animate-left { + animation: leftCollision 0.24s ease-out forwards; + } + @keyframes leftCollision { + 0% { + transform: translateX(-100%); + } // 初始位置 + 90% { + transform: translateX(10rpx); + } // 超过目标位置10rpx(冲刺) + 100% { + transform: translateX(0); + } // 回到目标位置(回弹) + } + + // 右侧元素回弹动画 + .animate-right { + animation: rightCollision 0.24s ease-out forwards; + } + @keyframes rightCollision { + 0% { + transform: translateX(100%); + } // 初始位置 + 90% { + transform: translateX(-10rpx); + } // 超过目标位置10rpx(冲刺) + 100% { + transform: translateX(0); + } // 回到目标位置(回弹) + } + + // 上面的双方头像 + .information-div-left, + .information-div-right { + display: flex; + flex-direction: column; + align-items: center; + // background-color: red; + width: 60%; + .avatar-img-div { + width: 120rpx; + height: 120rpx; + border: 4rpx solid #ffffff; + border-radius: 50%; + overflow: hidden; + .img { + width: 100%; + height: 100%; + } + } + .name { + font-size: 30rpx; + margin-top: 20rpx; + margin-bottom: 50rpx; + font-weight: 600; + font-size: 36rpx; + color: #ffffff; + // background-color: red; + } + .org { + // background-color: red; + font-weight: 400; + font-size: 25rpx; + color: #ffffff; + // white-space: nowrap; + overflow: hidden; + // text-overflow: ellipsis; + // width: 160rpx; + } + } + .information-div-left { + position: absolute; + top: 0%; + left: 0%; + transform: translate(10%, -20%); + .org { + text-align: left; + } + } + .information-div-right { + position: absolute; + right: 0%; + top: 0%; + transform: translate(-10%, -20%); + .org { + text-align: right; + } + } +} diff --git a/tra-app/src/pages/competitionRecord/components/competitionRecordItem.vue b/tra-app/src/pages/competitionRecord/components/competitionRecordItem.vue new file mode 100644 index 0000000..0424cfa --- /dev/null +++ b/tra-app/src/pages/competitionRecord/components/competitionRecordItem.vue @@ -0,0 +1,253 @@ + + + + + diff --git a/tra-app/src/pages/competitionRecord/index.vue b/tra-app/src/pages/competitionRecord/index.vue new file mode 100644 index 0000000..c9a0f3a --- /dev/null +++ b/tra-app/src/pages/competitionRecord/index.vue @@ -0,0 +1,139 @@ + + + + + diff --git a/tra-app/src/pages/course/components/chartFive.vue b/tra-app/src/pages/course/components/chartFive.vue new file mode 100644 index 0000000..34fed90 --- /dev/null +++ b/tra-app/src/pages/course/components/chartFive.vue @@ -0,0 +1,370 @@ + + + \ No newline at end of file diff --git a/tra-app/src/pages/course/components/chartFour.vue b/tra-app/src/pages/course/components/chartFour.vue new file mode 100644 index 0000000..281de97 --- /dev/null +++ b/tra-app/src/pages/course/components/chartFour.vue @@ -0,0 +1,296 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/course/components/chartSeven.vue b/tra-app/src/pages/course/components/chartSeven.vue new file mode 100644 index 0000000..203d185 --- /dev/null +++ b/tra-app/src/pages/course/components/chartSeven.vue @@ -0,0 +1,347 @@ + + + \ No newline at end of file diff --git a/tra-app/src/pages/course/components/chartSix.vue b/tra-app/src/pages/course/components/chartSix.vue new file mode 100644 index 0000000..588ce97 --- /dev/null +++ b/tra-app/src/pages/course/components/chartSix.vue @@ -0,0 +1,342 @@ + + + \ No newline at end of file diff --git a/tra-app/src/pages/course/components/chartThree.vue b/tra-app/src/pages/course/components/chartThree.vue new file mode 100644 index 0000000..96e89d0 --- /dev/null +++ b/tra-app/src/pages/course/components/chartThree.vue @@ -0,0 +1,304 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/course/components/talkingItem.vue b/tra-app/src/pages/course/components/talkingItem.vue new file mode 100644 index 0000000..5cced8f --- /dev/null +++ b/tra-app/src/pages/course/components/talkingItem.vue @@ -0,0 +1,1174 @@ + + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/course/components/touchBtn.vue b/tra-app/src/pages/course/components/touchBtn.vue new file mode 100644 index 0000000..3d1a24b --- /dev/null +++ b/tra-app/src/pages/course/components/touchBtn.vue @@ -0,0 +1,660 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/course/index.vue b/tra-app/src/pages/course/index.vue new file mode 100644 index 0000000..70180b4 --- /dev/null +++ b/tra-app/src/pages/course/index.vue @@ -0,0 +1,915 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/course/study.vue b/tra-app/src/pages/course/study.vue new file mode 100644 index 0000000..9413450 --- /dev/null +++ b/tra-app/src/pages/course/study.vue @@ -0,0 +1,1664 @@ + + + + + diff --git a/tra-app/src/pages/courseDetail/components/comment.vue b/tra-app/src/pages/courseDetail/components/comment.vue new file mode 100644 index 0000000..68523f8 --- /dev/null +++ b/tra-app/src/pages/courseDetail/components/comment.vue @@ -0,0 +1,495 @@ + + + + + diff --git a/tra-app/src/pages/courseDetail/components/introduce.vue b/tra-app/src/pages/courseDetail/components/introduce.vue new file mode 100644 index 0000000..466a384 --- /dev/null +++ b/tra-app/src/pages/courseDetail/components/introduce.vue @@ -0,0 +1,170 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/courseDetail/components/knowledge.vue b/tra-app/src/pages/courseDetail/components/knowledge.vue new file mode 100644 index 0000000..4fc3575 --- /dev/null +++ b/tra-app/src/pages/courseDetail/components/knowledge.vue @@ -0,0 +1,93 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/courseDetail/index.vue b/tra-app/src/pages/courseDetail/index.vue new file mode 100644 index 0000000..cba7fbc --- /dev/null +++ b/tra-app/src/pages/courseDetail/index.vue @@ -0,0 +1,530 @@ + + + + + diff --git a/tra-app/src/pages/courseDetail/submit.vue b/tra-app/src/pages/courseDetail/submit.vue new file mode 100644 index 0000000..a38ba55 --- /dev/null +++ b/tra-app/src/pages/courseDetail/submit.vue @@ -0,0 +1,317 @@ + + + + + diff --git a/tra-app/src/pages/courseRecord/components/esQuestion.vue b/tra-app/src/pages/courseRecord/components/esQuestion.vue new file mode 100644 index 0000000..4695d8b --- /dev/null +++ b/tra-app/src/pages/courseRecord/components/esQuestion.vue @@ -0,0 +1,314 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/courseRecord/components/esQuestionAnswer.vue b/tra-app/src/pages/courseRecord/components/esQuestionAnswer.vue new file mode 100644 index 0000000..c5de046 --- /dev/null +++ b/tra-app/src/pages/courseRecord/components/esQuestionAnswer.vue @@ -0,0 +1,369 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/courseRecord/components/examRecordItem.vue b/tra-app/src/pages/courseRecord/components/examRecordItem.vue new file mode 100644 index 0000000..46cb8dc --- /dev/null +++ b/tra-app/src/pages/courseRecord/components/examRecordItem.vue @@ -0,0 +1,251 @@ + + + + + diff --git a/tra-app/src/pages/courseRecord/components/examRecordList.vue b/tra-app/src/pages/courseRecord/components/examRecordList.vue new file mode 100644 index 0000000..b7525da --- /dev/null +++ b/tra-app/src/pages/courseRecord/components/examRecordList.vue @@ -0,0 +1,53 @@ + + + + diff --git a/tra-app/src/pages/courseRecord/components/practiceRecordItem.vue b/tra-app/src/pages/courseRecord/components/practiceRecordItem.vue new file mode 100644 index 0000000..129e69a --- /dev/null +++ b/tra-app/src/pages/courseRecord/components/practiceRecordItem.vue @@ -0,0 +1,250 @@ + + + + + diff --git a/tra-app/src/pages/courseRecord/components/practiceRecordList.vue b/tra-app/src/pages/courseRecord/components/practiceRecordList.vue new file mode 100644 index 0000000..9fb68f3 --- /dev/null +++ b/tra-app/src/pages/courseRecord/components/practiceRecordList.vue @@ -0,0 +1,53 @@ + + + + diff --git a/tra-app/src/pages/courseRecord/components/studyRecordItem.vue b/tra-app/src/pages/courseRecord/components/studyRecordItem.vue new file mode 100644 index 0000000..ce72abd --- /dev/null +++ b/tra-app/src/pages/courseRecord/components/studyRecordItem.vue @@ -0,0 +1,250 @@ + + + + + diff --git a/tra-app/src/pages/courseRecord/components/studyRecordList.vue b/tra-app/src/pages/courseRecord/components/studyRecordList.vue new file mode 100644 index 0000000..67a037c --- /dev/null +++ b/tra-app/src/pages/courseRecord/components/studyRecordList.vue @@ -0,0 +1,56 @@ + + + + diff --git a/tra-app/src/pages/courseRecord/components/talkingItem.vue b/tra-app/src/pages/courseRecord/components/talkingItem.vue new file mode 100644 index 0000000..8d7b96c --- /dev/null +++ b/tra-app/src/pages/courseRecord/components/talkingItem.vue @@ -0,0 +1,925 @@ + + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/courseRecord/coursePracticeRecordDetail.vue b/tra-app/src/pages/courseRecord/coursePracticeRecordDetail.vue new file mode 100644 index 0000000..20a838b --- /dev/null +++ b/tra-app/src/pages/courseRecord/coursePracticeRecordDetail.vue @@ -0,0 +1,132 @@ + + + + + diff --git a/tra-app/src/pages/courseRecord/courseStudyRecordDetail.vue b/tra-app/src/pages/courseRecord/courseStudyRecordDetail.vue new file mode 100644 index 0000000..d151f14 --- /dev/null +++ b/tra-app/src/pages/courseRecord/courseStudyRecordDetail.vue @@ -0,0 +1,177 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/courseRecord/index.vue b/tra-app/src/pages/courseRecord/index.vue new file mode 100644 index 0000000..4c99006 --- /dev/null +++ b/tra-app/src/pages/courseRecord/index.vue @@ -0,0 +1,172 @@ + + + + + diff --git a/tra-app/src/pages/courseRecord/useItemList.js b/tra-app/src/pages/courseRecord/useItemList.js new file mode 100644 index 0000000..97581fa --- /dev/null +++ b/tra-app/src/pages/courseRecord/useItemList.js @@ -0,0 +1,114 @@ +import { + computed, + reactive, + nextTick, + ref +} from 'vue'; + + +// 计算两个标准时间的时间差值 +// export const formatDuration = (startTm, endTm) => { +// if (endTm === null || startTm === null) return "00:00:00"; +// // 解析时间字符串为时间戳(毫秒) +// const startTime = new Date(startTm).getTime(); +// const endTime = new Date(endTm).getTime(); +// +// // 计算时间差(秒),确保为正数 +// const seconds = Math.abs(Math.floor((endTime - startTime) / 1000)); +// // 计算时、分、秒 +// const hours = Math.floor(seconds / 3600); +// const minutes = Math.floor((seconds % 3600) / 60); +// const remainingSeconds = seconds % 60; +// +// // 补零格式化函数(统一处理所有单位) +// const formatNumber = (num) => num.toString().padStart(2, '0'); +// +// // 小时、分钟、秒均保持两位数格式 +// return `${formatNumber(hours)}:${formatNumber(minutes)}:${formatNumber(remainingSeconds)}`; +// }; + +// 秒数转换成小时,带小数点的小时 +export const secondsToHours = (seconds) => { + if (typeof seconds === 'number') { + // 转换为小时并保留一位小数 + return (seconds / 3600).toFixed(1); + } + return '0.0'; +}; + +export function useItemList(fetchFunction, item) { + const status = ref('loadmore'); // loadmore - 加载前,loading - 加载中,nomore - 没有数据 + const limit = 5; + const page = ref(0); + const total = ref(-1); + const data_list = reactive([]); + const show_list_state = ref(false); + + const list_div_height = computed(() => { + console.log('小仙仙', status.value, data_list.length); + const statusHeight = status.value === 'nomore' ? 0 : 100; + if (!show_list_state.value) { + return '0'; + } else if (status.value === 'loading') { + return data_list.length * 136 + statusHeight + 'rpx'; + } else { + return data_list.length * 136 + statusHeight + 'rpx'; + } + }); + // 点击获取详情 + const show_list_click = (num = -1) => { + if (num === 0) return; + show_list_state.value = !show_list_state.value; + if (show_list_state.value) { + nextTick(() => { + getData('first'); + }); + } + }; + + const getData = (from = '') => { + console.log('getData'); + if (from === 'first') { + if (total.value !== -1) { + return; + } + status.value = 'loading'; + total.value = -1; + page.value = 1; + data_list.length = 0; + } else { + if (status.value !== 'loadmore' && status.value !== '') return; + status.value = 'loading'; + page.value++; + } + fetchFunction({ + page: page.value, + limit: limit + }) + .then((res) => { + total.value = res.total; + data_list.push(...res.body); + }) + .finally(() => { + if (data_list.length >= total.value) { + status.value = 'nomore'; + } else { + status.value = 'loadmore'; + } + }); + }; + const clear = () => { + total.value = -1 + data_list.length = 0 + show_list_state.value = false + } + return { + status, + data_list, + show_list_state, + list_div_height, + show_list_click, + getData, + clear + }; +} \ No newline at end of file diff --git a/tra-app/src/pages/examRanking/index.vue b/tra-app/src/pages/examRanking/index.vue new file mode 100644 index 0000000..5627107 --- /dev/null +++ b/tra-app/src/pages/examRanking/index.vue @@ -0,0 +1,380 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/examination/components/dialog.vue b/tra-app/src/pages/examination/components/dialog.vue new file mode 100644 index 0000000..299b587 --- /dev/null +++ b/tra-app/src/pages/examination/components/dialog.vue @@ -0,0 +1,117 @@ + + + + + diff --git a/tra-app/src/pages/examination/components/feedback.vue b/tra-app/src/pages/examination/components/feedback.vue new file mode 100644 index 0000000..8aea1a9 --- /dev/null +++ b/tra-app/src/pages/examination/components/feedback.vue @@ -0,0 +1,232 @@ + + + + + diff --git a/tra-app/src/pages/examination/index.vue b/tra-app/src/pages/examination/index.vue new file mode 100644 index 0000000..a53dff4 --- /dev/null +++ b/tra-app/src/pages/examination/index.vue @@ -0,0 +1,992 @@ + + + + + diff --git a/tra-app/src/pages/examination/result.vue b/tra-app/src/pages/examination/result.vue new file mode 100644 index 0000000..ee40d31 --- /dev/null +++ b/tra-app/src/pages/examination/result.vue @@ -0,0 +1,783 @@ + + + + + diff --git a/tra-app/src/pages/examinationRecord/components/examRecordItem.vue b/tra-app/src/pages/examinationRecord/components/examRecordItem.vue new file mode 100644 index 0000000..731a102 --- /dev/null +++ b/tra-app/src/pages/examinationRecord/components/examRecordItem.vue @@ -0,0 +1,249 @@ + + + + + diff --git a/tra-app/src/pages/examinationRecord/index.vue b/tra-app/src/pages/examinationRecord/index.vue new file mode 100644 index 0000000..f66f9d5 --- /dev/null +++ b/tra-app/src/pages/examinationRecord/index.vue @@ -0,0 +1,138 @@ + + + + + diff --git a/tra-app/src/pages/example/components/answer.vue b/tra-app/src/pages/example/components/answer.vue new file mode 100644 index 0000000..49b74e0 --- /dev/null +++ b/tra-app/src/pages/example/components/answer.vue @@ -0,0 +1,413 @@ + + + + + diff --git a/tra-app/src/pages/example/components/question.vue b/tra-app/src/pages/example/components/question.vue new file mode 100644 index 0000000..d8623d8 --- /dev/null +++ b/tra-app/src/pages/example/components/question.vue @@ -0,0 +1,266 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/example/components/talking-item.vue b/tra-app/src/pages/example/components/talking-item.vue new file mode 100644 index 0000000..d7b49ad --- /dev/null +++ b/tra-app/src/pages/example/components/talking-item.vue @@ -0,0 +1,81 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/example/dialog.vue b/tra-app/src/pages/example/dialog.vue new file mode 100644 index 0000000..f23d5a3 --- /dev/null +++ b/tra-app/src/pages/example/dialog.vue @@ -0,0 +1,1024 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/favorites/components/favoritesCourse.vue b/tra-app/src/pages/favorites/components/favoritesCourse.vue new file mode 100644 index 0000000..7b313b8 --- /dev/null +++ b/tra-app/src/pages/favorites/components/favoritesCourse.vue @@ -0,0 +1,200 @@ + + + + diff --git a/tra-app/src/pages/favorites/components/favoritesTask.vue b/tra-app/src/pages/favorites/components/favoritesTask.vue new file mode 100644 index 0000000..2a9705d --- /dev/null +++ b/tra-app/src/pages/favorites/components/favoritesTask.vue @@ -0,0 +1,272 @@ + + + + diff --git a/tra-app/src/pages/favorites/index.vue b/tra-app/src/pages/favorites/index.vue new file mode 100644 index 0000000..ea3e040 --- /dev/null +++ b/tra-app/src/pages/favorites/index.vue @@ -0,0 +1,173 @@ + + + + + diff --git a/tra-app/src/pages/index/components/answer.vue b/tra-app/src/pages/index/components/answer.vue new file mode 100644 index 0000000..f8fcb00 --- /dev/null +++ b/tra-app/src/pages/index/components/answer.vue @@ -0,0 +1,303 @@ + + + + + diff --git a/tra-app/src/pages/index/components/class_scroll.vue b/tra-app/src/pages/index/components/class_scroll.vue new file mode 100644 index 0000000..10ea912 --- /dev/null +++ b/tra-app/src/pages/index/components/class_scroll.vue @@ -0,0 +1,261 @@ + + + + + diff --git a/tra-app/src/pages/index/components/feedback.vue b/tra-app/src/pages/index/components/feedback.vue new file mode 100644 index 0000000..ab5cf7e --- /dev/null +++ b/tra-app/src/pages/index/components/feedback.vue @@ -0,0 +1,239 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/index/components/history.vue b/tra-app/src/pages/index/components/history.vue new file mode 100644 index 0000000..3167c9b --- /dev/null +++ b/tra-app/src/pages/index/components/history.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/tra-app/src/pages/index/components/preview.vue b/tra-app/src/pages/index/components/preview.vue new file mode 100644 index 0000000..4d0a182 --- /dev/null +++ b/tra-app/src/pages/index/components/preview.vue @@ -0,0 +1,93 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/index/components/question.vue b/tra-app/src/pages/index/components/question.vue new file mode 100644 index 0000000..d8623d8 --- /dev/null +++ b/tra-app/src/pages/index/components/question.vue @@ -0,0 +1,266 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/index/components/table.vue b/tra-app/src/pages/index/components/table.vue new file mode 100644 index 0000000..7699431 --- /dev/null +++ b/tra-app/src/pages/index/components/table.vue @@ -0,0 +1,172 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/index/components/talking-item.vue b/tra-app/src/pages/index/components/talking-item.vue new file mode 100644 index 0000000..d7b49ad --- /dev/null +++ b/tra-app/src/pages/index/components/talking-item.vue @@ -0,0 +1,81 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/index/dialog.vue b/tra-app/src/pages/index/dialog.vue new file mode 100644 index 0000000..4d0f6bc --- /dev/null +++ b/tra-app/src/pages/index/dialog.vue @@ -0,0 +1,1147 @@ + + + + + diff --git a/tra-app/src/pages/index/index.js b/tra-app/src/pages/index/index.js new file mode 100644 index 0000000..01fc071 --- /dev/null +++ b/tra-app/src/pages/index/index.js @@ -0,0 +1,20 @@ +// 静态样式配置 +export const customStyles = { + backgroundColor: "#DCE9FF", + paddingLeft: "40rpx", + paddingTop: "5px", + paddingBottom: "5px", +} +export const fixedCustomStyles = { + backgroundColor: "#ffffff", + paddingLeft: "40rpx", + paddingTop: "5px", + paddingBottom: "5px", + border: "none" +} +export const placeholderStyle = 'color:#76A3EB;font-size:26rpx' +export const suffixIconStyle = { + color: "#2c8ef2", + fontSize: "28px" +} + diff --git a/tra-app/src/pages/index/index.vue b/tra-app/src/pages/index/index.vue new file mode 100644 index 0000000..95a3dc9 --- /dev/null +++ b/tra-app/src/pages/index/index.vue @@ -0,0 +1,755 @@ + + + + + diff --git a/tra-app/src/pages/index/preview.vue b/tra-app/src/pages/index/preview.vue new file mode 100644 index 0000000..ff9740b --- /dev/null +++ b/tra-app/src/pages/index/preview.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/tra-app/src/pages/intelligentAnswering/index.vue b/tra-app/src/pages/intelligentAnswering/index.vue new file mode 100644 index 0000000..e1e9224 --- /dev/null +++ b/tra-app/src/pages/intelligentAnswering/index.vue @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/learningTasks/components/content-item.vue b/tra-app/src/pages/learningTasks/components/content-item.vue new file mode 100644 index 0000000..61daaf2 --- /dev/null +++ b/tra-app/src/pages/learningTasks/components/content-item.vue @@ -0,0 +1,179 @@ + + + + + diff --git a/tra-app/src/pages/learningTasks/components/content.vue b/tra-app/src/pages/learningTasks/components/content.vue new file mode 100644 index 0000000..b92706a --- /dev/null +++ b/tra-app/src/pages/learningTasks/components/content.vue @@ -0,0 +1,84 @@ + + + + diff --git a/tra-app/src/pages/learningTasks/details.vue b/tra-app/src/pages/learningTasks/details.vue new file mode 100644 index 0000000..ecb3651 --- /dev/null +++ b/tra-app/src/pages/learningTasks/details.vue @@ -0,0 +1,699 @@ + + + + + diff --git a/tra-app/src/pages/learningTasks/index.vue b/tra-app/src/pages/learningTasks/index.vue new file mode 100644 index 0000000..fb80c34 --- /dev/null +++ b/tra-app/src/pages/learningTasks/index.vue @@ -0,0 +1,180 @@ + + + + + diff --git a/tra-app/src/pages/login/login.vue b/tra-app/src/pages/login/login.vue new file mode 100644 index 0000000..e593fc3 --- /dev/null +++ b/tra-app/src/pages/login/login.vue @@ -0,0 +1,360 @@ + + + + + diff --git a/tra-app/src/pages/login/ysxy.vue b/tra-app/src/pages/login/ysxy.vue new file mode 100644 index 0000000..6bd5444 --- /dev/null +++ b/tra-app/src/pages/login/ysxy.vue @@ -0,0 +1,119 @@ + + + + diff --git a/tra-app/src/pages/mascot/mascot_select_list.vue b/tra-app/src/pages/mascot/mascot_select_list.vue new file mode 100644 index 0000000..e112476 --- /dev/null +++ b/tra-app/src/pages/mascot/mascot_select_list.vue @@ -0,0 +1,309 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/me/index.vue b/tra-app/src/pages/me/index.vue new file mode 100644 index 0000000..05e13aa --- /dev/null +++ b/tra-app/src/pages/me/index.vue @@ -0,0 +1,588 @@ + + + + + diff --git a/tra-app/src/pages/messageNotification/index.vue b/tra-app/src/pages/messageNotification/index.vue new file mode 100644 index 0000000..38dd8b0 --- /dev/null +++ b/tra-app/src/pages/messageNotification/index.vue @@ -0,0 +1,339 @@ + + + + + diff --git a/tra-app/src/pages/messageNotification/list.vue b/tra-app/src/pages/messageNotification/list.vue new file mode 100644 index 0000000..ecbb045 --- /dev/null +++ b/tra-app/src/pages/messageNotification/list.vue @@ -0,0 +1,256 @@ + + + + + diff --git a/tra-app/src/pages/other/web_view.vue b/tra-app/src/pages/other/web_view.vue new file mode 100644 index 0000000..72b057f --- /dev/null +++ b/tra-app/src/pages/other/web_view.vue @@ -0,0 +1,77 @@ + + + + + + diff --git a/tra-app/src/pages/pointsAndRank/badge.vue b/tra-app/src/pages/pointsAndRank/badge.vue new file mode 100644 index 0000000..93dc713 --- /dev/null +++ b/tra-app/src/pages/pointsAndRank/badge.vue @@ -0,0 +1,288 @@ + + + + + diff --git a/tra-app/src/pages/pointsAndRank/points.vue b/tra-app/src/pages/pointsAndRank/points.vue new file mode 100644 index 0000000..ece5258 --- /dev/null +++ b/tra-app/src/pages/pointsAndRank/points.vue @@ -0,0 +1,279 @@ + + + + + diff --git a/tra-app/src/pages/pointsAndRank/rank.vue b/tra-app/src/pages/pointsAndRank/rank.vue new file mode 100644 index 0000000..acd8524 --- /dev/null +++ b/tra-app/src/pages/pointsAndRank/rank.vue @@ -0,0 +1,258 @@ + + + + + diff --git a/tra-app/src/pages/pointsRedemption/index.vue b/tra-app/src/pages/pointsRedemption/index.vue new file mode 100644 index 0000000..b369c2b --- /dev/null +++ b/tra-app/src/pages/pointsRedemption/index.vue @@ -0,0 +1,292 @@ + + + + + diff --git a/tra-app/src/pages/practice/index.vue b/tra-app/src/pages/practice/index.vue new file mode 100644 index 0000000..c1d47a1 --- /dev/null +++ b/tra-app/src/pages/practice/index.vue @@ -0,0 +1,769 @@ + + + + + diff --git a/tra-app/src/pages/practice/result.vue b/tra-app/src/pages/practice/result.vue new file mode 100644 index 0000000..7f54975 --- /dev/null +++ b/tra-app/src/pages/practice/result.vue @@ -0,0 +1,280 @@ + + + + + diff --git a/tra-app/src/pages/preview/components/previewAiSparring.vue b/tra-app/src/pages/preview/components/previewAiSparring.vue new file mode 100644 index 0000000..9451638 --- /dev/null +++ b/tra-app/src/pages/preview/components/previewAiSparring.vue @@ -0,0 +1,124 @@ + + + + \ No newline at end of file diff --git a/tra-app/src/pages/preview/components/previewCourse.vue b/tra-app/src/pages/preview/components/previewCourse.vue new file mode 100644 index 0000000..283cb1c --- /dev/null +++ b/tra-app/src/pages/preview/components/previewCourse.vue @@ -0,0 +1,136 @@ + + + + diff --git a/tra-app/src/pages/preview/index.vue b/tra-app/src/pages/preview/index.vue new file mode 100644 index 0000000..82bb3a3 --- /dev/null +++ b/tra-app/src/pages/preview/index.vue @@ -0,0 +1,158 @@ + + + + + diff --git a/tra-app/src/pages/scene/index.vue b/tra-app/src/pages/scene/index.vue new file mode 100644 index 0000000..fd24b05 --- /dev/null +++ b/tra-app/src/pages/scene/index.vue @@ -0,0 +1,46 @@ + + + \ No newline at end of file diff --git a/tra-app/src/pages/search/result.vue b/tra-app/src/pages/search/result.vue new file mode 100644 index 0000000..9085ef1 --- /dev/null +++ b/tra-app/src/pages/search/result.vue @@ -0,0 +1,737 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/search/search.vue b/tra-app/src/pages/search/search.vue new file mode 100644 index 0000000..3c2dedd --- /dev/null +++ b/tra-app/src/pages/search/search.vue @@ -0,0 +1,242 @@ + + + + + diff --git a/tra-app/src/pages/setting/index.vue b/tra-app/src/pages/setting/index.vue new file mode 100644 index 0000000..3e2fab4 --- /dev/null +++ b/tra-app/src/pages/setting/index.vue @@ -0,0 +1,250 @@ + + + + + diff --git a/tra-app/src/pages/sparring/chooseRole.vue b/tra-app/src/pages/sparring/chooseRole.vue new file mode 100644 index 0000000..b32783a --- /dev/null +++ b/tra-app/src/pages/sparring/chooseRole.vue @@ -0,0 +1,287 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/sparring/components/answer.vue b/tra-app/src/pages/sparring/components/answer.vue new file mode 100644 index 0000000..c9ee804 --- /dev/null +++ b/tra-app/src/pages/sparring/components/answer.vue @@ -0,0 +1,484 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/sparring/components/card-swiper.vue b/tra-app/src/pages/sparring/components/card-swiper.vue new file mode 100644 index 0000000..44d157b --- /dev/null +++ b/tra-app/src/pages/sparring/components/card-swiper.vue @@ -0,0 +1,278 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/sparring/components/feedback.vue b/tra-app/src/pages/sparring/components/feedback.vue new file mode 100644 index 0000000..cf9fcec --- /dev/null +++ b/tra-app/src/pages/sparring/components/feedback.vue @@ -0,0 +1,235 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/sparring/components/question-preview.vue b/tra-app/src/pages/sparring/components/question-preview.vue new file mode 100644 index 0000000..e340391 --- /dev/null +++ b/tra-app/src/pages/sparring/components/question-preview.vue @@ -0,0 +1,308 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/sparring/components/question.vue b/tra-app/src/pages/sparring/components/question.vue new file mode 100644 index 0000000..d9b1e7d --- /dev/null +++ b/tra-app/src/pages/sparring/components/question.vue @@ -0,0 +1,330 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/sparring/components/sparring-item.vue b/tra-app/src/pages/sparring/components/sparring-item.vue new file mode 100644 index 0000000..565cf33 --- /dev/null +++ b/tra-app/src/pages/sparring/components/sparring-item.vue @@ -0,0 +1,190 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/sparring/components/talking-item.vue b/tra-app/src/pages/sparring/components/talking-item.vue new file mode 100644 index 0000000..bb5e1d5 --- /dev/null +++ b/tra-app/src/pages/sparring/components/talking-item.vue @@ -0,0 +1,211 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/sparring/components/talking-preview-item.vue b/tra-app/src/pages/sparring/components/talking-preview-item.vue new file mode 100644 index 0000000..e584d15 --- /dev/null +++ b/tra-app/src/pages/sparring/components/talking-preview-item.vue @@ -0,0 +1,180 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/sparring/components/touchBtn.vue b/tra-app/src/pages/sparring/components/touchBtn.vue new file mode 100644 index 0000000..dd26ed3 --- /dev/null +++ b/tra-app/src/pages/sparring/components/touchBtn.vue @@ -0,0 +1,560 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/sparring/detail.vue b/tra-app/src/pages/sparring/detail.vue new file mode 100644 index 0000000..1bb1291 --- /dev/null +++ b/tra-app/src/pages/sparring/detail.vue @@ -0,0 +1,399 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/sparring/index.vue b/tra-app/src/pages/sparring/index.vue new file mode 100644 index 0000000..8f30784 --- /dev/null +++ b/tra-app/src/pages/sparring/index.vue @@ -0,0 +1,826 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/sparring/result.vue b/tra-app/src/pages/sparring/result.vue new file mode 100644 index 0000000..7227ae3 --- /dev/null +++ b/tra-app/src/pages/sparring/result.vue @@ -0,0 +1,702 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/sparring/talk.vue b/tra-app/src/pages/sparring/talk.vue new file mode 100644 index 0000000..86dc355 --- /dev/null +++ b/tra-app/src/pages/sparring/talk.vue @@ -0,0 +1,164 @@ + + + \ No newline at end of file diff --git a/tra-app/src/pages/sparring/talk1.vue b/tra-app/src/pages/sparring/talk1.vue new file mode 100644 index 0000000..913cc10 --- /dev/null +++ b/tra-app/src/pages/sparring/talk1.vue @@ -0,0 +1,147 @@ + + + + diff --git a/tra-app/src/pages/sparring/talk3.vue b/tra-app/src/pages/sparring/talk3.vue new file mode 100644 index 0000000..e50d99b --- /dev/null +++ b/tra-app/src/pages/sparring/talk3.vue @@ -0,0 +1,182 @@ + + + + + diff --git a/tra-app/src/pages/sparring/talkCom.nvue b/tra-app/src/pages/sparring/talkCom.nvue new file mode 100644 index 0000000..ad0e5d1 --- /dev/null +++ b/tra-app/src/pages/sparring/talkCom.nvue @@ -0,0 +1,158 @@ + + + \ No newline at end of file diff --git a/tra-app/src/pages/sparring/tip.vue b/tra-app/src/pages/sparring/tip.vue new file mode 100644 index 0000000..38bd37a --- /dev/null +++ b/tra-app/src/pages/sparring/tip.vue @@ -0,0 +1,842 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/study/index.vue b/tra-app/src/pages/study/index.vue new file mode 100644 index 0000000..a0f7413 --- /dev/null +++ b/tra-app/src/pages/study/index.vue @@ -0,0 +1,50 @@ + + + \ No newline at end of file diff --git a/tra-app/src/pages/test/ProtocolCodec.ts b/tra-app/src/pages/test/ProtocolCodec.ts new file mode 100644 index 0000000..97851d8 --- /dev/null +++ b/tra-app/src/pages/test/ProtocolCodec.ts @@ -0,0 +1,365 @@ +// 导入 GZIP 处理库(浏览器/Node.js 通用,需提前安装:npm install pako @types/pako) +// import * as pako from "pako"; + +/** + * 协议常量类:存储协议核心配置(不可修改,确保前后端一致) + */ +export class ProtocolConst { + /** 协议版本:0~15(4位,存储在字节0低4位) */ + static readonly PROTOCOL_VERSION = 0b0001; + + /** 头部固定长度:8字节(字节0~7,结构严格定义,不可修改) */ + static readonly HEADER_SIZE = 8; + + /** + * 最大包体大小:16MB(3字节长度最大支持0xFFFFFF=16777215字节≈16MB) + * 4-6字节存储(24位),最大支持16MB,满足大部分场景且避免长度字段冗余 + */ + static readonly MAX_BODY_SIZE = 0xFFFFFF; // 16777215字节 ≈16MB + + /** 字符串编码格式:UTF-8(统一前后端字符串编解码,避免乱码) */ + static readonly STRING_ENCODING = "utf-8" as const; +} + +/** + * 消息类型枚举(4位,存储在字节0高4位,0~15范围) + * 二进制标识,统一编码风格 + */ +export enum MessageType { + PING = 0b0001, // 心跳消息(支持空包体) + AUDIO_DATA = 0b0010, // 纯音频数据 + TEXT_MESSAGE = 0b0011, // 纯文本消息 + CONTROL_CMD = 0b0100, // 控制指令 + IDENTITY = 0b0101, // 身份校验包json格式 + ERROR = 0b0110 // 错误信息json格式 + // 预留12种类型用于扩展(0b0100 ~ 0b1111) +} + +/** + * 序列化方式枚举(3位,存储在字节1高3位,1~8范围) + * 二进制标识,统一编码风格(1~8对应0b001~0b111) + */ +export enum SerializationType { + RAW = 0b001, // 原始二进制(1) + JSON = 0b010, // JSON 格式(2) + STRING = 0b011, // 直接字符串(3) + // 预留5种方式用于扩展(0b100 ~ 0b111) +} + +/** + * 压缩方式枚举(3位,存储在字节1中3位,1~8范围) + * 二进制标识,统一编码风格(1~8对应0b001~0b111) + */ +export enum CompressionType { + NONE = 0b001, // 无压缩(1,默认值) + GZIP = 0b010, // GZIP 压缩(2) + // 预留6种方式用于扩展(0b011 ~ 0b111) +} + +/** + * 控制指令枚举(配合 MessageType.CONTROL_CMD 使用) + */ +export enum ControlCommand { + HEARTBEAT = 0b0001, + PAUSE = 0b0010, + RESUME = 0b0011, + STOP = 0b0100, +} + +/** + * 解包返回结果接口 + */ +export interface UnpackedResult { + msgType : MessageType; + msgTypeName : keyof typeof MessageType; + serialization : SerializationType; + serializationName : keyof typeof SerializationType; + compression : CompressionType; + compressionName : keyof typeof CompressionType; + sequence : number; // 消息顺序号(0~65535,默认0) + body : Uint8Array | string | object | unknown[] | null; // PING 消息可能返回 null +} + +/** + * 打包入参类型别名(支持 PING 消息传入 null) + */ +type PackBody = Uint8Array | string | object | unknown[] | null; +type OptionalSerialization = SerializationType | null | undefined; + +/** + * 协议编解码工具类(支持 PING 消息空包体) + */ +export class ProtocolCodec { + /** + * 打包协议包 + * @param msgType 消息类型(二进制枚举,0~15) + * @param body 业务数据(PING 消息可传 null/undefined,其他类型必填) + * @param sequence 消息顺序号(0~65535,可选,默认0) + * @param serialization 序列化方式(二进制枚举,1~8,可选,自动推导) + * @param compression 压缩方式(二进制枚举,1~8,可选,默认NONE=0b001) + * @returns 完整协议包 + * @throws 类型错误、范围错误、包体过大等异常 + */ + static pack( + msgType : MessageType, + body : PackBody = null, // 默认为 null,支持 PING 消息空包体 + sequence : number = 0, // 可选参数,默认0 + serialization : OptionalSerialization = null, + compression : CompressionType = CompressionType.NONE + ) : Uint8Array { + // 校验顺序号范围(0~65535) + if (!Number.isInteger(sequence) || sequence < 0 || sequence > 0xFFFF) { + throw new RangeError(`消息顺序号必须是0~65535的整数,当前传入:${sequence}`); + } + + // 特殊处理:PING 消息允许空包体,强制 RAW 序列化(空二进制) + if (msgType === MessageType.PING) { + // PING 消息忽略传入的序列化方式,强制使用 RAW(空二进制最高效) + serialization = SerializationType.RAW; + // 若传入空包体,统一处理为空 Uint8Array + // body = body === null || body === undefined ? new Uint8Array(0) : body; + // PING 消息仅支持空包体或 Uint8Array(防止误传其他类型) + if (!(body instanceof Uint8Array)) { + throw new TypeError(`PING 消息仅支持空包体或 Uint8Array 类型,当前传入:${typeof body}`); + } + } else { + // 非 PING 消息:包体必填 + if (body === null || body === undefined) { + throw new TypeError(`非 PING 消息(${MessageType[msgType]})包体不能为空`); + } + } + + // 1. 自动推导序列化方式(非 PING 消息) + if (serialization === null || serialization === undefined && msgType !== MessageType.PING) { + if (msgType === MessageType.AUDIO_DATA) { + serialization = SerializationType.RAW; + } else if (msgType === MessageType.TEXT_MESSAGE) { + serialization = SerializationType.STRING; + } else if (msgType === MessageType.CONTROL_CMD) { + serialization = SerializationType.JSON; + } else if (msgType === MessageType.IDENTITY) { + serialization = SerializationType.JSON; + } else if (msgType === MessageType.ERROR) { + serialization = SerializationType.JSON; + } else { + throw new Error(`不支持的消息类型:${MessageType[msgType]}(值:${msgType})`); + } + } + + // 2. 校验枚举值范围(3位存储,1~8即0b001~0b111) + if (serialization < 0b001 || serialization > 0b111) { + throw new RangeError(`序列化方式必须在1~8(0b001~0b111)范围内,当前传入:${serialization}(0b${serialization.toString(2).padStart(3, '0')})`); + } + if (compression < 0b001 || compression > 0b111) { + throw new RangeError(`压缩方式必须在1~8(0b001~0b111)范围内,当前传入:${compression}(0b${compression.toString(2).padStart(3, '0')})`); + } + + // 3. 序列化包体 + let serializedBody : Uint8Array; + const textEncoder = new TextEncoder(); + + switch (serialization) { + case SerializationType.RAW: + // RAW 序列化:支持 Uint8Array(PING 消息可能是空 Uint8Array) + // if (!(body instanceof Uint8Array)) { + // throw new TypeError(`RAW 序列化要求 body 必须是 Uint8Array 类型,当前传入:${typeof body}`); + // } + serializedBody = body instanceof Uint8Array ? body : new Uint8Array(body as ArrayBuffer); + break; + + case SerializationType.STRING: + // STRING 序列化:必须传入字符串(非 PING 消息已校验非空) + serializedBody = textEncoder.encode(body as string); + break; + + case SerializationType.JSON: + // 断言:body 是 string 或 object + if (typeof body === 'string') { + serializedBody = textEncoder.encode(body); + } else { + serializedBody = textEncoder.encode(JSON.stringify(body)); + } + break; + + default: + throw new Error(`不支持的序列化方式:${SerializationType[serialization]}(值:${serialization},0b${serialization.toString(2).padStart(3, '0')})`); + } + + // 4. 压缩:暂不支持,直接赋值 + const compressedBody = serializedBody; + + // 5. 校验包体大小(24位长度最大支持0xFFFFFF=16777215字节) + const bodyLen = compressedBody.length; + if (bodyLen > ProtocolConst.MAX_BODY_SIZE) { + throw new Error( + `包体过大(${bodyLen}字节),最大支持${ProtocolConst.MAX_BODY_SIZE}字节(≈16MB)` + ); + } + + // 6. 构造头部(8字节,按最新结构) + const header = new Uint8Array(ProtocolConst.HEADER_SIZE); + + // 字节0:消息类型(高4位) + 协议版本(低4位) + header[0] = ((msgType & 0x0F) << 4) | (ProtocolConst.PROTOCOL_VERSION & 0x0F); + + // 字节1:序列化方式(高3位) + 压缩方式(中3位) + 保留位(低2位,填0) + header[1] = ((serialization & 0x07) << 5) | ((compression & 0x07) << 2) | 0x00; + + // 字节2~3:消息顺序号(16位大端序,0~65535) + header[2] = (sequence >> 8) & 0xFF; // 顺序号高8位 + header[3] = sequence & 0xFF; // 顺序号低8位 + + // 字节4~6:消息体长度(24位大端序,0~0xFFFFFF) + header[4] = (bodyLen >> 16) & 0xFF; // 长度高8位 + header[5] = (bodyLen >> 8) & 0xFF; // 长度中8位 + header[6] = bodyLen & 0xFF; // 长度低8位 + + // 字节7:保留位(固定填0x00) + header[7] = 0x00; + + // 7. 拼接头部和包体 + const totalLen = ProtocolConst.HEADER_SIZE + bodyLen; + const packet = new Uint8Array(totalLen); + packet.set(header, 0); + packet.set(compressedBody, ProtocolConst.HEADER_SIZE); + + return packet; + } + + /** + * 解包协议包 + * @param packet 完整协议包 + * @returns 结构化解包结果(含顺序号) + * @throws 各种解析异常 + */ + static unpack(packet : Uint8Array | ArrayBuffer) : UnpackedResult { + const uint8Packet = packet instanceof ArrayBuffer + ? new Uint8Array(packet) + : packet; + + // 1. 校验包长度(至少8字节头部) + if (uint8Packet.length < ProtocolConst.HEADER_SIZE) { + throw new Error( + `包长度过短(${uint8Packet.length}字节),至少需要${ProtocolConst.HEADER_SIZE}字节头部` + ); + } + + // 2. 拆分头部和包体 + const header = uint8Packet.subarray(0, ProtocolConst.HEADER_SIZE); + const bodyBuffer = uint8Packet.subarray(ProtocolConst.HEADER_SIZE); + + // 3. 解析头部字段 + // 字节0:消息类型(高4位) + 协议版本(低4位) + const byte0 = header[0]; + const msgType = (byte0 >> 4) & 0x0F; // 消息类型(0~15) + const version = byte0 & 0x0F; // 协议版本(0~15) + + // 校验消息类型 + if (!Object.values(MessageType).includes(msgType as MessageType)) { + throw new Error(`非法消息类型:${msgType}(0b${msgType.toString(2).padStart(4, '0')})`); + } + + // 校验版本 + if (version !== ProtocolConst.PROTOCOL_VERSION) { + throw new Error( + `协议版本不匹配:收到v${version}(0b${version.toString(2).padStart(4, '0')}),当前支持v${ProtocolConst.PROTOCOL_VERSION}(0b${ProtocolConst.PROTOCOL_VERSION.toString(2).padStart(4, '0')})` + ); + } + + // 字节1:序列化方式(高3位) + 压缩方式(中3位) + 保留位(低2位) + const byte1 = header[1]; + const serialization = (byte1 >> 5) & 0x07; // 高3位(1~8) + const compression = (byte1 >> 2) & 0x07; // 中3位(1~8) + // 保留位:(byte1 & 0x03),暂不处理 + + // 校验序列化方式 + if (!Object.values(SerializationType).includes(serialization as SerializationType)) { + throw new Error(`非法序列化方式:${serialization}(0b${serialization.toString(2).padStart(3, '0')})`); + } + + // 校验压缩方式 + if (!Object.values(CompressionType).includes(compression as CompressionType)) { + throw new Error(`非法压缩方式:${compression}(0b${compression.toString(2).padStart(3, '0')})`); + } + + // 字节2~3:消息顺序号(16位大端序) + const sequence = (header[2] << 8) | header[3]; // 0~65535 + + // 字节4~6:消息体长度(24位大端序),字节7:保留位(忽略) + const bodyLen = (header[4] << 16) | (header[5] << 8) | header[6]; + + // 校验包体长度(空包体时 bodyBuffer.length 应为0) + if (bodyBuffer.length !== bodyLen) { + throw new Error( + `包体长度不匹配:头部声明${bodyLen}字节,实际接收${bodyBuffer.length}字节` + ); + } + + // 4. 解压包体 + let decompressedBody : Uint8Array; + if (compression === CompressionType.NONE) { + decompressedBody = bodyBuffer; + } else if (compression === CompressionType.GZIP) { + // 若需启用GZIP,取消注释下方代码 + // try { + // decompressedBody = pako.ungzip(bodyBuffer); + // } catch (e) { + // throw new Error(`GZIP 解压失败:${(e as Error).message}`); + // } + throw new Error("GZIP 解压暂未启用,请导入pako库并取消对应代码注释"); + } else { + throw new Error(`不支持的压缩方式:${CompressionType[compression]}(值:${compression},0b${compression.toString(2).padStart(3, '0')})`); + } + + // 5. 反序列化包体(PING 消息空包体返回 null) + let body : UnpackedResult["body"]; + const textDecoder = new TextDecoder(); + + // 特殊处理:空包体(PING 消息常见)返回 null + if (decompressedBody.length === 0) { + body = null; + } else { + switch (serialization) { + case SerializationType.RAW: + body = decompressedBody; + break; + + case SerializationType.STRING: + try { + body = textDecoder.decode(decompressedBody); + } catch (e) { + throw new Error(`STRING 反序列化失败:UTF-8 解码错误`); + } + break; + + case SerializationType.JSON: + try { + const jsonStr = textDecoder.decode(decompressedBody); + body = JSON.parse(jsonStr); + } catch (e) { + if (e instanceof SyntaxError) { + throw new Error(`JSON 反序列化失败:格式错误(${(e as Error).message})`); + } else { + throw new Error(`JSON 反序列化失败:${(e as Error).message}`); + } + } + break; + + default: + throw new Error(`不支持的序列化方式:${SerializationType[serialization]}(值:${serialization},0b${serialization.toString(2).padStart(3, '0')})`); + } + } + + // 6. 返回解包结果 + return { + msgType: msgType as MessageType, + msgTypeName: MessageType[msgType] as keyof typeof MessageType, + serialization: serialization as SerializationType, + serializationName: SerializationType[serialization] as keyof typeof SerializationType, + compression: compression as CompressionType, + compressionName: CompressionType[compression] as keyof typeof CompressionType, + sequence: sequence, + body: body, + }; + } +} \ No newline at end of file diff --git a/tra-app/src/pages/test/index.vue b/tra-app/src/pages/test/index.vue new file mode 100644 index 0000000..85bfd02 --- /dev/null +++ b/tra-app/src/pages/test/index.vue @@ -0,0 +1,115 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/test/testChat.vue b/tra-app/src/pages/test/testChat.vue new file mode 100644 index 0000000..02d0a00 --- /dev/null +++ b/tra-app/src/pages/test/testChat.vue @@ -0,0 +1,236 @@ + + + + + \ No newline at end of file diff --git a/tra-app/src/pages/test/testChat2.vue b/tra-app/src/pages/test/testChat2.vue new file mode 100644 index 0000000..e250a7d --- /dev/null +++ b/tra-app/src/pages/test/testChat2.vue @@ -0,0 +1,147 @@ + + + + diff --git a/tra-app/src/pages/testingHall/components/content-item.vue b/tra-app/src/pages/testingHall/components/content-item.vue new file mode 100644 index 0000000..5032cf8 --- /dev/null +++ b/tra-app/src/pages/testingHall/components/content-item.vue @@ -0,0 +1,130 @@ + + + + + diff --git a/tra-app/src/pages/testingHall/components/content.vue b/tra-app/src/pages/testingHall/components/content.vue new file mode 100644 index 0000000..e90c230 --- /dev/null +++ b/tra-app/src/pages/testingHall/components/content.vue @@ -0,0 +1,115 @@ + + + + + diff --git a/tra-app/src/pages/testingHall/details.vue b/tra-app/src/pages/testingHall/details.vue new file mode 100644 index 0000000..8aca099 --- /dev/null +++ b/tra-app/src/pages/testingHall/details.vue @@ -0,0 +1,561 @@ + + + + + diff --git a/tra-app/src/pages/testingHall/index.vue b/tra-app/src/pages/testingHall/index.vue new file mode 100644 index 0000000..3917c15 --- /dev/null +++ b/tra-app/src/pages/testingHall/index.vue @@ -0,0 +1,180 @@ + + + + + diff --git a/tra-app/src/pages/traRecord/components/examRecordItem.vue b/tra-app/src/pages/traRecord/components/examRecordItem.vue new file mode 100644 index 0000000..800a52d --- /dev/null +++ b/tra-app/src/pages/traRecord/components/examRecordItem.vue @@ -0,0 +1,247 @@ + + + + + diff --git a/tra-app/src/pages/traRecord/components/examRecordList.vue b/tra-app/src/pages/traRecord/components/examRecordList.vue new file mode 100644 index 0000000..e021485 --- /dev/null +++ b/tra-app/src/pages/traRecord/components/examRecordList.vue @@ -0,0 +1,53 @@ + + + + diff --git a/tra-app/src/pages/traRecord/components/practiceRecordItem.vue b/tra-app/src/pages/traRecord/components/practiceRecordItem.vue new file mode 100644 index 0000000..6968227 --- /dev/null +++ b/tra-app/src/pages/traRecord/components/practiceRecordItem.vue @@ -0,0 +1,249 @@ + + + + + diff --git a/tra-app/src/pages/traRecord/components/practiceRecordList.vue b/tra-app/src/pages/traRecord/components/practiceRecordList.vue new file mode 100644 index 0000000..30bc563 --- /dev/null +++ b/tra-app/src/pages/traRecord/components/practiceRecordList.vue @@ -0,0 +1,53 @@ + + + + diff --git a/tra-app/src/pages/traRecord/index.vue b/tra-app/src/pages/traRecord/index.vue new file mode 100644 index 0000000..46388ad --- /dev/null +++ b/tra-app/src/pages/traRecord/index.vue @@ -0,0 +1,161 @@ + + + + + diff --git a/tra-app/src/pages/traRecord/useItemList.js b/tra-app/src/pages/traRecord/useItemList.js new file mode 100644 index 0000000..2ff3c9b --- /dev/null +++ b/tra-app/src/pages/traRecord/useItemList.js @@ -0,0 +1,112 @@ +import { + computed, + reactive, + nextTick, + ref +} from 'vue'; + + +// 计算两个标准时间的时间差值 +export const formatDuration = (startTm, endTm) => { + if (endTm === null || startTm === null) return "00:00:00"; + // 解析时间字符串为时间戳(毫秒) + const startTime = new Date(startTm).getTime(); + const endTime = new Date(endTm).getTime(); + + // 计算时间差(秒),确保为正数 + const seconds = Math.abs(Math.floor((endTime - startTime) / 1000)); + // 计算时、分、秒 + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const remainingSeconds = seconds % 60; + + // 补零格式化函数(统一处理所有单位) + const formatNumber = (num) => num.toString().padStart(2, '0'); + + // 小时、分钟、秒均保持两位数格式 + return `${formatNumber(hours)}:${formatNumber(minutes)}:${formatNumber(remainingSeconds)}`; +}; + +// 秒数转换成小时,带小数点的小时 +export const secondsToHours = (seconds) => { + if (typeof seconds === 'number') { + // 转换为小时并保留一位小数 + return (seconds / 3600).toFixed(1); + } + return '0.0'; +}; + +export function useItemList(fetchFunction, item) { + const status = ref('loadmore'); // loadmore - 加载前,loading - 加载中,nomore - 没有数据 + const limit = 5; + const page = ref(0); + const total = ref(-1); + const data_list = reactive([]); + const show_list_state = ref(false); + + const list_div_height = computed(() => { + const statusHeight = status.value === 'nomore' ? 0 : 100; + if (!show_list_state.value) { + return '0'; + } else if (status.value === 'loading') { + return data_list.length * 136 + statusHeight + 'rpx'; + } else { + return data_list.length * 136 + statusHeight + 'rpx'; + } + }); + // 点击获取详情 + const show_list_click = (num = -1) => { + if (num === 0) return; + show_list_state.value = !show_list_state.value; + if (show_list_state.value) { + nextTick(() => { + getData('first'); + }); + } + }; + + const getData = (from = '') => { + if (from === 'first') { + if (total.value !== -1) { + return; + } + status.value = 'loading'; + total.value = -1; + page.value = 1; + data_list.length = 0; + } else { + if (status.value !== 'loadmore' && status.value !== '') return; + status.value = 'loading'; + page.value++; + } + fetchFunction({ + page: page.value, + limit: limit + }) + .then((res) => { + total.value = res.total; + data_list.push(...res.body); + }) + .finally(() => { + if (data_list.length >= total.value) { + status.value = 'nomore'; + } else { + status.value = 'loadmore'; + } + }); + }; + const clear = () => { + total.value = -1 + data_list.length = 0 + show_list_state.value = false + } + return { + status, + data_list, + show_list_state, + list_div_height, + show_list_click, + getData, + clear + }; +} \ No newline at end of file diff --git a/tra-app/src/pages/welcome/index.vue b/tra-app/src/pages/welcome/index.vue new file mode 100644 index 0000000..5232204 --- /dev/null +++ b/tra-app/src/pages/welcome/index.vue @@ -0,0 +1,92 @@ + + + diff --git a/tra-app/src/pages/wrongQuestionRecord/components/correct.vue b/tra-app/src/pages/wrongQuestionRecord/components/correct.vue new file mode 100644 index 0000000..a2ae505 --- /dev/null +++ b/tra-app/src/pages/wrongQuestionRecord/components/correct.vue @@ -0,0 +1,77 @@ + + + + diff --git a/tra-app/src/pages/wrongQuestionRecord/components/favorites-course.vue b/tra-app/src/pages/wrongQuestionRecord/components/favorites-course.vue new file mode 100644 index 0000000..f5b1a62 --- /dev/null +++ b/tra-app/src/pages/wrongQuestionRecord/components/favorites-course.vue @@ -0,0 +1,92 @@ + + + + diff --git a/tra-app/src/pages/wrongQuestionRecord/correct.vue b/tra-app/src/pages/wrongQuestionRecord/correct.vue new file mode 100644 index 0000000..04e3882 --- /dev/null +++ b/tra-app/src/pages/wrongQuestionRecord/correct.vue @@ -0,0 +1,793 @@ + + + + + diff --git a/tra-app/src/pages/wrongQuestionRecord/index.vue b/tra-app/src/pages/wrongQuestionRecord/index.vue new file mode 100644 index 0000000..678b369 --- /dev/null +++ b/tra-app/src/pages/wrongQuestionRecord/index.vue @@ -0,0 +1,222 @@ + + + + + diff --git a/tra-app/src/pages/wrongQuestionRecord/result.vue b/tra-app/src/pages/wrongQuestionRecord/result.vue new file mode 100644 index 0000000..48f622a --- /dev/null +++ b/tra-app/src/pages/wrongQuestionRecord/result.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/tra-app/src/pagesA.json b/tra-app/src/pagesA.json new file mode 100644 index 0000000..552ff43 --- /dev/null +++ b/tra-app/src/pagesA.json @@ -0,0 +1,359 @@ +{ + "pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages + { + "path": "pages/study/index", + "style": { + "navigationBarTitleText": "学习", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/scene/index", + "style": { + "navigationBarTitleText": "场景", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/me/index", + "style": { + "navigationBarTitleText": "我的", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/login/login", + "style": { + "navigationBarTitleText": "登录", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/other/web_view", + "style": { + "navigationBarTitleText": "", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/login/ysxy", + "style": { + "navigationBarTitleText": "", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/search/search", + "style": { + "navigationBarTitleText": "", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/mascot/mascot_select_list", + "style": { + "navigationBarTitleText": "吉祥物列表", + "disableSwipeBack": true, + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/preview/index", + "style": { + "navigationBarTitleText": "", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/favorites/index", + "style": { + "navigationBarTitleText": "", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/messageNotification/index", + "style": { + "navigationBarTitleText": "消息", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/messageNotification/list", + "style": { + "navigationBarTitleText": "消息列表", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/testingHall/index", + "style": { + "navigationBarTitleText": "考试中心", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/testingHall/details", + "style": { + "navigationBarTitleText": "考试详情", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/wrongQuestionRecord/index", + "style": { + "navigationBarTitleText": "错题本", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/wrongQuestionRecord/correct", + "style": { + "navigationBarTitleText": "错题本做题页", + "disableSwipeBack": true, + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/examination/index", + "style": { + "navigationBarTitleText": "考试", + "disableSwipeBack": true, + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/examination/result", + "style": { + "navigationBarTitleText": "考试结果", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/practice/index", + "style": { + "navigationBarTitleText": "练习", + "disableSwipeBack": true, + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/learningTasks/index", + "style": { + "navigationBarTitleText": "学习任务", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/learningTasks/details", + "style": { + "navigationBarTitleText": "学习详情", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/examRanking/index", + "style": { + "navigationBarTitleText": "考试排行榜页", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/courseRecord/index", + "style": { + "navigationBarTitleText": "课程记录页", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/examinationRecord/index", + "style": { + "navigationBarTitleText": "考试记录页", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/courseRecord/courseStudyRecordDetail", + "style": { + "navigationBarTitleText": "课程记录学习记录详情", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/courseRecord/coursePracticeRecordDetail", + "style": { + "navigationBarTitleText": "课程记录练习记录详情", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/wrongQuestionRecord/result", + "style": { + "navigationBarTitleText": "答题本答题完成结果页", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/practice/result", + "style": { + "navigationBarTitleText": "练习完成结果页", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/competition/index", + "style": { + "navigationBarTitleText": "竞赛列表页", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/competition/matching", + "style": { + "navigationBarTitleText": "匹配页面", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/competition/pk", + "style": { + "navigationBarTitleText": "pk页面", + "disableSwipeBack": true, + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/competition/result", + "style": { + "navigationBarTitleText": "pk结果页面", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/competition/ranking", + "style": { + "navigationBarTitleText": "pk排行榜", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/pointsAndRank/points", + "style": { + "navigationBarTitleText": "积分变动明细", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/pointsAndRank/rank", + "style": { + "navigationBarTitleText": "段位列表", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/pointsAndRank/badge", + "style": { + "navigationBarTitleText": "徽章墙", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/competitionRecord/index", + "style": { + "navigationBarTitleText": "竞赛记录", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/pointsRedemption/index", + "style": { + "navigationBarTitleText": "积分兑换", + "app-plus": { + "titleNView": false + } + } + },{ + "path": "pages/test/testChat", + "style": { + "navigationBarTitleText": "", + "app-plus": { + "titleNView": false + } + } + } + ] +} \ No newline at end of file diff --git a/tra-app/src/pagesB.json b/tra-app/src/pagesB.json new file mode 100644 index 0000000..2b2f044 --- /dev/null +++ b/tra-app/src/pagesB.json @@ -0,0 +1,132 @@ +{ + "pages": [{ + "path": "pages/course/index", + "style": { + "navigationBarTitleText": "课程中心", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/course/study", + "style": { + "navigationBarTitleText": "课程学习", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/charts/index", + "style": { + "navigationBarTitleText": "", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/index/preview", + "style": { + "navigationBarTitleText": "", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/search/result", + "style": { + "navigationBarTitleText": "", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/example/dialog", + "style": { + "navigationBarTitleText": "AI问答演示", + "app-plus": { + "titleNView": false, + "animationType": "fade-in" + } + } + }, + { + "path": "pages/sparring/index", + "style": { + "navigationBarTitleText": "AI陪练", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/sparring/detail", + "style": { + "navigationBarTitleText": "AI陪练详情", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/sparring/tip", + "style": { + "navigationBarTitleText": "AI陪练提示", + "disableSwipeBack": true, + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/sparring/chooseRole", + "style": { + "navigationBarTitleText": "AI陪练选择角色", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/sparring/talk", + "style": { + "navigationBarTitleText": "AI陪练通话", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/sparring/result", + "style": { + "navigationBarTitleText": "AI陪练报告", + "disableSwipeBack": true, + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/analytics/study", + "style": { + "navigationBarTitleText": "学习分析", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/traRecord/index", + "style": { + "navigationBarTitleText": "陪练记录页", + "app-plus": { + "titleNView": false + } + } + }, + ] +} \ No newline at end of file diff --git a/tra-app/src/pagesOther.json b/tra-app/src/pagesOther.json new file mode 100644 index 0000000..3b5f6d2 --- /dev/null +++ b/tra-app/src/pagesOther.json @@ -0,0 +1,113 @@ +{ + "pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages + { + "path": "pages/index/dialog", + "style": { + "navigationBarTitleText": "AI问答", + "app-plus": { + "titleNView": false, + "animationType": "fade-in" + } + } + }, + { + "path": "pages/index/index", + "style": { + "navigationBarTitleText": "首页", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/courseDetail/index", + "style": { + "navigationBarTitleText": "课程详情", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/courseDetail/submit", + "style": { + "navigationBarTitleText": "提交评价", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/setting/index", + "style": { + "navigationBarTitleText": "设置", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/welcome/index", + "style": { + "navigationBarTitleText": "欢迎", + "app-plus": { + "titleNView": false + } + } + }, + { + "path": "pages/test/index", + "style": { + "navigationBarTitleText": "测试", + "app-plus": { + "titleNView": false + } + } + } + ], + "tabBar": { + "list": [{ + "text": "首页", + "pagePath": "pages/index/index" + }, + { + "text": "学习", + "pagePath": "pages/course/index" + }, + { + "text": "场景", + "pagePath": "pages/sparring/index" + }, + { + "text": "我的", + "pagePath": "pages/me/index" + } + ], + "color": "#666666", + "selectedColor": "#0066FF", + "fontSize": "16px", + "borderStyle": "white", + "borderColor": "#ffffff", + "height": "70px", + "spacing": "10px", + "midButton": { + "iconPath": "/static/images/icon/fawn-1.png", + "width": "86px", + "height": "86px", + "iconWidth": "90px" + } + }, + "globalStyle": { + "navigationBarTextStyle": "black", + "navigationBarTitleText": "AI智能培训", + "navigationBarBackgroundColor": "#F8F8F8", + "backgroundColor": "#F8F8F8", + "navigationStyle": "custom", + "app-plus": { + "titleView": false, + "bounce": "none", + "softinputNavBar": "none" + } + }, + "uniIdRouter": {} +} \ No newline at end of file diff --git a/tra-app/src/static/font/PingFang-SC.ttf b/tra-app/src/static/font/PingFang-SC.ttf new file mode 100644 index 0000000..b31eb2a Binary files /dev/null and b/tra-app/src/static/font/PingFang-SC.ttf differ diff --git a/tra-app/src/static/font/font_2225171_8kdcwk4po24.ttf b/tra-app/src/static/font/font_2225171_8kdcwk4po24.ttf new file mode 100644 index 0000000..befa24a Binary files /dev/null and b/tra-app/src/static/font/font_2225171_8kdcwk4po24.ttf differ diff --git a/tra-app/src/static/images/analytics/bg1.png b/tra-app/src/static/images/analytics/bg1.png new file mode 100644 index 0000000..c0b764e Binary files /dev/null and b/tra-app/src/static/images/analytics/bg1.png differ diff --git a/tra-app/src/static/images/analytics/bg2.png b/tra-app/src/static/images/analytics/bg2.png new file mode 100644 index 0000000..d27cd0b Binary files /dev/null and b/tra-app/src/static/images/analytics/bg2.png differ diff --git a/tra-app/src/static/images/analytics/course-1.png b/tra-app/src/static/images/analytics/course-1.png new file mode 100644 index 0000000..113a6c9 Binary files /dev/null and b/tra-app/src/static/images/analytics/course-1.png differ diff --git a/tra-app/src/static/images/analytics/course.png b/tra-app/src/static/images/analytics/course.png new file mode 100644 index 0000000..017f2bb Binary files /dev/null and b/tra-app/src/static/images/analytics/course.png differ diff --git a/tra-app/src/static/images/analytics/date-icon.png b/tra-app/src/static/images/analytics/date-icon.png new file mode 100644 index 0000000..181fd99 Binary files /dev/null and b/tra-app/src/static/images/analytics/date-icon.png differ diff --git a/tra-app/src/static/images/analytics/den-tips.png b/tra-app/src/static/images/analytics/den-tips.png new file mode 100644 index 0000000..c5e3e6e Binary files /dev/null and b/tra-app/src/static/images/analytics/den-tips.png differ diff --git a/tra-app/src/static/images/analytics/den-tips2.png b/tra-app/src/static/images/analytics/den-tips2.png new file mode 100644 index 0000000..0882cd7 Binary files /dev/null and b/tra-app/src/static/images/analytics/den-tips2.png differ diff --git a/tra-app/src/static/images/analytics/knowledge.png b/tra-app/src/static/images/analytics/knowledge.png new file mode 100644 index 0000000..d766b25 Binary files /dev/null and b/tra-app/src/static/images/analytics/knowledge.png differ diff --git a/tra-app/src/static/images/analytics/score.png b/tra-app/src/static/images/analytics/score.png new file mode 100644 index 0000000..3e58d31 Binary files /dev/null and b/tra-app/src/static/images/analytics/score.png differ diff --git a/tra-app/src/static/images/analytics/study-1.png b/tra-app/src/static/images/analytics/study-1.png new file mode 100644 index 0000000..133fa8b Binary files /dev/null and b/tra-app/src/static/images/analytics/study-1.png differ diff --git a/tra-app/src/static/images/analytics/study.png b/tra-app/src/static/images/analytics/study.png new file mode 100644 index 0000000..da127ff Binary files /dev/null and b/tra-app/src/static/images/analytics/study.png differ diff --git a/tra-app/src/static/images/analytics/study1-1.png b/tra-app/src/static/images/analytics/study1-1.png new file mode 100644 index 0000000..85fdfa7 Binary files /dev/null and b/tra-app/src/static/images/analytics/study1-1.png differ diff --git a/tra-app/src/static/images/analytics/study1.png b/tra-app/src/static/images/analytics/study1.png new file mode 100644 index 0000000..1ac7b7f Binary files /dev/null and b/tra-app/src/static/images/analytics/study1.png differ diff --git a/tra-app/src/static/images/analytics/time-icon.png b/tra-app/src/static/images/analytics/time-icon.png new file mode 100644 index 0000000..e36472f Binary files /dev/null and b/tra-app/src/static/images/analytics/time-icon.png differ diff --git a/tra-app/src/static/images/analytics/tips1.png b/tra-app/src/static/images/analytics/tips1.png new file mode 100644 index 0000000..16a4d47 Binary files /dev/null and b/tra-app/src/static/images/analytics/tips1.png differ diff --git a/tra-app/src/static/images/analytics/tips2.png b/tra-app/src/static/images/analytics/tips2.png new file mode 100644 index 0000000..8482412 Binary files /dev/null and b/tra-app/src/static/images/analytics/tips2.png differ diff --git a/tra-app/src/static/images/analytics/weidu-1.png b/tra-app/src/static/images/analytics/weidu-1.png new file mode 100644 index 0000000..8bb46af Binary files /dev/null and b/tra-app/src/static/images/analytics/weidu-1.png differ diff --git a/tra-app/src/static/images/analytics/weidu.png b/tra-app/src/static/images/analytics/weidu.png new file mode 100644 index 0000000..eb976b1 Binary files /dev/null and b/tra-app/src/static/images/analytics/weidu.png differ diff --git a/tra-app/src/static/images/charts/01.svg b/tra-app/src/static/images/charts/01.svg new file mode 100644 index 0000000..262985a --- /dev/null +++ b/tra-app/src/static/images/charts/01.svg @@ -0,0 +1,9 @@ + + + 01 + + + + + + \ No newline at end of file diff --git a/tra-app/src/static/images/charts/02.svg b/tra-app/src/static/images/charts/02.svg new file mode 100644 index 0000000..d8bf43d --- /dev/null +++ b/tra-app/src/static/images/charts/02.svg @@ -0,0 +1,9 @@ + + + 02 + + + + + + \ No newline at end of file diff --git a/tra-app/src/static/images/charts/03.svg b/tra-app/src/static/images/charts/03.svg new file mode 100644 index 0000000..47ab127 --- /dev/null +++ b/tra-app/src/static/images/charts/03.svg @@ -0,0 +1,9 @@ + + + 03 + + + + + + \ No newline at end of file diff --git a/tra-app/src/static/images/charts/avator.png b/tra-app/src/static/images/charts/avator.png new file mode 100644 index 0000000..03b34b9 Binary files /dev/null and b/tra-app/src/static/images/charts/avator.png differ diff --git a/tra-app/src/static/images/charts/bg-top.png b/tra-app/src/static/images/charts/bg-top.png new file mode 100644 index 0000000..0d96a40 Binary files /dev/null and b/tra-app/src/static/images/charts/bg-top.png differ diff --git a/tra-app/src/static/images/charts/cup.png b/tra-app/src/static/images/charts/cup.png new file mode 100644 index 0000000..899e20b Binary files /dev/null and b/tra-app/src/static/images/charts/cup.png differ diff --git a/tra-app/src/static/images/charts/text.png b/tra-app/src/static/images/charts/text.png new file mode 100644 index 0000000..f2cedbf Binary files /dev/null and b/tra-app/src/static/images/charts/text.png differ diff --git a/tra-app/src/static/images/common/default_img.png b/tra-app/src/static/images/common/default_img.png new file mode 100644 index 0000000..531f792 Binary files /dev/null and b/tra-app/src/static/images/common/default_img.png differ diff --git a/tra-app/src/static/images/common/default_img_2.png b/tra-app/src/static/images/common/default_img_2.png new file mode 100644 index 0000000..2f7feab Binary files /dev/null and b/tra-app/src/static/images/common/default_img_2.png differ diff --git a/tra-app/src/static/images/common/error.png b/tra-app/src/static/images/common/error.png new file mode 100644 index 0000000..2922e98 Binary files /dev/null and b/tra-app/src/static/images/common/error.png differ diff --git a/tra-app/src/static/images/common/loading.mp4 b/tra-app/src/static/images/common/loading.mp4 new file mode 100644 index 0000000..3a4ce6e Binary files /dev/null and b/tra-app/src/static/images/common/loading.mp4 differ diff --git a/tra-app/src/static/images/common/primary.png b/tra-app/src/static/images/common/primary.png new file mode 100644 index 0000000..ca49f01 Binary files /dev/null and b/tra-app/src/static/images/common/primary.png differ diff --git a/tra-app/src/static/images/common/success.png b/tra-app/src/static/images/common/success.png new file mode 100644 index 0000000..ccad708 Binary files /dev/null and b/tra-app/src/static/images/common/success.png differ diff --git a/tra-app/src/static/images/common/wc.png b/tra-app/src/static/images/common/wc.png new file mode 100644 index 0000000..ac46b7a Binary files /dev/null and b/tra-app/src/static/images/common/wc.png differ diff --git a/tra-app/src/static/images/competition/avatar_surround.png b/tra-app/src/static/images/competition/avatar_surround.png new file mode 100644 index 0000000..cd12d0b Binary files /dev/null and b/tra-app/src/static/images/competition/avatar_surround.png differ diff --git a/tra-app/src/static/images/competition/bg.png b/tra-app/src/static/images/competition/bg.png new file mode 100644 index 0000000..c8b35ff Binary files /dev/null and b/tra-app/src/static/images/competition/bg.png differ diff --git a/tra-app/src/static/images/competition/competition_default_avatar.png b/tra-app/src/static/images/competition/competition_default_avatar.png new file mode 100644 index 0000000..1660ff8 Binary files /dev/null and b/tra-app/src/static/images/competition/competition_default_avatar.png differ diff --git a/tra-app/src/static/images/competition/list_empty.png b/tra-app/src/static/images/competition/list_empty.png new file mode 100644 index 0000000..b197bdf Binary files /dev/null and b/tra-app/src/static/images/competition/list_empty.png differ diff --git a/tra-app/src/static/images/competition/matching-success-left.png b/tra-app/src/static/images/competition/matching-success-left.png new file mode 100644 index 0000000..451fbb7 Binary files /dev/null and b/tra-app/src/static/images/competition/matching-success-left.png differ diff --git a/tra-app/src/static/images/competition/matching-success-right.png b/tra-app/src/static/images/competition/matching-success-right.png new file mode 100644 index 0000000..68a5443 Binary files /dev/null and b/tra-app/src/static/images/competition/matching-success-right.png differ diff --git a/tra-app/src/static/images/competition/matching-success-text-2.png b/tra-app/src/static/images/competition/matching-success-text-2.png new file mode 100644 index 0000000..cc7602f Binary files /dev/null and b/tra-app/src/static/images/competition/matching-success-text-2.png differ diff --git a/tra-app/src/static/images/competition/matching-success-text.png b/tra-app/src/static/images/competition/matching-success-text.png new file mode 100644 index 0000000..648c702 Binary files /dev/null and b/tra-app/src/static/images/competition/matching-success-text.png differ diff --git a/tra-app/src/static/images/competition/pk_bg_icon.png b/tra-app/src/static/images/competition/pk_bg_icon.png new file mode 100644 index 0000000..e64e4ef Binary files /dev/null and b/tra-app/src/static/images/competition/pk_bg_icon.png differ diff --git a/tra-app/src/static/images/competition/ranking/1@2x.png b/tra-app/src/static/images/competition/ranking/1@2x.png new file mode 100644 index 0000000..fcfe338 Binary files /dev/null and b/tra-app/src/static/images/competition/ranking/1@2x.png differ diff --git a/tra-app/src/static/images/competition/ranking/2@2x.png b/tra-app/src/static/images/competition/ranking/2@2x.png new file mode 100644 index 0000000..6912627 Binary files /dev/null and b/tra-app/src/static/images/competition/ranking/2@2x.png differ diff --git a/tra-app/src/static/images/competition/ranking/3@2x.png b/tra-app/src/static/images/competition/ranking/3@2x.png new file mode 100644 index 0000000..abb2f86 Binary files /dev/null and b/tra-app/src/static/images/competition/ranking/3@2x.png differ diff --git a/tra-app/src/static/images/competition/ranking/ranking_bg.png b/tra-app/src/static/images/competition/ranking/ranking_bg.png new file mode 100644 index 0000000..a4e3634 Binary files /dev/null and b/tra-app/src/static/images/competition/ranking/ranking_bg.png differ diff --git a/tra-app/src/static/images/competition/ranking/ranking_title.png b/tra-app/src/static/images/competition/ranking/ranking_title.png new file mode 100644 index 0000000..e642692 Binary files /dev/null and b/tra-app/src/static/images/competition/ranking/ranking_title.png differ diff --git a/tra-app/src/static/images/competition/result_bg.png b/tra-app/src/static/images/competition/result_bg.png new file mode 100644 index 0000000..4181ef1 Binary files /dev/null and b/tra-app/src/static/images/competition/result_bg.png differ diff --git a/tra-app/src/static/images/competition/result_huosheng.png b/tra-app/src/static/images/competition/result_huosheng.png new file mode 100644 index 0000000..d1636c0 Binary files /dev/null and b/tra-app/src/static/images/competition/result_huosheng.png differ diff --git a/tra-app/src/static/images/competition/result_loading_mascot.png b/tra-app/src/static/images/competition/result_loading_mascot.png new file mode 100644 index 0000000..87d1c00 Binary files /dev/null and b/tra-app/src/static/images/competition/result_loading_mascot.png differ diff --git a/tra-app/src/static/images/competition/result_loading_msg.png b/tra-app/src/static/images/competition/result_loading_msg.png new file mode 100644 index 0000000..b49369d Binary files /dev/null and b/tra-app/src/static/images/competition/result_loading_msg.png differ diff --git a/tra-app/src/static/images/competition/result_pk_bg.png b/tra-app/src/static/images/competition/result_pk_bg.png new file mode 100644 index 0000000..d7b953d Binary files /dev/null and b/tra-app/src/static/images/competition/result_pk_bg.png differ diff --git a/tra-app/src/static/images/competition/result_shengli.png b/tra-app/src/static/images/competition/result_shengli.png new file mode 100644 index 0000000..d02a1f8 Binary files /dev/null and b/tra-app/src/static/images/competition/result_shengli.png differ diff --git a/tra-app/src/static/images/competition/result_shibai.png b/tra-app/src/static/images/competition/result_shibai.png new file mode 100644 index 0000000..c18d305 Binary files /dev/null and b/tra-app/src/static/images/competition/result_shibai.png differ diff --git a/tra-app/src/static/images/course/active-left.png b/tra-app/src/static/images/course/active-left.png new file mode 100644 index 0000000..0303785 Binary files /dev/null and b/tra-app/src/static/images/course/active-left.png differ diff --git a/tra-app/src/static/images/course/close-active.png b/tra-app/src/static/images/course/close-active.png new file mode 100644 index 0000000..f139f09 Binary files /dev/null and b/tra-app/src/static/images/course/close-active.png differ diff --git a/tra-app/src/static/images/course/close.png b/tra-app/src/static/images/course/close.png new file mode 100644 index 0000000..080473f Binary files /dev/null and b/tra-app/src/static/images/course/close.png differ diff --git a/tra-app/src/static/images/course/edit.png b/tra-app/src/static/images/course/edit.png new file mode 100644 index 0000000..9c008c2 Binary files /dev/null and b/tra-app/src/static/images/course/edit.png differ diff --git a/tra-app/src/static/images/course/filter.png b/tra-app/src/static/images/course/filter.png new file mode 100644 index 0000000..d339eb1 Binary files /dev/null and b/tra-app/src/static/images/course/filter.png differ diff --git a/tra-app/src/static/images/course/goon.png b/tra-app/src/static/images/course/goon.png new file mode 100644 index 0000000..034ad22 Binary files /dev/null and b/tra-app/src/static/images/course/goon.png differ diff --git a/tra-app/src/static/images/course/jianpan.png b/tra-app/src/static/images/course/jianpan.png new file mode 100644 index 0000000..0f6d8e0 Binary files /dev/null and b/tra-app/src/static/images/course/jianpan.png differ diff --git a/tra-app/src/static/images/course/pause-focus.png b/tra-app/src/static/images/course/pause-focus.png new file mode 100644 index 0000000..d8293c6 Binary files /dev/null and b/tra-app/src/static/images/course/pause-focus.png differ diff --git a/tra-app/src/static/images/course/pause.png b/tra-app/src/static/images/course/pause.png new file mode 100644 index 0000000..7bb8332 Binary files /dev/null and b/tra-app/src/static/images/course/pause.png differ diff --git a/tra-app/src/static/images/course/play-blue.png b/tra-app/src/static/images/course/play-blue.png new file mode 100644 index 0000000..7b7072d Binary files /dev/null and b/tra-app/src/static/images/course/play-blue.png differ diff --git a/tra-app/src/static/images/course/play.png b/tra-app/src/static/images/course/play.png new file mode 100644 index 0000000..cbe9ee8 Binary files /dev/null and b/tra-app/src/static/images/course/play.png differ diff --git a/tra-app/src/static/images/course/record.png b/tra-app/src/static/images/course/record.png new file mode 100644 index 0000000..17de76a Binary files /dev/null and b/tra-app/src/static/images/course/record.png differ diff --git a/tra-app/src/static/images/course/refresh-white.png b/tra-app/src/static/images/course/refresh-white.png new file mode 100644 index 0000000..554a24c Binary files /dev/null and b/tra-app/src/static/images/course/refresh-white.png differ diff --git a/tra-app/src/static/images/course/replay.png b/tra-app/src/static/images/course/replay.png new file mode 100644 index 0000000..a03d5a6 Binary files /dev/null and b/tra-app/src/static/images/course/replay.png differ diff --git a/tra-app/src/static/images/course/restart.png b/tra-app/src/static/images/course/restart.png new file mode 100644 index 0000000..c7c705b Binary files /dev/null and b/tra-app/src/static/images/course/restart.png differ diff --git a/tra-app/src/static/images/course/restart2.png b/tra-app/src/static/images/course/restart2.png new file mode 100644 index 0000000..926ff53 Binary files /dev/null and b/tra-app/src/static/images/course/restart2.png differ diff --git a/tra-app/src/static/images/course/restudy.png b/tra-app/src/static/images/course/restudy.png new file mode 100644 index 0000000..e8798d8 Binary files /dev/null and b/tra-app/src/static/images/course/restudy.png differ diff --git a/tra-app/src/static/images/course/score.png b/tra-app/src/static/images/course/score.png new file mode 100644 index 0000000..f647c09 Binary files /dev/null and b/tra-app/src/static/images/course/score.png differ diff --git a/tra-app/src/static/images/course/send.png b/tra-app/src/static/images/course/send.png new file mode 100644 index 0000000..33009dc Binary files /dev/null and b/tra-app/src/static/images/course/send.png differ diff --git a/tra-app/src/static/images/course/setting.png b/tra-app/src/static/images/course/setting.png new file mode 100644 index 0000000..bed6323 Binary files /dev/null and b/tra-app/src/static/images/course/setting.png differ diff --git a/tra-app/src/static/images/course/stop-blue.png b/tra-app/src/static/images/course/stop-blue.png new file mode 100644 index 0000000..43d88e9 Binary files /dev/null and b/tra-app/src/static/images/course/stop-blue.png differ diff --git a/tra-app/src/static/images/course/study-bg.png b/tra-app/src/static/images/course/study-bg.png new file mode 100644 index 0000000..89f4ce4 Binary files /dev/null and b/tra-app/src/static/images/course/study-bg.png differ diff --git a/tra-app/src/static/images/course/tag-item.png b/tra-app/src/static/images/course/tag-item.png new file mode 100644 index 0000000..1c89c30 Binary files /dev/null and b/tra-app/src/static/images/course/tag-item.png differ diff --git a/tra-app/src/static/images/course/talk-icon.png b/tra-app/src/static/images/course/talk-icon.png new file mode 100644 index 0000000..d91cd18 Binary files /dev/null and b/tra-app/src/static/images/course/talk-icon.png differ diff --git a/tra-app/src/static/images/course/talking.png b/tra-app/src/static/images/course/talking.png new file mode 100644 index 0000000..930e393 Binary files /dev/null and b/tra-app/src/static/images/course/talking.png differ diff --git a/tra-app/src/static/images/course/to-text.png b/tra-app/src/static/images/course/to-text.png new file mode 100644 index 0000000..a8c799e Binary files /dev/null and b/tra-app/src/static/images/course/to-text.png differ diff --git a/tra-app/src/static/images/course/voice-gif.gif b/tra-app/src/static/images/course/voice-gif.gif new file mode 100644 index 0000000..2a90518 Binary files /dev/null and b/tra-app/src/static/images/course/voice-gif.gif differ diff --git a/tra-app/src/static/images/course/voice-gray.png b/tra-app/src/static/images/course/voice-gray.png new file mode 100644 index 0000000..ecac74a Binary files /dev/null and b/tra-app/src/static/images/course/voice-gray.png differ diff --git a/tra-app/src/static/images/course/voice-white.gif b/tra-app/src/static/images/course/voice-white.gif new file mode 100644 index 0000000..6010ae1 Binary files /dev/null and b/tra-app/src/static/images/course/voice-white.gif differ diff --git a/tra-app/src/static/images/courseDetail/ASK.png b/tra-app/src/static/images/courseDetail/ASK.png new file mode 100644 index 0000000..3852178 Binary files /dev/null and b/tra-app/src/static/images/courseDetail/ASK.png differ diff --git a/tra-app/src/static/images/courseDetail/SUG.png b/tra-app/src/static/images/courseDetail/SUG.png new file mode 100644 index 0000000..6ecb5b0 Binary files /dev/null and b/tra-app/src/static/images/courseDetail/SUG.png differ diff --git a/tra-app/src/static/images/courseDetail/charts.png b/tra-app/src/static/images/courseDetail/charts.png new file mode 100644 index 0000000..719c3d1 Binary files /dev/null and b/tra-app/src/static/images/courseDetail/charts.png differ diff --git a/tra-app/src/static/images/courseDetail/ditu.png b/tra-app/src/static/images/courseDetail/ditu.png new file mode 100644 index 0000000..d3bb3b7 Binary files /dev/null and b/tra-app/src/static/images/courseDetail/ditu.png differ diff --git a/tra-app/src/static/images/courseDetail/ditu2.png b/tra-app/src/static/images/courseDetail/ditu2.png new file mode 100644 index 0000000..ec6af6e Binary files /dev/null and b/tra-app/src/static/images/courseDetail/ditu2.png differ diff --git a/tra-app/src/static/images/courseDetail/empty_message.png b/tra-app/src/static/images/courseDetail/empty_message.png new file mode 100644 index 0000000..6d638e4 Binary files /dev/null and b/tra-app/src/static/images/courseDetail/empty_message.png differ diff --git a/tra-app/src/static/images/courseDetail/in_collect.png b/tra-app/src/static/images/courseDetail/in_collect.png new file mode 100644 index 0000000..8483396 Binary files /dev/null and b/tra-app/src/static/images/courseDetail/in_collect.png differ diff --git a/tra-app/src/static/images/courseDetail/out_collect.png b/tra-app/src/static/images/courseDetail/out_collect.png new file mode 100644 index 0000000..29c5a53 Binary files /dev/null and b/tra-app/src/static/images/courseDetail/out_collect.png differ diff --git a/tra-app/src/static/images/courseDetail/yixue.png b/tra-app/src/static/images/courseDetail/yixue.png new file mode 100644 index 0000000..ce4481f Binary files /dev/null and b/tra-app/src/static/images/courseDetail/yixue.png differ diff --git a/tra-app/src/static/images/courseDetail/zhishidian.png b/tra-app/src/static/images/courseDetail/zhishidian.png new file mode 100644 index 0000000..b85c673 Binary files /dev/null and b/tra-app/src/static/images/courseDetail/zhishidian.png differ diff --git a/tra-app/src/static/images/courseRecord/duration.png b/tra-app/src/static/images/courseRecord/duration.png new file mode 100644 index 0000000..a4febe9 Binary files /dev/null and b/tra-app/src/static/images/courseRecord/duration.png differ diff --git a/tra-app/src/static/images/courseRecord/learn.png b/tra-app/src/static/images/courseRecord/learn.png new file mode 100644 index 0000000..233ba2b Binary files /dev/null and b/tra-app/src/static/images/courseRecord/learn.png differ diff --git a/tra-app/src/static/images/courseRecord/time.png b/tra-app/src/static/images/courseRecord/time.png new file mode 100644 index 0000000..3b26002 Binary files /dev/null and b/tra-app/src/static/images/courseRecord/time.png differ diff --git a/tra-app/src/static/images/dialog/answer-fankui.png b/tra-app/src/static/images/dialog/answer-fankui.png new file mode 100644 index 0000000..e125e58 Binary files /dev/null and b/tra-app/src/static/images/dialog/answer-fankui.png differ diff --git a/tra-app/src/static/images/dialog/answer-fankui1.png b/tra-app/src/static/images/dialog/answer-fankui1.png new file mode 100644 index 0000000..1e27569 Binary files /dev/null and b/tra-app/src/static/images/dialog/answer-fankui1.png differ diff --git a/tra-app/src/static/images/dialog/answer-restart.png b/tra-app/src/static/images/dialog/answer-restart.png new file mode 100644 index 0000000..0816704 Binary files /dev/null and b/tra-app/src/static/images/dialog/answer-restart.png differ diff --git a/tra-app/src/static/images/dialog/answer-restart1.png b/tra-app/src/static/images/dialog/answer-restart1.png new file mode 100644 index 0000000..f60fb78 Binary files /dev/null and b/tra-app/src/static/images/dialog/answer-restart1.png differ diff --git a/tra-app/src/static/images/dialog/answer-stop.png b/tra-app/src/static/images/dialog/answer-stop.png new file mode 100644 index 0000000..14aa0ea Binary files /dev/null and b/tra-app/src/static/images/dialog/answer-stop.png differ diff --git a/tra-app/src/static/images/dialog/answer-stop1.png b/tra-app/src/static/images/dialog/answer-stop1.png new file mode 100644 index 0000000..7437a7b Binary files /dev/null and b/tra-app/src/static/images/dialog/answer-stop1.png differ diff --git a/tra-app/src/static/images/dialog/answer-yifankui.png b/tra-app/src/static/images/dialog/answer-yifankui.png new file mode 100644 index 0000000..66ae2b5 Binary files /dev/null and b/tra-app/src/static/images/dialog/answer-yifankui.png differ diff --git a/tra-app/src/static/images/dialog/answer-yifankui1.png b/tra-app/src/static/images/dialog/answer-yifankui1.png new file mode 100644 index 0000000..84f45c6 Binary files /dev/null and b/tra-app/src/static/images/dialog/answer-yifankui1.png differ diff --git a/tra-app/src/static/images/dialog/answer-yizan.png b/tra-app/src/static/images/dialog/answer-yizan.png new file mode 100644 index 0000000..c30d48b Binary files /dev/null and b/tra-app/src/static/images/dialog/answer-yizan.png differ diff --git a/tra-app/src/static/images/dialog/answer-yizan1.png b/tra-app/src/static/images/dialog/answer-yizan1.png new file mode 100644 index 0000000..4a4afb4 Binary files /dev/null and b/tra-app/src/static/images/dialog/answer-yizan1.png differ diff --git a/tra-app/src/static/images/dialog/answer-zan.png b/tra-app/src/static/images/dialog/answer-zan.png new file mode 100644 index 0000000..6e5d97a Binary files /dev/null and b/tra-app/src/static/images/dialog/answer-zan.png differ diff --git a/tra-app/src/static/images/dialog/answer-zan1.png b/tra-app/src/static/images/dialog/answer-zan1.png new file mode 100644 index 0000000..c40ea0b Binary files /dev/null and b/tra-app/src/static/images/dialog/answer-zan1.png differ diff --git a/tra-app/src/static/images/dialog/history-icon.png b/tra-app/src/static/images/dialog/history-icon.png new file mode 100644 index 0000000..6b0d333 Binary files /dev/null and b/tra-app/src/static/images/dialog/history-icon.png differ diff --git a/tra-app/src/static/images/dialog/history.png b/tra-app/src/static/images/dialog/history.png new file mode 100644 index 0000000..03bd6da Binary files /dev/null and b/tra-app/src/static/images/dialog/history.png differ diff --git a/tra-app/src/static/images/dialog/icons/PK.png b/tra-app/src/static/images/dialog/icons/PK.png new file mode 100644 index 0000000..9ce27d7 Binary files /dev/null and b/tra-app/src/static/images/dialog/icons/PK.png differ diff --git a/tra-app/src/static/images/dialog/icons/exam.png b/tra-app/src/static/images/dialog/icons/exam.png new file mode 100644 index 0000000..7797aa8 Binary files /dev/null and b/tra-app/src/static/images/dialog/icons/exam.png differ diff --git a/tra-app/src/static/images/dialog/icons/static.png b/tra-app/src/static/images/dialog/icons/static.png new file mode 100644 index 0000000..120647d Binary files /dev/null and b/tra-app/src/static/images/dialog/icons/static.png differ diff --git a/tra-app/src/static/images/dialog/icons/study.png b/tra-app/src/static/images/dialog/icons/study.png new file mode 100644 index 0000000..2f90dc9 Binary files /dev/null and b/tra-app/src/static/images/dialog/icons/study.png differ diff --git a/tra-app/src/static/images/dialog/icons/task.png b/tra-app/src/static/images/dialog/icons/task.png new file mode 100644 index 0000000..bfb015b Binary files /dev/null and b/tra-app/src/static/images/dialog/icons/task.png differ diff --git a/tra-app/src/static/images/dialog/icons/test.png b/tra-app/src/static/images/dialog/icons/test.png new file mode 100644 index 0000000..2cc2e3e Binary files /dev/null and b/tra-app/src/static/images/dialog/icons/test.png differ diff --git a/tra-app/src/static/images/dialog/icons/wrong.png b/tra-app/src/static/images/dialog/icons/wrong.png new file mode 100644 index 0000000..3d7496c Binary files /dev/null and b/tra-app/src/static/images/dialog/icons/wrong.png differ diff --git a/tra-app/src/static/images/dialog/new-dialog.png b/tra-app/src/static/images/dialog/new-dialog.png new file mode 100644 index 0000000..d4ba2e8 Binary files /dev/null and b/tra-app/src/static/images/dialog/new-dialog.png differ diff --git a/tra-app/src/static/images/dialog/pet-bg-base.png b/tra-app/src/static/images/dialog/pet-bg-base.png new file mode 100644 index 0000000..3512f92 Binary files /dev/null and b/tra-app/src/static/images/dialog/pet-bg-base.png differ diff --git a/tra-app/src/static/images/dialog/pet-bg.png b/tra-app/src/static/images/dialog/pet-bg.png new file mode 100644 index 0000000..40725df Binary files /dev/null and b/tra-app/src/static/images/dialog/pet-bg.png differ diff --git a/tra-app/src/static/images/examRanking/bg.png b/tra-app/src/static/images/examRanking/bg.png new file mode 100644 index 0000000..a6e313c Binary files /dev/null and b/tra-app/src/static/images/examRanking/bg.png differ diff --git a/tra-app/src/static/images/examRanking/cup.png b/tra-app/src/static/images/examRanking/cup.png new file mode 100644 index 0000000..acf36b8 Binary files /dev/null and b/tra-app/src/static/images/examRanking/cup.png differ diff --git a/tra-app/src/static/images/examRanking/no-1.png b/tra-app/src/static/images/examRanking/no-1.png new file mode 100644 index 0000000..9896fd5 Binary files /dev/null and b/tra-app/src/static/images/examRanking/no-1.png differ diff --git a/tra-app/src/static/images/examRanking/no-2.png b/tra-app/src/static/images/examRanking/no-2.png new file mode 100644 index 0000000..711edf2 Binary files /dev/null and b/tra-app/src/static/images/examRanking/no-2.png differ diff --git a/tra-app/src/static/images/examRanking/no-3.png b/tra-app/src/static/images/examRanking/no-3.png new file mode 100644 index 0000000..11871aa Binary files /dev/null and b/tra-app/src/static/images/examRanking/no-3.png differ diff --git a/tra-app/src/static/images/examRanking/no1.png b/tra-app/src/static/images/examRanking/no1.png new file mode 100644 index 0000000..3e6e3f5 Binary files /dev/null and b/tra-app/src/static/images/examRanking/no1.png differ diff --git a/tra-app/src/static/images/examRanking/no2.png b/tra-app/src/static/images/examRanking/no2.png new file mode 100644 index 0000000..d97f0d0 Binary files /dev/null and b/tra-app/src/static/images/examRanking/no2.png differ diff --git a/tra-app/src/static/images/examRanking/no3.png b/tra-app/src/static/images/examRanking/no3.png new file mode 100644 index 0000000..d3fa949 Binary files /dev/null and b/tra-app/src/static/images/examRanking/no3.png differ diff --git a/tra-app/src/static/images/examRanking/title.png b/tra-app/src/static/images/examRanking/title.png new file mode 100644 index 0000000..0f70b9b Binary files /dev/null and b/tra-app/src/static/images/examRanking/title.png differ diff --git a/tra-app/src/static/images/examination/bg2@2x.png b/tra-app/src/static/images/examination/bg2@2x.png new file mode 100644 index 0000000..b237623 Binary files /dev/null and b/tra-app/src/static/images/examination/bg2@2x.png differ diff --git a/tra-app/src/static/images/examination/duration-icon.png b/tra-app/src/static/images/examination/duration-icon.png new file mode 100644 index 0000000..464f0bd Binary files /dev/null and b/tra-app/src/static/images/examination/duration-icon.png differ diff --git a/tra-app/src/static/images/examination/examination-icon.png b/tra-app/src/static/images/examination/examination-icon.png new file mode 100644 index 0000000..2b93e37 Binary files /dev/null and b/tra-app/src/static/images/examination/examination-icon.png differ diff --git a/tra-app/src/static/images/examination/getting_in.gif b/tra-app/src/static/images/examination/getting_in.gif new file mode 100644 index 0000000..5937ceb Binary files /dev/null and b/tra-app/src/static/images/examination/getting_in.gif differ diff --git a/tra-app/src/static/images/examination/getting_robot.png b/tra-app/src/static/images/examination/getting_robot.png new file mode 100644 index 0000000..4404c93 Binary files /dev/null and b/tra-app/src/static/images/examination/getting_robot.png differ diff --git a/tra-app/src/static/images/examination/qualified.png b/tra-app/src/static/images/examination/qualified.png new file mode 100644 index 0000000..dfb3e5a Binary files /dev/null and b/tra-app/src/static/images/examination/qualified.png differ diff --git a/tra-app/src/static/images/examination/sheet.png b/tra-app/src/static/images/examination/sheet.png new file mode 100644 index 0000000..04168bb Binary files /dev/null and b/tra-app/src/static/images/examination/sheet.png differ diff --git a/tra-app/src/static/images/examination/unable_img.png b/tra-app/src/static/images/examination/unable_img.png new file mode 100644 index 0000000..9915085 Binary files /dev/null and b/tra-app/src/static/images/examination/unable_img.png differ diff --git a/tra-app/src/static/images/examination/unqualified.png b/tra-app/src/static/images/examination/unqualified.png new file mode 100644 index 0000000..61f3315 Binary files /dev/null and b/tra-app/src/static/images/examination/unqualified.png differ diff --git a/tra-app/src/static/images/icon/fawn-1.png b/tra-app/src/static/images/icon/fawn-1.png new file mode 100644 index 0000000..a6f2770 Binary files /dev/null and b/tra-app/src/static/images/icon/fawn-1.png differ diff --git a/tra-app/src/static/images/icon/fawn-2.png b/tra-app/src/static/images/icon/fawn-2.png new file mode 100644 index 0000000..b897c2a Binary files /dev/null and b/tra-app/src/static/images/icon/fawn-2.png differ diff --git a/tra-app/src/static/images/icon/fawn-3.png b/tra-app/src/static/images/icon/fawn-3.png new file mode 100644 index 0000000..8ab3d34 Binary files /dev/null and b/tra-app/src/static/images/icon/fawn-3.png differ diff --git a/tra-app/src/static/images/icon/fawn-4.png b/tra-app/src/static/images/icon/fawn-4.png new file mode 100644 index 0000000..6c2d27b Binary files /dev/null and b/tra-app/src/static/images/icon/fawn-4.png differ diff --git a/tra-app/src/static/images/icon/fawn-5.png b/tra-app/src/static/images/icon/fawn-5.png new file mode 100644 index 0000000..1b92c79 Binary files /dev/null and b/tra-app/src/static/images/icon/fawn-5.png differ diff --git a/tra-app/src/static/images/icon/fawn-x.png b/tra-app/src/static/images/icon/fawn-x.png new file mode 100644 index 0000000..9ce669a Binary files /dev/null and b/tra-app/src/static/images/icon/fawn-x.png differ diff --git a/tra-app/src/static/images/index/bg.png b/tra-app/src/static/images/index/bg.png new file mode 100644 index 0000000..5bacd22 Binary files /dev/null and b/tra-app/src/static/images/index/bg.png differ diff --git a/tra-app/src/static/images/index/fttz.png b/tra-app/src/static/images/index/fttz.png new file mode 100644 index 0000000..dd38865 Binary files /dev/null and b/tra-app/src/static/images/index/fttz.png differ diff --git a/tra-app/src/static/images/index/hot.png b/tra-app/src/static/images/index/hot.png new file mode 100644 index 0000000..96790e7 Binary files /dev/null and b/tra-app/src/static/images/index/hot.png differ diff --git a/tra-app/src/static/images/index/logo_text.png b/tra-app/src/static/images/index/logo_text.png new file mode 100644 index 0000000..6bbba70 Binary files /dev/null and b/tra-app/src/static/images/index/logo_text.png differ diff --git a/tra-app/src/static/images/index/message.png b/tra-app/src/static/images/index/message.png new file mode 100644 index 0000000..c9a5fb3 Binary files /dev/null and b/tra-app/src/static/images/index/message.png differ diff --git a/tra-app/src/static/images/index/message_b.png b/tra-app/src/static/images/index/message_b.png new file mode 100644 index 0000000..7e10471 Binary files /dev/null and b/tra-app/src/static/images/index/message_b.png differ diff --git a/tra-app/src/static/images/index/navigation_ai.png b/tra-app/src/static/images/index/navigation_ai.png new file mode 100644 index 0000000..2b58abd Binary files /dev/null and b/tra-app/src/static/images/index/navigation_ai.png differ diff --git a/tra-app/src/static/images/index/navigation_course.png b/tra-app/src/static/images/index/navigation_course.png new file mode 100644 index 0000000..e8fabdd Binary files /dev/null and b/tra-app/src/static/images/index/navigation_course.png differ diff --git a/tra-app/src/static/images/index/navigation_examination.png b/tra-app/src/static/images/index/navigation_examination.png new file mode 100644 index 0000000..dab9709 Binary files /dev/null and b/tra-app/src/static/images/index/navigation_examination.png differ diff --git a/tra-app/src/static/images/index/navigation_learning_analysis.png b/tra-app/src/static/images/index/navigation_learning_analysis.png new file mode 100644 index 0000000..675a497 Binary files /dev/null and b/tra-app/src/static/images/index/navigation_learning_analysis.png differ diff --git a/tra-app/src/static/images/index/navigation_learning_tasks.png b/tra-app/src/static/images/index/navigation_learning_tasks.png new file mode 100644 index 0000000..67170b6 Binary files /dev/null and b/tra-app/src/static/images/index/navigation_learning_tasks.png differ diff --git a/tra-app/src/static/images/index/navigation_practice.png b/tra-app/src/static/images/index/navigation_practice.png new file mode 100644 index 0000000..470393e Binary files /dev/null and b/tra-app/src/static/images/index/navigation_practice.png differ diff --git a/tra-app/src/static/images/index/navigation_question_answering.png b/tra-app/src/static/images/index/navigation_question_answering.png new file mode 100644 index 0000000..d6b47d7 Binary files /dev/null and b/tra-app/src/static/images/index/navigation_question_answering.png differ diff --git a/tra-app/src/static/images/index/navigation_wrong_question.png b/tra-app/src/static/images/index/navigation_wrong_question.png new file mode 100644 index 0000000..4d8b776 Binary files /dev/null and b/tra-app/src/static/images/index/navigation_wrong_question.png differ diff --git a/tra-app/src/static/images/index/new.png b/tra-app/src/static/images/index/new.png new file mode 100644 index 0000000..fa389af Binary files /dev/null and b/tra-app/src/static/images/index/new.png differ diff --git a/tra-app/src/static/images/index/recommend.png b/tra-app/src/static/images/index/recommend.png new file mode 100644 index 0000000..22a8c9d Binary files /dev/null and b/tra-app/src/static/images/index/recommend.png differ diff --git a/tra-app/src/static/images/index/search.png b/tra-app/src/static/images/index/search.png new file mode 100644 index 0000000..208799a Binary files /dev/null and b/tra-app/src/static/images/index/search.png differ diff --git a/tra-app/src/static/images/learningTasks/learningTasksBg.png b/tra-app/src/static/images/learningTasks/learningTasksBg.png new file mode 100644 index 0000000..0b47794 Binary files /dev/null and b/tra-app/src/static/images/learningTasks/learningTasksBg.png differ diff --git a/tra-app/src/static/images/login/bg.png b/tra-app/src/static/images/login/bg.png new file mode 100644 index 0000000..e97d90e Binary files /dev/null and b/tra-app/src/static/images/login/bg.png differ diff --git a/tra-app/src/static/images/login/clap.png b/tra-app/src/static/images/login/clap.png new file mode 100644 index 0000000..e569395 Binary files /dev/null and b/tra-app/src/static/images/login/clap.png differ diff --git a/tra-app/src/static/images/login/logo_text.png b/tra-app/src/static/images/login/logo_text.png new file mode 100644 index 0000000..64711b4 Binary files /dev/null and b/tra-app/src/static/images/login/logo_text.png differ diff --git a/tra-app/src/static/images/login/no_show.png b/tra-app/src/static/images/login/no_show.png new file mode 100644 index 0000000..4598c7d Binary files /dev/null and b/tra-app/src/static/images/login/no_show.png differ diff --git a/tra-app/src/static/images/login/profile.png b/tra-app/src/static/images/login/profile.png new file mode 100644 index 0000000..897744b Binary files /dev/null and b/tra-app/src/static/images/login/profile.png differ diff --git a/tra-app/src/static/images/login/show.png b/tra-app/src/static/images/login/show.png new file mode 100644 index 0000000..33a24bf Binary files /dev/null and b/tra-app/src/static/images/login/show.png differ diff --git a/tra-app/src/static/images/mascot/bg_1.png b/tra-app/src/static/images/mascot/bg_1.png new file mode 100644 index 0000000..cd84f10 Binary files /dev/null and b/tra-app/src/static/images/mascot/bg_1.png differ diff --git a/tra-app/src/static/images/mascot/btn_1.png b/tra-app/src/static/images/mascot/btn_1.png new file mode 100644 index 0000000..f69f686 Binary files /dev/null and b/tra-app/src/static/images/mascot/btn_1.png differ diff --git a/tra-app/src/static/images/mascot/btn_2.png b/tra-app/src/static/images/mascot/btn_2.png new file mode 100644 index 0000000..4ff2f46 Binary files /dev/null and b/tra-app/src/static/images/mascot/btn_2.png differ diff --git a/tra-app/src/static/images/mascot/btn_left.png b/tra-app/src/static/images/mascot/btn_left.png new file mode 100644 index 0000000..cd15372 Binary files /dev/null and b/tra-app/src/static/images/mascot/btn_left.png differ diff --git a/tra-app/src/static/images/mascot/btn_right.png b/tra-app/src/static/images/mascot/btn_right.png new file mode 100644 index 0000000..4c2cd7b Binary files /dev/null and b/tra-app/src/static/images/mascot/btn_right.png differ diff --git a/tra-app/src/static/images/mascot/ma_1.png b/tra-app/src/static/images/mascot/ma_1.png new file mode 100644 index 0000000..2e639a2 Binary files /dev/null and b/tra-app/src/static/images/mascot/ma_1.png differ diff --git a/tra-app/src/static/images/mascot/ma_2.png b/tra-app/src/static/images/mascot/ma_2.png new file mode 100644 index 0000000..54b5d73 Binary files /dev/null and b/tra-app/src/static/images/mascot/ma_2.png differ diff --git a/tra-app/src/static/images/mascot/ma_3.png b/tra-app/src/static/images/mascot/ma_3.png new file mode 100644 index 0000000..93b76a9 Binary files /dev/null and b/tra-app/src/static/images/mascot/ma_3.png differ diff --git a/tra-app/src/static/images/mascot/ma_4.png b/tra-app/src/static/images/mascot/ma_4.png new file mode 100644 index 0000000..375ad2d Binary files /dev/null and b/tra-app/src/static/images/mascot/ma_4.png differ diff --git a/tra-app/src/static/images/mascot/title.png b/tra-app/src/static/images/mascot/title.png new file mode 100644 index 0000000..0ab4532 Binary files /dev/null and b/tra-app/src/static/images/mascot/title.png differ diff --git a/tra-app/src/static/images/me/bg.png b/tra-app/src/static/images/me/bg.png new file mode 100644 index 0000000..8d90136 Binary files /dev/null and b/tra-app/src/static/images/me/bg.png differ diff --git a/tra-app/src/static/images/me/default_rank.png b/tra-app/src/static/images/me/default_rank.png new file mode 100644 index 0000000..bb0992e Binary files /dev/null and b/tra-app/src/static/images/me/default_rank.png differ diff --git a/tra-app/src/static/images/me/default_user_avatar.png b/tra-app/src/static/images/me/default_user_avatar.png new file mode 100644 index 0000000..f7ff365 Binary files /dev/null and b/tra-app/src/static/images/me/default_user_avatar.png differ diff --git a/tra-app/src/static/images/me/dot.png b/tra-app/src/static/images/me/dot.png new file mode 100644 index 0000000..ea1eeb3 Binary files /dev/null and b/tra-app/src/static/images/me/dot.png differ diff --git a/tra-app/src/static/images/me/examination.png b/tra-app/src/static/images/me/examination.png new file mode 100644 index 0000000..40ec3bd Binary files /dev/null and b/tra-app/src/static/images/me/examination.png differ diff --git a/tra-app/src/static/images/me/man.png b/tra-app/src/static/images/me/man.png new file mode 100644 index 0000000..2417f19 Binary files /dev/null and b/tra-app/src/static/images/me/man.png differ diff --git a/tra-app/src/static/images/me/points.png b/tra-app/src/static/images/me/points.png new file mode 100644 index 0000000..9b2698c Binary files /dev/null and b/tra-app/src/static/images/me/points.png differ diff --git a/tra-app/src/static/images/me/report.png b/tra-app/src/static/images/me/report.png new file mode 100644 index 0000000..a35d1ab Binary files /dev/null and b/tra-app/src/static/images/me/report.png differ diff --git a/tra-app/src/static/images/me/sign-in-button-bg.png b/tra-app/src/static/images/me/sign-in-button-bg.png new file mode 100644 index 0000000..736d22e Binary files /dev/null and b/tra-app/src/static/images/me/sign-in-button-bg.png differ diff --git a/tra-app/src/static/images/me/sign-in-button-bg2.png b/tra-app/src/static/images/me/sign-in-button-bg2.png new file mode 100644 index 0000000..555239e Binary files /dev/null and b/tra-app/src/static/images/me/sign-in-button-bg2.png differ diff --git a/tra-app/src/static/images/me/study.png b/tra-app/src/static/images/me/study.png new file mode 100644 index 0000000..4c93648 Binary files /dev/null and b/tra-app/src/static/images/me/study.png differ diff --git a/tra-app/src/static/images/me/todo.png b/tra-app/src/static/images/me/todo.png new file mode 100644 index 0000000..3412e2b Binary files /dev/null and b/tra-app/src/static/images/me/todo.png differ diff --git a/tra-app/src/static/images/me/wuman.png b/tra-app/src/static/images/me/wuman.png new file mode 100644 index 0000000..f644baf Binary files /dev/null and b/tra-app/src/static/images/me/wuman.png differ diff --git a/tra-app/src/static/images/messageNotification/delete_icon.png b/tra-app/src/static/images/messageNotification/delete_icon.png new file mode 100644 index 0000000..2411029 Binary files /dev/null and b/tra-app/src/static/images/messageNotification/delete_icon.png differ diff --git a/tra-app/src/static/images/messageNotification/popup_bg.png b/tra-app/src/static/images/messageNotification/popup_bg.png new file mode 100644 index 0000000..9e7b941 Binary files /dev/null and b/tra-app/src/static/images/messageNotification/popup_bg.png differ diff --git a/tra-app/src/static/images/messageNotification/type_01.png b/tra-app/src/static/images/messageNotification/type_01.png new file mode 100644 index 0000000..88a3548 Binary files /dev/null and b/tra-app/src/static/images/messageNotification/type_01.png differ diff --git a/tra-app/src/static/images/messageNotification/type_02.png b/tra-app/src/static/images/messageNotification/type_02.png new file mode 100644 index 0000000..57e04c4 Binary files /dev/null and b/tra-app/src/static/images/messageNotification/type_02.png differ diff --git a/tra-app/src/static/images/messageNotification/type_03.png b/tra-app/src/static/images/messageNotification/type_03.png new file mode 100644 index 0000000..369e7f0 Binary files /dev/null and b/tra-app/src/static/images/messageNotification/type_03.png differ diff --git a/tra-app/src/static/images/messageNotification/type_04.png b/tra-app/src/static/images/messageNotification/type_04.png new file mode 100644 index 0000000..bc55191 Binary files /dev/null and b/tra-app/src/static/images/messageNotification/type_04.png differ diff --git a/tra-app/src/static/images/messageNotification/type_05.png b/tra-app/src/static/images/messageNotification/type_05.png new file mode 100644 index 0000000..19c0094 Binary files /dev/null and b/tra-app/src/static/images/messageNotification/type_05.png differ diff --git a/tra-app/src/static/images/messageNotification/type_06.png b/tra-app/src/static/images/messageNotification/type_06.png new file mode 100644 index 0000000..c962f96 Binary files /dev/null and b/tra-app/src/static/images/messageNotification/type_06.png differ diff --git a/tra-app/src/static/images/messageNotification/type_07.png b/tra-app/src/static/images/messageNotification/type_07.png new file mode 100644 index 0000000..115c671 Binary files /dev/null and b/tra-app/src/static/images/messageNotification/type_07.png differ diff --git a/tra-app/src/static/images/pointsAndRank/badge-bg.png b/tra-app/src/static/images/pointsAndRank/badge-bg.png new file mode 100644 index 0000000..d94ed04 Binary files /dev/null and b/tra-app/src/static/images/pointsAndRank/badge-bg.png differ diff --git a/tra-app/src/static/images/pointsAndRank/badge-bg1.png b/tra-app/src/static/images/pointsAndRank/badge-bg1.png new file mode 100644 index 0000000..0c800a1 Binary files /dev/null and b/tra-app/src/static/images/pointsAndRank/badge-bg1.png differ diff --git a/tra-app/src/static/images/pointsAndRank/badge-icon.png b/tra-app/src/static/images/pointsAndRank/badge-icon.png new file mode 100644 index 0000000..6dcf5b5 Binary files /dev/null and b/tra-app/src/static/images/pointsAndRank/badge-icon.png differ diff --git a/tra-app/src/static/images/pointsAndRank/badge-title-left.png b/tra-app/src/static/images/pointsAndRank/badge-title-left.png new file mode 100644 index 0000000..0e03e19 Binary files /dev/null and b/tra-app/src/static/images/pointsAndRank/badge-title-left.png differ diff --git a/tra-app/src/static/images/pointsAndRank/badge-title-right.png b/tra-app/src/static/images/pointsAndRank/badge-title-right.png new file mode 100644 index 0000000..ad3f1f7 Binary files /dev/null and b/tra-app/src/static/images/pointsAndRank/badge-title-right.png differ diff --git a/tra-app/src/static/images/pointsAndRank/badge-wheat-left.png b/tra-app/src/static/images/pointsAndRank/badge-wheat-left.png new file mode 100644 index 0000000..e70ccac Binary files /dev/null and b/tra-app/src/static/images/pointsAndRank/badge-wheat-left.png differ diff --git a/tra-app/src/static/images/pointsAndRank/badge-wheat-right.png b/tra-app/src/static/images/pointsAndRank/badge-wheat-right.png new file mode 100644 index 0000000..88331a4 Binary files /dev/null and b/tra-app/src/static/images/pointsAndRank/badge-wheat-right.png differ diff --git a/tra-app/src/static/images/pointsAndRank/bg.png b/tra-app/src/static/images/pointsAndRank/bg.png new file mode 100644 index 0000000..7bcccbf Binary files /dev/null and b/tra-app/src/static/images/pointsAndRank/bg.png differ diff --git a/tra-app/src/static/images/pointsAndRank/fs.png b/tra-app/src/static/images/pointsAndRank/fs.png new file mode 100644 index 0000000..c6ceb2f Binary files /dev/null and b/tra-app/src/static/images/pointsAndRank/fs.png differ diff --git a/tra-app/src/static/images/pointsAndRank/like.png b/tra-app/src/static/images/pointsAndRank/like.png new file mode 100644 index 0000000..3bbca0a Binary files /dev/null and b/tra-app/src/static/images/pointsAndRank/like.png differ diff --git a/tra-app/src/static/images/search/delete.png b/tra-app/src/static/images/search/delete.png new file mode 100644 index 0000000..940a069 Binary files /dev/null and b/tra-app/src/static/images/search/delete.png differ diff --git a/tra-app/src/static/images/search/no_search.png b/tra-app/src/static/images/search/no_search.png new file mode 100644 index 0000000..1273e38 Binary files /dev/null and b/tra-app/src/static/images/search/no_search.png differ diff --git a/tra-app/src/static/images/search/result/ability.png b/tra-app/src/static/images/search/result/ability.png new file mode 100644 index 0000000..bf81184 Binary files /dev/null and b/tra-app/src/static/images/search/result/ability.png differ diff --git a/tra-app/src/static/images/search/result/course.png b/tra-app/src/static/images/search/result/course.png new file mode 100644 index 0000000..5d721a3 Binary files /dev/null and b/tra-app/src/static/images/search/result/course.png differ diff --git a/tra-app/src/static/images/search/result/exam.png b/tra-app/src/static/images/search/result/exam.png new file mode 100644 index 0000000..b4cda33 Binary files /dev/null and b/tra-app/src/static/images/search/result/exam.png differ diff --git a/tra-app/src/static/images/search/result/task.png b/tra-app/src/static/images/search/result/task.png new file mode 100644 index 0000000..97959aa Binary files /dev/null and b/tra-app/src/static/images/search/result/task.png differ diff --git a/tra-app/src/static/images/search/result/test.png b/tra-app/src/static/images/search/result/test.png new file mode 100644 index 0000000..7b60093 Binary files /dev/null and b/tra-app/src/static/images/search/result/test.png differ diff --git a/tra-app/src/static/images/sparring/ask-icon.png b/tra-app/src/static/images/sparring/ask-icon.png new file mode 100644 index 0000000..9d86269 Binary files /dev/null and b/tra-app/src/static/images/sparring/ask-icon.png differ diff --git a/tra-app/src/static/images/sparring/bg.png b/tra-app/src/static/images/sparring/bg.png new file mode 100644 index 0000000..d3aca1a Binary files /dev/null and b/tra-app/src/static/images/sparring/bg.png differ diff --git a/tra-app/src/static/images/sparring/close-talk.png b/tra-app/src/static/images/sparring/close-talk.png new file mode 100644 index 0000000..90ed80f Binary files /dev/null and b/tra-app/src/static/images/sparring/close-talk.png differ diff --git a/tra-app/src/static/images/sparring/def-res.png b/tra-app/src/static/images/sparring/def-res.png new file mode 100644 index 0000000..a9a4b2a Binary files /dev/null and b/tra-app/src/static/images/sparring/def-res.png differ diff --git a/tra-app/src/static/images/sparring/den-icon.png b/tra-app/src/static/images/sparring/den-icon.png new file mode 100644 index 0000000..fee97e0 Binary files /dev/null and b/tra-app/src/static/images/sparring/den-icon.png differ diff --git a/tra-app/src/static/images/sparring/detail-icon-1.png b/tra-app/src/static/images/sparring/detail-icon-1.png new file mode 100644 index 0000000..847361f Binary files /dev/null and b/tra-app/src/static/images/sparring/detail-icon-1.png differ diff --git a/tra-app/src/static/images/sparring/detail-icon-2.png b/tra-app/src/static/images/sparring/detail-icon-2.png new file mode 100644 index 0000000..15aa01f Binary files /dev/null and b/tra-app/src/static/images/sparring/detail-icon-2.png differ diff --git a/tra-app/src/static/images/sparring/detail-icon-3.png b/tra-app/src/static/images/sparring/detail-icon-3.png new file mode 100644 index 0000000..db205b2 Binary files /dev/null and b/tra-app/src/static/images/sparring/detail-icon-3.png differ diff --git a/tra-app/src/static/images/sparring/detail-icon-4.png b/tra-app/src/static/images/sparring/detail-icon-4.png new file mode 100644 index 0000000..45d963f Binary files /dev/null and b/tra-app/src/static/images/sparring/detail-icon-4.png differ diff --git a/tra-app/src/static/images/sparring/feedback.png b/tra-app/src/static/images/sparring/feedback.png new file mode 100644 index 0000000..e81f958 Binary files /dev/null and b/tra-app/src/static/images/sparring/feedback.png differ diff --git a/tra-app/src/static/images/sparring/info-icon.png b/tra-app/src/static/images/sparring/info-icon.png new file mode 100644 index 0000000..a99c1a7 Binary files /dev/null and b/tra-app/src/static/images/sparring/info-icon.png differ diff --git a/tra-app/src/static/images/sparring/loading-dialog.gif b/tra-app/src/static/images/sparring/loading-dialog.gif new file mode 100644 index 0000000..fe121c9 Binary files /dev/null and b/tra-app/src/static/images/sparring/loading-dialog.gif differ diff --git a/tra-app/src/static/images/sparring/pass-res.png b/tra-app/src/static/images/sparring/pass-res.png new file mode 100644 index 0000000..c12f8ec Binary files /dev/null and b/tra-app/src/static/images/sparring/pass-res.png differ diff --git a/tra-app/src/static/images/sparring/phone-icon.png b/tra-app/src/static/images/sparring/phone-icon.png new file mode 100644 index 0000000..d476e1f Binary files /dev/null and b/tra-app/src/static/images/sparring/phone-icon.png differ diff --git a/tra-app/src/static/images/sparring/role-icon1.png b/tra-app/src/static/images/sparring/role-icon1.png new file mode 100644 index 0000000..00e8fd0 Binary files /dev/null and b/tra-app/src/static/images/sparring/role-icon1.png differ diff --git a/tra-app/src/static/images/sparring/role-icon2.png b/tra-app/src/static/images/sparring/role-icon2.png new file mode 100644 index 0000000..7119bee Binary files /dev/null and b/tra-app/src/static/images/sparring/role-icon2.png differ diff --git a/tra-app/src/static/images/sparring/role-icon3.png b/tra-app/src/static/images/sparring/role-icon3.png new file mode 100644 index 0000000..1d09ace Binary files /dev/null and b/tra-app/src/static/images/sparring/role-icon3.png differ diff --git a/tra-app/src/static/images/sparring/role-icon4.png b/tra-app/src/static/images/sparring/role-icon4.png new file mode 100644 index 0000000..71bcb95 Binary files /dev/null and b/tra-app/src/static/images/sparring/role-icon4.png differ diff --git a/tra-app/src/static/images/sparring/role-icon5.png b/tra-app/src/static/images/sparring/role-icon5.png new file mode 100644 index 0000000..7ae55f7 Binary files /dev/null and b/tra-app/src/static/images/sparring/role-icon5.png differ diff --git a/tra-app/src/static/images/sparring/role-icon6.png b/tra-app/src/static/images/sparring/role-icon6.png new file mode 100644 index 0000000..79351c1 Binary files /dev/null and b/tra-app/src/static/images/sparring/role-icon6.png differ diff --git a/tra-app/src/static/images/sparring/talk-icon.png b/tra-app/src/static/images/sparring/talk-icon.png new file mode 100644 index 0000000..4755b76 Binary files /dev/null and b/tra-app/src/static/images/sparring/talk-icon.png differ diff --git a/tra-app/src/static/images/sparring/time-icon.png b/tra-app/src/static/images/sparring/time-icon.png new file mode 100644 index 0000000..52f8e78 Binary files /dev/null and b/tra-app/src/static/images/sparring/time-icon.png differ diff --git a/tra-app/src/static/images/sparring/top-bg.png b/tra-app/src/static/images/sparring/top-bg.png new file mode 100644 index 0000000..ec0806a Binary files /dev/null and b/tra-app/src/static/images/sparring/top-bg.png differ diff --git a/tra-app/src/static/images/sparring/video-icon.png b/tra-app/src/static/images/sparring/video-icon.png new file mode 100644 index 0000000..6ebadd9 Binary files /dev/null and b/tra-app/src/static/images/sparring/video-icon.png differ diff --git a/tra-app/src/static/images/sparring/waiting.png b/tra-app/src/static/images/sparring/waiting.png new file mode 100644 index 0000000..b4637e7 Binary files /dev/null and b/tra-app/src/static/images/sparring/waiting.png differ diff --git a/tra-app/src/static/images/test/1.png b/tra-app/src/static/images/test/1.png new file mode 100644 index 0000000..e9c4458 Binary files /dev/null and b/tra-app/src/static/images/test/1.png differ diff --git a/tra-app/src/static/images/test/2.png b/tra-app/src/static/images/test/2.png new file mode 100644 index 0000000..c2628b2 Binary files /dev/null and b/tra-app/src/static/images/test/2.png differ diff --git a/tra-app/src/static/images/test/3.png b/tra-app/src/static/images/test/3.png new file mode 100644 index 0000000..9473e13 Binary files /dev/null and b/tra-app/src/static/images/test/3.png differ diff --git a/tra-app/src/static/images/test/4.png b/tra-app/src/static/images/test/4.png new file mode 100644 index 0000000..522212f Binary files /dev/null and b/tra-app/src/static/images/test/4.png differ diff --git a/tra-app/src/static/images/test/search_img.png b/tra-app/src/static/images/test/search_img.png new file mode 100644 index 0000000..ba1127d Binary files /dev/null and b/tra-app/src/static/images/test/search_img.png differ diff --git a/tra-app/src/static/images/test/touxiang.png b/tra-app/src/static/images/test/touxiang.png new file mode 100644 index 0000000..02a5d4e Binary files /dev/null and b/tra-app/src/static/images/test/touxiang.png differ diff --git a/tra-app/src/static/images/test/xz.png b/tra-app/src/static/images/test/xz.png new file mode 100644 index 0000000..13cfd3c Binary files /dev/null and b/tra-app/src/static/images/test/xz.png differ diff --git a/tra-app/src/static/images/test/xz1.png b/tra-app/src/static/images/test/xz1.png new file mode 100644 index 0000000..a77a124 Binary files /dev/null and b/tra-app/src/static/images/test/xz1.png differ diff --git a/tra-app/src/static/images/test/xz2.png b/tra-app/src/static/images/test/xz2.png new file mode 100644 index 0000000..82ed1ae Binary files /dev/null and b/tra-app/src/static/images/test/xz2.png differ diff --git a/tra-app/src/static/images/test/xz3.png b/tra-app/src/static/images/test/xz3.png new file mode 100644 index 0000000..7bf9600 Binary files /dev/null and b/tra-app/src/static/images/test/xz3.png differ diff --git a/tra-app/src/static/images/test/xz4.png b/tra-app/src/static/images/test/xz4.png new file mode 100644 index 0000000..0012f15 Binary files /dev/null and b/tra-app/src/static/images/test/xz4.png differ diff --git a/tra-app/src/static/images/test/xz5.png b/tra-app/src/static/images/test/xz5.png new file mode 100644 index 0000000..b86ad56 Binary files /dev/null and b/tra-app/src/static/images/test/xz5.png differ diff --git a/tra-app/src/static/images/testingHall/testing.png b/tra-app/src/static/images/testingHall/testing.png new file mode 100644 index 0000000..06ef909 Binary files /dev/null and b/tra-app/src/static/images/testingHall/testing.png differ diff --git a/tra-app/src/static/images/welcome/01.png b/tra-app/src/static/images/welcome/01.png new file mode 100644 index 0000000..6ac692a Binary files /dev/null and b/tra-app/src/static/images/welcome/01.png differ diff --git a/tra-app/src/static/images/welcome/02.png b/tra-app/src/static/images/welcome/02.png new file mode 100644 index 0000000..e188755 Binary files /dev/null and b/tra-app/src/static/images/welcome/02.png differ diff --git a/tra-app/src/static/images/welcome/03.png b/tra-app/src/static/images/welcome/03.png new file mode 100644 index 0000000..354fb94 Binary files /dev/null and b/tra-app/src/static/images/welcome/03.png differ diff --git a/tra-app/src/static/view-pdf/pdf.html b/tra-app/src/static/view-pdf/pdf.html new file mode 100644 index 0000000..b6b4b32 --- /dev/null +++ b/tra-app/src/static/view-pdf/pdf.html @@ -0,0 +1,126 @@ + + + + + + + + + + + + + 详情 + + + + + + +
+
+
+
100
+
%
+
-
+
+
+ + + + + + + + \ No newline at end of file diff --git a/tra-app/src/static/view-pdf/pdf/jquery-1.11.3.min.js b/tra-app/src/static/view-pdf/pdf/jquery-1.11.3.min.js new file mode 100644 index 0000000..fdd413a --- /dev/null +++ b/tra-app/src/static/view-pdf/pdf/jquery-1.11.3.min.js @@ -0,0 +1,5 @@ +/*! jQuery v1.11.3 | (c) 2005, 2015 jQuery Foundation, Inc. | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l="1.11.3",m=function(a,b){return new m.fn.init(a,b)},n=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,o=/^-ms-/,p=/-([\da-z])/gi,q=function(a,b){return b.toUpperCase()};m.fn=m.prototype={jquery:l,constructor:m,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=m.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return m.each(this,a,b)},map:function(a){return this.pushStack(m.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},m.extend=m.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||m.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(m.isPlainObject(c)||(b=m.isArray(c)))?(b?(b=!1,f=a&&m.isArray(a)?a:[]):f=a&&m.isPlainObject(a)?a:{},g[d]=m.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},m.extend({expando:"jQuery"+(l+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===m.type(a)},isArray:Array.isArray||function(a){return"array"===m.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){return!m.isArray(a)&&a-parseFloat(a)+1>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==m.type(a)||a.nodeType||m.isWindow(a))return!1;try{if(a.constructor&&!j.call(a,"constructor")&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(k.ownLast)for(b in a)return j.call(a,b);for(b in a);return void 0===b||j.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(b){b&&m.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(o,"ms-").replace(p,q)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=r(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(n,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(r(Object(a))?m.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(g)return g.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=r(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(f=a[b],b=a,a=f),m.isFunction(a)?(c=d.call(arguments,2),e=function(){return a.apply(b||this,c.concat(d.call(arguments)))},e.guid=a.guid=a.guid||m.guid++,e):void 0},now:function(){return+new Date},support:k}),m.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function r(a){var b="length"in a&&a.length,c=m.type(a);return"function"===c||m.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var s=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N=M.replace("w","w#"),O="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+N+"))|)"+L+"*\\]",P=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+O+")*)|.*)\\)|)",Q=new RegExp(L+"+","g"),R=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),S=new RegExp("^"+L+"*,"+L+"*"),T=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),U=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),V=new RegExp(P),W=new RegExp("^"+N+"$"),X={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+O),PSEUDO:new RegExp("^"+P),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,aa=/[+~]/,ba=/'|\\/g,ca=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),da=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ea=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(fa){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],k=b.nodeType,"string"!=typeof a||!a||1!==k&&9!==k&&11!==k)return d;if(!e&&p){if(11!==k&&(f=_.exec(a)))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return H.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName)return H.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=1!==k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(ba,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+ra(o[l]);w=aa.test(a)&&pa(b.parentNode)||b,x=o.join(",")}if(x)try{return H.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function pa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=g.documentElement,e=g.defaultView,e&&e!==e.top&&(e.addEventListener?e.addEventListener("unload",ea,!1):e.attachEvent&&e.attachEvent("onunload",ea)),p=!f(g),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(g.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(g.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!g.getElementsByName||!g.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ca,da);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ca,da);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(g.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){var b=g.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",P)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===g||a.ownerDocument===v&&t(v,a)?-1:b===g||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,h=[a],i=[b];if(!e||!f)return a===g?-1:b===g?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?la(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},g):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ca,da),a[3]=(a[3]||a[4]||a[5]||"").replace(ca,da),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ca,da).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(Q," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(ca,da),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return W.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(ca,da).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:oa(function(){return[0]}),last:oa(function(a,b){return[b-1]}),eq:oa(function(a,b,c){return[0>c?c+b:c]}),even:oa(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:oa(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:oa(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:oa(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function sa(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function ta(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ua(a,b,c){for(var d=0,e=b.length;e>d;d++)ga(a,b[d],c);return c}function va(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function wa(a,b,c,d,e,f){return d&&!d[u]&&(d=wa(d)),e&&!e[u]&&(e=wa(e,f)),ia(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ua(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:va(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=va(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=va(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function xa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=sa(function(a){return a===b},h,!0),l=sa(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[sa(ta(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return wa(i>1&&ta(m),i>1&&ra(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&xa(a.slice(i,e)),f>e&&xa(a=a.slice(e)),f>e&&ra(a))}m.push(c)}return ta(m)}function ya(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=F.call(i));s=va(s)}H.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&ga.uniqueSort(i)}return k&&(w=v,j=t),r};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=xa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,ya(e,d)),f.selector=a}return f},i=ga.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ca,da),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ca,da),aa.test(j[0].type)&&pa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&ra(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,aa.test(a)&&pa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);m.find=s,m.expr=s.selectors,m.expr[":"]=m.expr.pseudos,m.unique=s.uniqueSort,m.text=s.getText,m.isXMLDoc=s.isXML,m.contains=s.contains;var t=m.expr.match.needsContext,u=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,v=/^.[^:#\[\.,]*$/;function w(a,b,c){if(m.isFunction(b))return m.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return m.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(v.test(b))return m.filter(b,a,c);b=m.filter(b,a)}return m.grep(a,function(a){return m.inArray(a,b)>=0!==c})}m.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?m.find.matchesSelector(d,a)?[d]:[]:m.find.matches(a,m.grep(b,function(a){return 1===a.nodeType}))},m.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(m(a).filter(function(){for(b=0;e>b;b++)if(m.contains(d[b],this))return!0}));for(b=0;e>b;b++)m.find(a,d[b],c);return c=this.pushStack(e>1?m.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(w(this,a||[],!1))},not:function(a){return this.pushStack(w(this,a||[],!0))},is:function(a){return!!w(this,"string"==typeof a&&t.test(a)?m(a):a||[],!1).length}});var x,y=a.document,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=m.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||x).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof m?b[0]:b,m.merge(this,m.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:y,!0)),u.test(c[1])&&m.isPlainObject(b))for(c in b)m.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}if(d=y.getElementById(c[2]),d&&d.parentNode){if(d.id!==c[2])return x.find(a);this.length=1,this[0]=d}return this.context=y,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):m.isFunction(a)?"undefined"!=typeof x.ready?x.ready(a):a(m):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),m.makeArray(a,this))};A.prototype=m.fn,x=m(y);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};m.extend({dir:function(a,b,c){var d=[],e=a[b];while(e&&9!==e.nodeType&&(void 0===c||1!==e.nodeType||!m(e).is(c)))1===e.nodeType&&d.push(e),e=e[b];return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),m.fn.extend({has:function(a){var b,c=m(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(m.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=t.test(a)||"string"!=typeof a?m(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&m.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?m.unique(f):f)},index:function(a){return a?"string"==typeof a?m.inArray(this[0],m(a)):m.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(m.unique(m.merge(this.get(),m(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}m.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return m.dir(a,"parentNode")},parentsUntil:function(a,b,c){return m.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return m.dir(a,"nextSibling")},prevAll:function(a){return m.dir(a,"previousSibling")},nextUntil:function(a,b,c){return m.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return m.dir(a,"previousSibling",c)},siblings:function(a){return m.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return m.sibling(a.firstChild)},contents:function(a){return m.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:m.merge([],a.childNodes)}},function(a,b){m.fn[a]=function(c,d){var e=m.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=m.filter(d,e)),this.length>1&&(C[a]||(e=m.unique(e)),B.test(a)&&(e=e.reverse())),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return m.each(a.match(E)||[],function(a,c){b[c]=!0}),b}m.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):m.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(c=a.memory&&l,d=!0,f=g||0,g=0,e=h.length,b=!0;h&&e>f;f++)if(h[f].apply(l[0],l[1])===!1&&a.stopOnFalse){c=!1;break}b=!1,h&&(i?i.length&&j(i.shift()):c?h=[]:k.disable())},k={add:function(){if(h){var d=h.length;!function f(b){m.each(b,function(b,c){var d=m.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&f(c)})}(arguments),b?e=h.length:c&&(g=d,j(c))}return this},remove:function(){return h&&m.each(arguments,function(a,c){var d;while((d=m.inArray(c,h,d))>-1)h.splice(d,1),b&&(e>=d&&e--,f>=d&&f--)}),this},has:function(a){return a?m.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],e=0,this},disable:function(){return h=i=c=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,c||k.disable(),this},locked:function(){return!i},fireWith:function(a,c){return!h||d&&!i||(c=c||[],c=[a,c.slice?c.slice():c],b?i.push(c):j(c)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!d}};return k},m.extend({Deferred:function(a){var b=[["resolve","done",m.Callbacks("once memory"),"resolved"],["reject","fail",m.Callbacks("once memory"),"rejected"],["notify","progress",m.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return m.Deferred(function(c){m.each(b,function(b,f){var g=m.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&m.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?m.extend(a,d):d}},e={};return d.pipe=d.then,m.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&m.isFunction(a.promise)?e:0,g=1===f?a:m.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&m.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;m.fn.ready=function(a){return m.ready.promise().done(a),this},m.extend({isReady:!1,readyWait:1,holdReady:function(a){a?m.readyWait++:m.ready(!0)},ready:function(a){if(a===!0?!--m.readyWait:!m.isReady){if(!y.body)return setTimeout(m.ready);m.isReady=!0,a!==!0&&--m.readyWait>0||(H.resolveWith(y,[m]),m.fn.triggerHandler&&(m(y).triggerHandler("ready"),m(y).off("ready")))}}});function I(){y.addEventListener?(y.removeEventListener("DOMContentLoaded",J,!1),a.removeEventListener("load",J,!1)):(y.detachEvent("onreadystatechange",J),a.detachEvent("onload",J))}function J(){(y.addEventListener||"load"===event.type||"complete"===y.readyState)&&(I(),m.ready())}m.ready.promise=function(b){if(!H)if(H=m.Deferred(),"complete"===y.readyState)setTimeout(m.ready);else if(y.addEventListener)y.addEventListener("DOMContentLoaded",J,!1),a.addEventListener("load",J,!1);else{y.attachEvent("onreadystatechange",J),a.attachEvent("onload",J);var c=!1;try{c=null==a.frameElement&&y.documentElement}catch(d){}c&&c.doScroll&&!function e(){if(!m.isReady){try{c.doScroll("left")}catch(a){return setTimeout(e,50)}I(),m.ready()}}()}return H.promise(b)};var K="undefined",L;for(L in m(k))break;k.ownLast="0"!==L,k.inlineBlockNeedsLayout=!1,m(function(){var a,b,c,d;c=y.getElementsByTagName("body")[0],c&&c.style&&(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),typeof b.style.zoom!==K&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",k.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(d))}),function(){var a=y.createElement("div");if(null==k.deleteExpando){k.deleteExpando=!0;try{delete a.test}catch(b){k.deleteExpando=!1}}a=null}(),m.acceptData=function(a){var b=m.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b};var M=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,N=/([A-Z])/g;function O(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(N,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:M.test(c)?m.parseJSON(c):c}catch(e){}m.data(a,b,c)}else c=void 0}return c}function P(a){var b;for(b in a)if(("data"!==b||!m.isEmptyObject(a[b]))&&"toJSON"!==b)return!1; + +return!0}function Q(a,b,d,e){if(m.acceptData(a)){var f,g,h=m.expando,i=a.nodeType,j=i?m.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||m.guid++:h),j[k]||(j[k]=i?{}:{toJSON:m.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=m.extend(j[k],b):j[k].data=m.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[m.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[m.camelCase(b)])):f=g,f}}function R(a,b,c){if(m.acceptData(a)){var d,e,f=a.nodeType,g=f?m.cache:a,h=f?a[m.expando]:m.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){m.isArray(b)?b=b.concat(m.map(b,m.camelCase)):b in d?b=[b]:(b=m.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!P(d):!m.isEmptyObject(d))return}(c||(delete g[h].data,P(g[h])))&&(f?m.cleanData([a],!0):k.deleteExpando||g!=g.window?delete g[h]:g[h]=null)}}}m.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?m.cache[a[m.expando]]:a[m.expando],!!a&&!P(a)},data:function(a,b,c){return Q(a,b,c)},removeData:function(a,b){return R(a,b)},_data:function(a,b,c){return Q(a,b,c,!0)},_removeData:function(a,b){return R(a,b,!0)}}),m.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=m.data(f),1===f.nodeType&&!m._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=m.camelCase(d.slice(5)),O(f,d,e[d])));m._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){m.data(this,a)}):arguments.length>1?this.each(function(){m.data(this,a,b)}):f?O(f,a,m.data(f,a)):void 0},removeData:function(a){return this.each(function(){m.removeData(this,a)})}}),m.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=m._data(a,b),c&&(!d||m.isArray(c)?d=m._data(a,b,m.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=m.queue(a,b),d=c.length,e=c.shift(),f=m._queueHooks(a,b),g=function(){m.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return m._data(a,c)||m._data(a,c,{empty:m.Callbacks("once memory").add(function(){m._removeData(a,b+"queue"),m._removeData(a,c)})})}}),m.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},W=/^(?:checkbox|radio)$/i;!function(){var a=y.createElement("input"),b=y.createElement("div"),c=y.createDocumentFragment();if(b.innerHTML="
a",k.leadingWhitespace=3===b.firstChild.nodeType,k.tbody=!b.getElementsByTagName("tbody").length,k.htmlSerialize=!!b.getElementsByTagName("link").length,k.html5Clone="<:nav>"!==y.createElement("nav").cloneNode(!0).outerHTML,a.type="checkbox",a.checked=!0,c.appendChild(a),k.appendChecked=a.checked,b.innerHTML="",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue,c.appendChild(b),b.innerHTML="",k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,k.noCloneEvent=!0,b.attachEvent&&(b.attachEvent("onclick",function(){k.noCloneEvent=!1}),b.cloneNode(!0).click()),null==k.deleteExpando){k.deleteExpando=!0;try{delete b.test}catch(d){k.deleteExpando=!1}}}(),function(){var b,c,d=y.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(k[b+"Bubbles"]=c in a)||(d.setAttribute(c,"t"),k[b+"Bubbles"]=d.attributes[c].expando===!1);d=null}();var X=/^(?:input|select|textarea)$/i,Y=/^key/,Z=/^(?:mouse|pointer|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=/^([^.]*)(?:\.(.+)|)$/;function aa(){return!0}function ba(){return!1}function ca(){try{return y.activeElement}catch(a){}}m.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=m.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return typeof m===K||a&&m.event.triggered===a.type?void 0:m.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(E)||[""],h=b.length;while(h--)f=_.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=m.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=m.event.special[o]||{},l=m.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&m.expr.match.needsContext.test(e),namespace:p.join(".")},i),(n=g[o])||(n=g[o]=[],n.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?n.splice(n.delegateCount++,0,l):n.push(l),m.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m.hasData(a)&&m._data(a);if(r&&(k=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=_.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=m.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,n=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=n.length;while(f--)g=n[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(n.splice(f,1),g.selector&&n.delegateCount--,l.remove&&l.remove.call(a,g));i&&!n.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||m.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)m.event.remove(a,o+b[j],c,d,!0);m.isEmptyObject(k)&&(delete r.handle,m._removeData(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,l,n,o=[d||y],p=j.call(b,"type")?b.type:b,q=j.call(b,"namespace")?b.namespace.split("."):[];if(h=l=d=d||y,3!==d.nodeType&&8!==d.nodeType&&!$.test(p+m.event.triggered)&&(p.indexOf(".")>=0&&(q=p.split("."),p=q.shift(),q.sort()),g=p.indexOf(":")<0&&"on"+p,b=b[m.expando]?b:new m.Event(p,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=q.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:m.makeArray(c,[b]),k=m.event.special[p]||{},e||!k.trigger||k.trigger.apply(d,c)!==!1)){if(!e&&!k.noBubble&&!m.isWindow(d)){for(i=k.delegateType||p,$.test(i+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),l=h;l===(d.ownerDocument||y)&&o.push(l.defaultView||l.parentWindow||a)}n=0;while((h=o[n++])&&!b.isPropagationStopped())b.type=n>1?i:k.bindType||p,f=(m._data(h,"events")||{})[b.type]&&m._data(h,"handle"),f&&f.apply(h,c),f=g&&h[g],f&&f.apply&&m.acceptData(h)&&(b.result=f.apply(h,c),b.result===!1&&b.preventDefault());if(b.type=p,!e&&!b.isDefaultPrevented()&&(!k._default||k._default.apply(o.pop(),c)===!1)&&m.acceptData(d)&&g&&d[p]&&!m.isWindow(d)){l=d[g],l&&(d[g]=null),m.event.triggered=p;try{d[p]()}catch(r){}m.event.triggered=void 0,l&&(d[g]=l)}return b.result}},dispatch:function(a){a=m.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(m._data(this,"events")||{})[a.type]||[],k=m.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=m.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,g=0;while((e=f.handlers[g++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(e.namespace))&&(a.handleObj=e,a.data=e.data,c=((m.event.special[e.origType]||{}).handle||e.handler).apply(f.elem,i),void 0!==c&&(a.result=c)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(e=[],f=0;h>f;f++)d=b[f],c=d.selector+" ",void 0===e[c]&&(e[c]=d.needsContext?m(c,this).index(i)>=0:m.find(c,this,null,[i]).length),e[c]&&e.push(d);e.length&&g.push({elem:i,handlers:e})}return h]","i"),ha=/^\s+/,ia=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,ja=/<([\w:]+)/,ka=/\s*$/g,ra={option:[1,""],legend:[1,"
","
"],area:[1,"",""],param:[1,"",""],thead:[1,"","
"],tr:[2,"","
"],col:[2,"","
"],td:[3,"","
"],_default:k.htmlSerialize?[0,"",""]:[1,"X
","
"]},sa=da(y),ta=sa.appendChild(y.createElement("div"));ra.optgroup=ra.option,ra.tbody=ra.tfoot=ra.colgroup=ra.caption=ra.thead,ra.th=ra.td;function ua(a,b){var c,d,e=0,f=typeof a.getElementsByTagName!==K?a.getElementsByTagName(b||"*"):typeof a.querySelectorAll!==K?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||m.nodeName(d,b)?f.push(d):m.merge(f,ua(d,b));return void 0===b||b&&m.nodeName(a,b)?m.merge([a],f):f}function va(a){W.test(a.type)&&(a.defaultChecked=a.checked)}function wa(a,b){return m.nodeName(a,"table")&&m.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function xa(a){return a.type=(null!==m.find.attr(a,"type"))+"/"+a.type,a}function ya(a){var b=pa.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function za(a,b){for(var c,d=0;null!=(c=a[d]);d++)m._data(c,"globalEval",!b||m._data(b[d],"globalEval"))}function Aa(a,b){if(1===b.nodeType&&m.hasData(a)){var c,d,e,f=m._data(a),g=m._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)m.event.add(b,c,h[c][d])}g.data&&(g.data=m.extend({},g.data))}}function Ba(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!k.noCloneEvent&&b[m.expando]){e=m._data(b);for(d in e.events)m.removeEvent(b,d,e.handle);b.removeAttribute(m.expando)}"script"===c&&b.text!==a.text?(xa(b).text=a.text,ya(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),k.html5Clone&&a.innerHTML&&!m.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&W.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}}m.extend({clone:function(a,b,c){var d,e,f,g,h,i=m.contains(a.ownerDocument,a);if(k.html5Clone||m.isXMLDoc(a)||!ga.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(ta.innerHTML=a.outerHTML,ta.removeChild(f=ta.firstChild)),!(k.noCloneEvent&&k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||m.isXMLDoc(a)))for(d=ua(f),h=ua(a),g=0;null!=(e=h[g]);++g)d[g]&&Ba(e,d[g]);if(b)if(c)for(h=h||ua(a),d=d||ua(f),g=0;null!=(e=h[g]);g++)Aa(e,d[g]);else Aa(a,f);return d=ua(f,"script"),d.length>0&&za(d,!i&&ua(a,"script")),d=h=e=null,f},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,l,n=a.length,o=da(b),p=[],q=0;n>q;q++)if(f=a[q],f||0===f)if("object"===m.type(f))m.merge(p,f.nodeType?[f]:f);else if(la.test(f)){h=h||o.appendChild(b.createElement("div")),i=(ja.exec(f)||["",""])[1].toLowerCase(),l=ra[i]||ra._default,h.innerHTML=l[1]+f.replace(ia,"<$1>")+l[2],e=l[0];while(e--)h=h.lastChild;if(!k.leadingWhitespace&&ha.test(f)&&p.push(b.createTextNode(ha.exec(f)[0])),!k.tbody){f="table"!==i||ka.test(f)?""!==l[1]||ka.test(f)?0:h:h.firstChild,e=f&&f.childNodes.length;while(e--)m.nodeName(j=f.childNodes[e],"tbody")&&!j.childNodes.length&&f.removeChild(j)}m.merge(p,h.childNodes),h.textContent="";while(h.firstChild)h.removeChild(h.firstChild);h=o.lastChild}else p.push(b.createTextNode(f));h&&o.removeChild(h),k.appendChecked||m.grep(ua(p,"input"),va),q=0;while(f=p[q++])if((!d||-1===m.inArray(f,d))&&(g=m.contains(f.ownerDocument,f),h=ua(o.appendChild(f),"script"),g&&za(h),c)){e=0;while(f=h[e++])oa.test(f.type||"")&&c.push(f)}return h=null,o},cleanData:function(a,b){for(var d,e,f,g,h=0,i=m.expando,j=m.cache,l=k.deleteExpando,n=m.event.special;null!=(d=a[h]);h++)if((b||m.acceptData(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)n[e]?m.event.remove(d,e):m.removeEvent(d,e,g.handle);j[f]&&(delete j[f],l?delete d[i]:typeof d.removeAttribute!==K?d.removeAttribute(i):d[i]=null,c.push(f))}}}),m.fn.extend({text:function(a){return V(this,function(a){return void 0===a?m.text(this):this.empty().append((this[0]&&this[0].ownerDocument||y).createTextNode(a))},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wa(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wa(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?m.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||m.cleanData(ua(c)),c.parentNode&&(b&&m.contains(c.ownerDocument,c)&&za(ua(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&m.cleanData(ua(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&m.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return m.clone(this,a,b)})},html:function(a){return V(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(fa,""):void 0;if(!("string"!=typeof a||ma.test(a)||!k.htmlSerialize&&ga.test(a)||!k.leadingWhitespace&&ha.test(a)||ra[(ja.exec(a)||["",""])[1].toLowerCase()])){a=a.replace(ia,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(m.cleanData(ua(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,m.cleanData(ua(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,n=this,o=l-1,p=a[0],q=m.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&na.test(p))return this.each(function(c){var d=n.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(i=m.buildFragment(a,this[0].ownerDocument,!1,this),c=i.firstChild,1===i.childNodes.length&&(i=c),c)){for(g=m.map(ua(i,"script"),xa),f=g.length;l>j;j++)d=i,j!==o&&(d=m.clone(d,!0,!0),f&&m.merge(g,ua(d,"script"))),b.call(this[j],d,j);if(f)for(h=g[g.length-1].ownerDocument,m.map(g,ya),j=0;f>j;j++)d=g[j],oa.test(d.type||"")&&!m._data(d,"globalEval")&&m.contains(h,d)&&(d.src?m._evalUrl&&m._evalUrl(d.src):m.globalEval((d.text||d.textContent||d.innerHTML||"").replace(qa,"")));i=c=null}return this}}),m.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){m.fn[a]=function(a){for(var c,d=0,e=[],g=m(a),h=g.length-1;h>=d;d++)c=d===h?this:this.clone(!0),m(g[d])[b](c),f.apply(e,c.get());return this.pushStack(e)}});var Ca,Da={};function Ea(b,c){var d,e=m(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:m.css(e[0],"display");return e.detach(),f}function Fa(a){var b=y,c=Da[a];return c||(c=Ea(a,b),"none"!==c&&c||(Ca=(Ca||m("