diff --git a/api/apps/sdk/dale_extra.py b/api/apps/sdk/dale_extra.py index cf3d0fe8..e88f4bd0 100644 --- a/api/apps/sdk/dale_extra.py +++ b/api/apps/sdk/dale_extra.py @@ -141,15 +141,35 @@ def upload_file(tenant_id,mesum_id): #logging.info(f"mesumid={mesum_id} {joined_string}") - prompt = (f"你是一名图片识别和理解助手" + prompt1 = (f"你是一名图片识别和理解助手" f"任务是先识别图片中文字,然后理解文字中包含的内容,分析哪一项可以作为识别出文字的标题," f"你的回答有3个结果,第一个结果匹配出的结果,JSON键值为antique" - f"从下面的候选项:{antiques_selected}进行匹配,每一个候选项中间以','分割,如果没有任何匹配则结果为'',以免误触发讲解,匹配成功则输出匹配出的内容" + f"从下面的候选项:{antiques_selected}进行匹配,每一个候选项中间以';'分割,如果没有任何匹配则结果为'',以免误触发讲解,匹配成功则输出匹配出的内容" f",第二个结果是原始识别的所有文字,json 键值为text" f"第三个结果是识别出文字与匹配项列表中元素的匹配度,范围从0-1,1表示100%匹配,0表示完全不匹配,JSON键值为match_score," "3个结果输出以{ }的json格式给出,匹配出文物、事件、人物的结果键值为antique" f"原始数据的键值为text,输出是1个完整的JSON数据,不要有多余的前置和后置内容,确保前端能正确解析出JSON数据") - + + prompt = ( + f"作为图片识别和理解助手,您的任务是:" + f"\n1. 精确识别图片中的文字内容" + f"\n2. 理解文字语义" + f"\n3. 从以下候选标题中选择最佳匹配项:" + f"\n [{antiques_selected}]" + f"\n\n### 输出要求:" + f"\n- 以严格JSON格式输出,包含3个字段:" + f"\n • `antique`: 匹配的标题(多个用英文分号';'分割,最多匹配3个,无匹配则空字符串)" + f"\n • `text`: 识别出的完整文字" + f"\n • `match_score`: 整体匹配度(0-1的浮点数),1=完全匹配" + f"\n\n### 匹配规则:" + f"\n1. 语义匹配优先于字面匹配" + f"\n2. 考虑同义词、近义词和描述性匹配" + f"\n3. 允许部分匹配(如'青铜酒器'匹配'青铜器')" + f"\n4. 若无明确匹配项,`antique`返回空字符串" + f"\n\n### 重要:" + f"\n- 输出必须是可直接解析的JSON,无任何前置/后置文本" + f"\n- 匹配度评分需客观反映文本与候选标题的相似度" + ) file = request.files['file'] if file.filename == '': @@ -221,12 +241,23 @@ def upload_file(tenant_id,mesum_id): message = response.choices[0].message parsed_json_res = parse_markdown_json(message.content) parsed_json_data = {"antique": "", "text": "", "match_score": 0} - + matchedArray = [] if parsed_json_res.get('success') is True: parsed_json_data = parsed_json_res.get('data') - for item in labels_with_id: - if item['label'] == parsed_json_data.get('antique'): - parsed_json_data['id'] = item.get('id') + matchedAntiqueArray = parsed_json_data.get('antique').split(';') # 识别出的文物的数组,中间以';'分割,可能有多个 + if len(matchedAntiqueArray) ==1: # 只有一个匹配项,直接返回 + for item in labels_with_id: + if item['label'] == parsed_json_data.get('antique'): + parsed_json_data['id'] = item.get('id') + else: # 有多个匹配项,需要进行多个匹配 + for label in matchedAntiqueArray: + antique= {'label':label} + for item in labels_with_id: + if item['label'] == label: + antique['id'] = item.get('id') + matchedArray.append(antique) + if len(matchedArray) > 0: + parsed_json_data['matchedArray'] = matchedArray logging.info(f"{parsed_json_data}") return jsonify({'message': 'File uploaded successfully','text': message.content, 'data': parsed_json_data}), 200 @@ -372,6 +403,9 @@ def start_background_cleaner(): # 应用启动时启动清理线程 start_background_cleaner() +# 在返回大模型对话的文本中,同时生成tts音频,由dialog_service 中的StreamSessionManager进行管理 +# session_id 为 def create_session(self, tts_model,sample_rate =8000, stream_format='mp3'): +# session_id = str(uuid.uuid4()) @manager.route('/tts_stream/',methods=['GET']) def tts_stream(session_id): session = stream_manager.sessions.get(session_id) @@ -415,7 +449,6 @@ def tts_stream(session_id): if session: # 延迟关闭会话,确保所有数据已发送 stream_manager.close_session(session_id) - logging.info(f"Session {session_id} closed. {total_audio_strean_length}") # 关键响应头设置 if session['stream_format'] == "wav": @@ -656,7 +689,8 @@ def dialog_tts_post(tenant_id, chat_id): f"{tts_sample_rate} {tts_stream_format}") # 返回音频流URL return jsonify({"tts_url": audio_stream_url, "audio_stream_id": audio_stream_id, - "sample_rate":tts_sample_rate, "stream_format":tts_stream_format,}) + "sample_rate":tts_sample_rate, "stream_format":tts_stream_format, + "ws_url":audio_stream_url}) except Exception as e: logging.error(f"请求处理失败: {str(e)}", exc_info=True) @@ -802,6 +836,15 @@ def minio_put_obj(tenant_id): except Exception as e: return get_error_data_result(message=f"minio put object error {e}") +@manager.route('/minio/list//', methods=['GET']) +@token_required +def list_objects(tenant_id,bucket: str, prefix: str = "", recursive: bool = True): + try: + result=minio_client.list_objects(bucket,prefix ,True) + return get_result(data=result) + except Exception as e: + return get_error_data_result(message=f"minio put list objects error {e}") + #------------------------------------------------ def audio_fade_in(audio_data, fade_length): # 假设音频数据是16位单声道PCM diff --git a/api/db/db_models.py b/api/db/db_models.py index a0f26bad..8edf3c7b 100644 --- a/api/db/db_models.py +++ b/api/db/db_models.py @@ -1025,6 +1025,11 @@ class MesumOverview(DataBaseModel): null=True, help_text="图片地址", index=False) + photo_prefix = CharField( + max_length=255, + null=True, + help_text="文物图片前缀", + index=False) address = CharField( max_length=1024, null=True, @@ -1055,6 +1060,8 @@ class MesumAntique(DataBaseModel): combined = TextField(null=True) ttsUrl_adult = CharField(max_length=256, null=True) ttsUrl_child = CharField(max_length=256, null=True) + photo_url = CharField(max_length=256, null=True) + orgin_text = TextField(null=True) class Meta: db_table = 'mesum_antique' diff --git a/api/db/services/ali_tts_service.py b/api/db/services/ali_tts_service.py new file mode 100644 index 00000000..52652b48 --- /dev/null +++ b/api/db/services/ali_tts_service.py @@ -0,0 +1,367 @@ +import asyncio,logging +from collections import deque +import threading, time,queue,uuid,time,array +from concurrent.futures import ThreadPoolExecutor + +ALI_KEY = "sk-a47a3fb5f4a94f66bbaf713779101c75" +from dashscope.api_entities.dashscope_response import SpeechSynthesisResponse +from dashscope.audio.tts import ( + ResultCallback as TTSResultCallback, + SpeechSynthesizer as TTSSpeechSynthesizer, + SpeechSynthesisResult as TTSSpeechSynthesisResult, + ) +# cyx 2025 01 19 测试cosyvoice 使用tts_v2 版本 +from dashscope.audio.tts_v2 import ( + ResultCallback as CosyResultCallback, + SpeechSynthesizer as CosySpeechSynthesizer, + AudioFormat, + ) + +class QwenTTS: + def __init__(self, key,format="mp3",sample_rate=44100, model_name="cosyvoice-v1/longyuan"): + import dashscope + import ssl + logging.info(f"---QwenTTS Construtor-- {format} {sample_rate} {model_name}") # cyx + self.model_name = model_name + dashscope.api_key = key + ssl._create_default_https_context = ssl._create_unverified_context # 禁用验证 + self.synthesizer = None + self.callback = None + self.is_cosyvoice = False + self.voice = "" + self.format = format + self.sample_rate = sample_rate + if '/' in model_name: + parts = model_name.split('/', 1) + # 返回分离后的两个字符串parts[0], parts[1] + if parts[0] == 'cosyvoice-v1': + self.is_cosyvoice = True + self.voice = parts[1] + + class Callback(TTSResultCallback): + def __init__(self) -> None: + self.dque = deque() + + def _run(self): + while True: + if not self.dque: + time.sleep(0) + continue + val = self.dque.popleft() + if val: + yield val + else: + break + + def on_open(self): + pass + + def on_complete(self): + self.dque.append(None) + + def on_error(self, response: SpeechSynthesisResponse): + print("Qwen tts error", str(response)) + raise RuntimeError(str(response)) + + def on_close(self): + pass + + def on_event(self, result: TTSSpeechSynthesisResult): + if result.get_audio_frame() is not None: + self.dque.append(result.get_audio_frame()) + + # -------------------------- + + class Callback_Cosy(CosyResultCallback): + def __init__(self,on_audio_data) -> None: + self.dque = deque() + self.on_audio_data = on_audio_data + + def _run(self): + while True: + if not self.dque: + time.sleep(0) + continue + val = self.dque.popleft() + if val: + yield val + else: + break + + def on_open(self): + logging.info("---Qwen tts on_open---") + pass + + def on_complete(self): + self.dque.append(None) + + def on_error(self, response: SpeechSynthesisResponse): + print("Qwen tts error", str(response)) + raise RuntimeError(str(response)) + + def on_close(self): + # print("---Qwen call back close") # cyx + logging.info("---Qwen tts on_close---") + pass + + """ canceled for test 语音大模型CosyVoice + def on_event(self, result: SpeechSynthesisResult): + if result.get_audio_frame() is not None: + self.dque.append(result.get_audio_frame()) + """ + + def on_event(self, message): + # print(f"recv speech synthsis message {message}") + pass + + # 以下适合语音大模型CosyVoice + def on_data(self, data: bytes) -> None: + if len(data) > 0: + if self.on_audio_data: + self.on_audio_data(data) + else: + self.dque.append(data) + + # -------------------------- + + def tts(self, text): + print(f"--QwenTTS--tts_stream begin-- {text} {self.is_cosyvoice} {self.voice}") # cyx + # text = self.normalize_text(text) + try: + # if self.model_name != 'cosyvoice-v1': + if self.is_cosyvoice is False: + self.callback = self.Callback() + TTSSpeechSynthesizer.call(model=self.model_name, + text=text, + callback=self.callback, + format="wav") # format="mp3") + else: + self.callback = self.Callback_Cosy(None) + format =self.get_audio_format(self.format,self.sample_rate) + self.synthesizer = CosySpeechSynthesizer( + model='cosyvoice-v1', + # voice="longyuan", #"longfei", + voice=self.voice, + callback=self.callback, + format=format + ) + self.synthesizer.call(text) + except Exception as e: + print(f"---dale---20 error {e}") # cyx + # ----------------------------------- + try: + for data in self.callback._run(): + #logging.info(f"dashcope return data {len(data)}") + yield data + # print(f"---Qwen return data {num_tokens_from_string(text)}") + # yield num_tokens_from_string(text) + + except Exception as e: + raise RuntimeError(f"**ERROR**: {e}") + + def init_streaming_call(self, on_data): + try: + self.callback = self.Callback_Cosy(on_data) + format =self.get_audio_format(self.format,self.sample_rate) + self.synthesizer = CosySpeechSynthesizer( + model='cosyvoice-v1', + # voice="longyuan", #"longfei", + voice=self.voice, + callback=self.callback, + format=format + ) + except Exception as e: + print(f"---dale---30 error {e}") # cyx + # ----------------------------------- + + def streaming_call(self,text): + if self.synthesizer: + self.synthesizer.streaming_call(text) + def end_streaming_call(self): + if self.synthesizer: + self.synthesizer.streaming_complete() + + def get_audio_format(self, format: str, sample_rate: int): + """动态获取音频格式""" + from dashscope.audio.tts_v2 import AudioFormat + format_map = { + (8000, 'mp3'): AudioFormat.MP3_8000HZ_MONO_128KBPS, + (8000, 'pcm'): AudioFormat.PCM_8000HZ_MONO_16BIT, + (8000, 'wav'): AudioFormat.WAV_8000HZ_MONO_16BIT, + (16000, 'pcm'): AudioFormat.PCM_16000HZ_MONO_16BIT, + (22050, 'mp3'): AudioFormat.MP3_22050HZ_MONO_256KBPS, + (22050, 'pcm'): AudioFormat.PCM_22050HZ_MONO_16BIT, + (22050, 'wav'): AudioFormat.WAV_22050HZ_MONO_16BIT, + (44100, 'mp3'): AudioFormat.MP3_44100HZ_MONO_256KBPS, + (44100, 'pcm'): AudioFormat.PCM_44100HZ_MONO_16BIT, + (44100, 'wav'): AudioFormat.WAV_44100HZ_MONO_16BIT, + (48000, 'mp3'): AudioFormat.MP3_48000HZ_MONO_256KBPS, + (48000, 'pcm'): AudioFormat.PCM_48000HZ_MONO_16BIT, + (48000, 'wav'):AudioFormat.WAV_48000HZ_MONO_16BIT + + } + return format_map.get((sample_rate, format), AudioFormat.MP3_16000HZ_MONO_128KBPS) + +class StreamSessionManager: + def __init__(self): + self.sessions = {} # {session_id: {'tts_model': obj, 'buffer': queue, 'task_queue': Queue}} + self.lock = threading.Lock() + self.executor = ThreadPoolExecutor(max_workers=30) # 固定大小线程池 + self.gc_interval = 300 # 5分钟清理一次 5 x 60 300秒 + self.gc_tts = 10 # 10s 大模型开始输出文本有可能需要比较久,2025年5 24 从3s->10s + + def create_session(self, tts_model,sample_rate =8000, stream_format='mp3',voice='cosyvoice-v1/longxiaochun'): + session_id = str(uuid.uuid4()) + def on_audio_data(chunk): + session = self.sessions.get(session_id) + first_chunk = not session['tts_chunk_data_valid'] + if session['stream_format'] == 'wav': + if first_chunk: + chunk_len = len(chunk) + if chunk_len > 2048: + session['buffer'].put(audio_fade_in(chunk, 1024)) + else: + session['buffer'].put(audio_fade_in(chunk, chunk_len)) + else: + session['buffer'].put(chunk) + else: + session['buffer'].put(chunk) + session['last_active'] = time.time() + session['audio_chunk_count'] = session['audio_chunk_count'] + 1 + if session['tts_chunk_data_valid'] is False: + session['tts_chunk_data_valid'] = True # 20250510 增加,表示连接TTS后台已经返回,可以通知前端了 + + with self.lock: + ali_tts_model = QwenTTS(ALI_KEY,stream_format, sample_rate,voice.split('@')[0]) + self.sessions[session_id] = { + 'tts_model': ali_tts_model, #tts_model, + 'buffer': queue.Queue(maxsize=300), # 线程安全队列 + 'task_queue': queue.Queue(), + 'active': True, + 'last_active': time.time(), + 'audio_chunk_count':0, + 'finished': threading.Event(), # 添加事件对象 + 'sample_rate':sample_rate, + 'stream_format':stream_format, + "tts_chunk_data_valid":False, + 'voice':voice, + } + self.sessions[session_id]['tts_model'].init_streaming_call(on_audio_data) + # 启动任务处理线程 + threading.Thread(target=self._process_tasks, args=(session_id,), daemon=True).start() + return session_id + + def append_text(self, session_id, text): + with self.lock: + session = self.sessions.get(session_id) + if not session: return + # 将文本放入任务队列(非阻塞) + #logging.info(f"StreamSessionManager append_text {text}") + try: + session['task_queue'].put(text, block=False) + except queue.Full: + logging.warning(f"Session {session_id} task queue full") + + def _process_tasks(self, session_id): + """任务处理线程(每个会话独立)""" + while True: + session = self.sessions.get(session_id) + if not session or not session['active']: + break + try: + #logging.info(f"StreamSessionManager _process_tasks {session['task_queue'].qsize()}") + # 合并多个文本块(最多等待50ms) + texts = [] + while len(texts) < 5: # 最大合并5个文本块 + try: + text = session['task_queue'].get(timeout=0.1) + #logging.info(f"StreamSessionManager _process_tasks --0 {len(texts)}") + texts.append(text) + except queue.Empty: + break + + if texts: + session['last_active'] = time.time() # 如果有处理文本,重置活跃时间 + # 提交到线程池处理 + #future=self.executor.submit( + # self._generate_audio, + # session_id, + # ' '.join(texts) # 合并文本减少请求次数 + #) + #future.result() # 等待转换任务执行完毕 + session['tts_model'].streaming_call(''.join(texts)) + session['last_active'] = time.time() + # 会话超时检查 + if time.time() - session['last_active'] > self.gc_interval: + self.close_session(session_id) + break + if time.time() - session['last_active'] > self.gc_tts: + session['tts_model'].end_streaming_call() + session['finished'].set() + break + + except Exception as e: + logging.error(f"Task processing error: {str(e)}") + + def _generate_audio(self, session_id, text): + """实际生成音频(线程池执行)""" + session = self.sessions.get(session_id) + if not session: return + # logging.info(f"_generate_audio:{text}") + first_chunk = True + logging.info(f"转换开始!!! {text}") + try: + for chunk in session['tts_model'].tts(text,session['sample_rate'],session['stream_format']): + if session['stream_format'] == 'wav': + if first_chunk: + chunk_len = len(chunk) + if chunk_len > 2048: + session['buffer'].put(audio_fade_in(chunk,1024)) + else: + session['buffer'].put(audio_fade_in(chunk, chunk_len)) + first_chunk = False + else: + session['buffer'].put(chunk) + else: + session['buffer'].put(chunk) + session['last_active'] = time.time() + session['audio_chunk_count'] = session['audio_chunk_count'] + 1 + if session['tts_chunk_data_valid'] is False: + session['tts_chunk_data_valid'] = True #20250510 增加,表示连接TTS后台已经返回,可以通知前端了 + logging.info(f"转换结束!!! {session['audio_chunk_count'] }") + except Exception as e: + session['buffer'].put(f"ERROR:{str(e)}") + logging.info(f"--_generate_audio--error {str(e)}") + + + def close_session(self, session_id): + with self.lock: + if session_id in self.sessions: + logging.info(f"--Session {session_id} close_session") + # 标记会话为不活跃 + self.sessions[session_id]['active'] = False + # 延迟2秒后清理资源 + threading.Timer(1, self._clean_session, args=[session_id]).start() + + def _clean_session(self, session_id): + with self.lock: + if session_id in self.sessions: + del self.sessions[session_id] + + def get_session(self, session_id): + return self.sessions.get(session_id) + + +stream_manager_w_stream = StreamSessionManager() +def audio_fade_in(audio_data, fade_length): + # 假设音频数据是16位单声道PCM + # 将二进制数据转换为整数数组 + samples = array.array('h', audio_data) + + # 对前fade_length个样本进行淡入处理 + for i in range(fade_length): + fade_factor = i / fade_length + samples[i] = int(samples[i] * fade_factor) + + # 将整数数组转换回二进制数据 + return samples.tobytes() \ No newline at end of file diff --git a/api/db/services/antique_service.py b/api/db/services/antique_service.py index 2d4b89d3..93872091 100644 --- a/api/db/services/antique_service.py +++ b/api/db/services/antique_service.py @@ -46,7 +46,8 @@ class MesumAntiqueService(CommonService): if mesum_brief: categories_text= mesum_brief[0].category # 统一替换中文分号为英文分号,并去除末尾分号 - categories_text = categories_text.replace(";", ";").rstrip(";") + if categories_text: + categories_text = categories_text.replace(";", ";").rstrip(";") # 分割并清理空格/空值 mesum_antique_categories = [dynasty.strip() for dynasty in categories_text.split(";") if dynasty.strip()] diff --git a/api/db/services/dialog_service.py b/api/db/services/dialog_service.py index 1e17f23c..03af6c3f 100644 --- a/api/db/services/dialog_service.py +++ b/api/db/services/dialog_service.py @@ -35,6 +35,7 @@ from api.utils.file_utils import get_project_base_directory from peewee import fn import threading, queue,uuid,time,array from concurrent.futures import ThreadPoolExecutor +from api.db.services.ali_tts_service import (stream_manager_w_stream as stream_manager) def audio_fade_in(audio_data, fade_length): # 假设音频数据是16位单声道PCM @@ -173,8 +174,7 @@ class StreamSessionManager: def get_session(self, session_id): return self.sessions.get(session_id) - -stream_manager = StreamSessionManager() +stream_manager_bk = StreamSessionManager() class DialogService(CommonService): model = Dialog @@ -756,16 +756,20 @@ def chat(dialog, messages, stream=True, **kwargs): if stream: last_ans = "" answer = "" + audio_url = None + if not kwargs.get('tts_disable'): # 创建TTS会话(提前初始化) - tts_session_id = stream_manager.create_session(tts_mdl,sample_rate=tts_sample_rate,stream_format=tts_stream_format) - tts_session = stream_manager.get_session(tts_session_id) - audio_url = f"/tts_stream/{tts_session_id}" + tts_session_id = stream_manager.create_session(tts_mdl,sample_rate=tts_sample_rate,stream_format=tts_stream_format, + voice = kwargs.get('tts_model')) + tts_session = stream_manager.get_session(tts_session_id) + audio_url = f"/tts_stream/{tts_session_id}" send_tts_url = False chunk_buffer = [] # 新增文本缓冲 last_flush_time = time.time() # 初始化时间戳 # 下面优先处理知识库中没有找到相关内容 cyx 20250323 修改 if not kwargs["knowledge"] or kwargs["knowledge"] =="" or len(kwargs["knowledge"]) < 4: - stream_manager.append_text(tts_session_id, "未找到相关内容") + if not kwargs.get('tts_disable'): + stream_manager.append_text(tts_session_id, "未找到相关内容") yield { "answer": "未找到相关内容", "delta_ans": "未找到相关内容", @@ -810,18 +814,19 @@ def chat(dialog, messages, stream=True, **kwargs): yield {"answer": answer, "delta_ans": sanitized_text, "reference": {}} """ # 首块返回音频URL - if send_tts_url is False and tts_session['tts_chunk_data_valid'] is True: - yield { - "answer": answer, - "delta_ans": sanitized_text, - "session_id": tts_session_id, - "reference": {}, - "audio_stream_url": audio_url, - "sample_rate":tts_sample_rate, - "stream_format":tts_stream_format, - } - send_tts_url = True # 发送一次tts url 给前端即可,不能重复发送 - logging.info(f"--chat retur tts url {audio_url}") + if send_tts_url is False and not kwargs.get('tts_disable'): + if tts_session['tts_chunk_data_valid'] is True: + yield { + "answer": answer, + "delta_ans": sanitized_text, + "session_id": tts_session_id, + "reference": {}, + "audio_stream_url": audio_url, + "sample_rate":tts_sample_rate, + "stream_format":tts_stream_format, + } + send_tts_url = True # 发送一次tts url 给前端即可,不能重复发送 + logging.info(f"--chat retur tts url {audio_url}") else: yield {"answer": answer, "delta_ans": sanitized_text,"reference": {}} diff --git a/asr-monitor-test/.env b/asr-monitor-test/.env index 090173fb..4e7b48fe 100644 --- a/asr-monitor-test/.env +++ b/asr-monitor-test/.env @@ -1 +1,4 @@ TIMEZONE=Asia/Shanghai +DASHSCOPE_API_KEY = sk-a47a3fb5f4a94f66bbaf713779101c75 + + diff --git a/asr-monitor-test/app.log b/asr-monitor-test/app.log new file mode 100644 index 00000000..e957e7fb --- /dev/null +++ b/asr-monitor-test/app.log @@ -0,0 +1,467 @@ +INFO: Started server process [50175] +INFO: Waiting for application startup. +21:51:05.972 - INFO - 监控服务已启动 +INFO: Application startup complete. +INFO: Uvicorn running on http://0.0.0.0:9580 (Press CTRL+C to quit) +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + ASR & Monitor Service Start +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +INFO: 35.203.210.77:60796 - "GET / HTTP/1.1" 404 Not Found +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +INFO: 221.238.94.141:45448 - "GET / HTTP/1.0" 404 Not Found +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +INFO: 221.238.94.141:45526 - "OPTIONS / HTTP/1.0" 404 Not Found +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +INFO: 221.238.94.141:45640 - "GET /nice%20ports%2C/Trinity.txt.bak HTTP/1.0" 404 Not Found +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +INFO: 221.238.94.141:38084 - "GET /api HTTP/1.0" 404 Not Found +WARNING: Invalid HTTP request received. +INFO: 221.238.94.141:38098 - "GET /hazelcast/rest/cluster HTTP/1.0" 404 Not Found +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +WARNING: Invalid HTTP request received. +INFO: 221.238.94.141:38374 - "GET / HTTP/1.1" 404 Not Found +06:57:54.681 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 106.55.206.109:0 - "GET /auth/verify HTTP/1.1" 200 OK +06:57:55.071 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 43.144.107.210:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +06:59:18.037 - INFO - Creating TTS request: {'text': '陶鬲形猪首盖盉,商,通高27厘米,腹径20.5厘米,2010年安钢大道M78出土,中国社会科学院考古研究所藏。器型呈猪首状,长嘴,圆眼微凸,双耳直立,憨态可掬。肩部饰三角绳纹,腹及袋足饰细绳纹,作为温酒的容器,加热后美酒从它的口中流出。', 'session_id': '0b4cdbaeaf9111efa53df171065841e8', 'delay_gen_audio': True, 'tts_sample_rate': 48000, 'tts_stream_format': 'mp3', 'model_name': 'cosyvoice-v1/longyuan@Tongyi-Qianwen', 'sample_rate': 48000, 'stream_format': 'mp3'} +INFO: 43.144.107.28:0 - "POST /tts/chats/39e9a2ba5a4711f0865bbb55c66f9471/tts?device_id=17513727656058688719 HTTP/1.1" 200 OK +INFO: ('1.13.185.116', 50174) - "WebSocket /tts/chats/39e9a2ba5a4711f0865bbb55c66f9471/tts/d45fdb6d-9bcb-4458-9b83-d63f3eb69720" [accepted] +06:59:19.472 - INFO - 新连接建立: 414fa19a-4425-4b60-b172-ec661a3457b2 +06:59:19.472 - INFO - 已经启动 start tts task {audio_stream_id} +06:59:19.473 - INFO - ---begin--init QwenTTS-- mp3 48000 cosyvoice-v1/longyuan@Tongyi-Qianwen cosyvoice-v1/longyuan +06:59:19.473 - INFO - QwenTTS--get_audio_format-- mp3 48000 +06:59:19.473 - INFO - setup_tts longyuan MP3 with 48000Hz sample rate, mono channel, 256kbps +INFO: connection open +06:59:19.617 - INFO - Websocket connected +06:59:19.729 - INFO - Qwen CosyVoice tts open +06:59:28.170 - INFO - --data_handler on_complete +06:59:28.170 - INFO - Qwen CosyVoice tts close +06:59:28.170 - INFO - --tts task event set error = None +INFO: connection closed +07:00:54.600 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 43.144.107.210:0 - "GET /auth/verify HTTP/1.1" 200 OK +07:00:54.946 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 43.140.60.44:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +07:04:53.633 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 43.144.107.28:0 - "GET /auth/verify HTTP/1.1" 200 OK +07:04:53.793 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 43.140.60.44:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +07:04:57.090 - INFO - Creating TTS request: {'text': '1945年10月,通县民主政府成立,机关设在潮白河西岸侯各庄西北角的一座菩萨庙(圆通庵)内,领导全县人民进行反蒋解放斗争。', 'session_id': '0b4cdbaeaf9111efa53df171065841e8', 'delay_gen_audio': True, 'tts_sample_rate': 48000, 'tts_stream_format': 'mp3', 'model_name': 'cosyvoice-v1/longyuan@Tongyi-Qianwen', 'sample_rate': 48000, 'stream_format': 'mp3'} +INFO: 43.140.60.33:0 - "POST /tts/chats/9cb04ecead5111ef99e80242ac120006/tts?device_id=17513727656058688719 HTTP/1.1" 200 OK +INFO: ('1.13.185.116', 33684) - "WebSocket /tts/chats/9cb04ecead5111ef99e80242ac120006/tts/c83abcdf-225e-41a1-a20d-0cc228b7c547" [accepted] +07:04:57.520 - INFO - 新连接建立: 2025badf-3414-48c6-8382-76c51673d9ef +07:04:57.520 - INFO - 已经启动 start tts task {audio_stream_id} +07:04:57.520 - INFO - ---begin--init QwenTTS-- mp3 48000 cosyvoice-v1/longyuan@Tongyi-Qianwen cosyvoice-v1/longyuan +07:04:57.520 - INFO - QwenTTS--get_audio_format-- mp3 48000 +07:04:57.520 - INFO - setup_tts longyuan MP3 with 48000Hz sample rate, mono channel, 256kbps +INFO: connection open +07:04:57.639 - INFO - Websocket connected +07:04:57.768 - INFO - Qwen CosyVoice tts open +07:05:01.717 - INFO - --data_handler on_complete +07:05:01.717 - INFO - Qwen CosyVoice tts close +07:05:01.717 - INFO - --tts task event set error = None +INFO: connection closed +07:06:17.189 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 43.140.60.44:0 - "GET /auth/verify HTTP/1.1" 200 OK +07:06:17.383 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 43.140.60.33:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +07:06:20.644 - INFO - Creating TTS request: {'text': '元大都水利建设最终形成了金水河、高梁河两进,坝河、通惠河两出,瓮山泊、积水潭两大蓄水水面格局,水利景观呈现“一心、一廊”的发展特征。城内供水区以积水潭为核心,通惠河漕运走廊连接北运河与白浮泉,串联起自然山水、寺庙、集市、私园、公共桥闸等景观,充分体现了城市、水利、景观三者之间的相互作用。元代水利建设对北京城市水利景观和生态环境具有深远的影响。', 'session_id': '0b4cdbaeaf9111efa53df171065841e8', 'delay_gen_audio': True, 'tts_sample_rate': 48000, 'tts_stream_format': 'mp3', 'model_name': 'cosyvoice-v1/longyuan@Tongyi-Qianwen', 'sample_rate': 48000, 'stream_format': 'mp3'} +INFO: 43.140.60.33:0 - "POST /tts/chats/9cb04ecead5111ef99e80242ac120006/tts?device_id=17513727656058688719 HTTP/1.1" 200 OK +INFO: ('1.13.185.116', 47246) - "WebSocket /tts/chats/9cb04ecead5111ef99e80242ac120006/tts/7d2bd2dd-dfc0-43e6-9b23-08e5251bb954" [accepted] +07:06:21.077 - INFO - 新连接建立: caa0458b-7662-4ebe-9a6b-480c57c400b6 +07:06:21.077 - INFO - 已经启动 start tts task {audio_stream_id} +07:06:21.077 - INFO - ---begin--init QwenTTS-- mp3 48000 cosyvoice-v1/longyuan@Tongyi-Qianwen cosyvoice-v1/longyuan +07:06:21.078 - INFO - QwenTTS--get_audio_format-- mp3 48000 +07:06:21.078 - INFO - setup_tts longyuan MP3 with 48000Hz sample rate, mono channel, 256kbps +INFO: connection open +07:06:21.194 - INFO - Websocket connected +07:06:21.324 - INFO - Qwen CosyVoice tts open +07:06:32.422 - INFO - --data_handler on_complete +07:06:32.422 - INFO - Qwen CosyVoice tts close +07:06:32.422 - INFO - --tts task event set error = None +INFO: connection closed +07:44:06.202 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +07:44:06.290 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +07:44:52.287 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +07:44:52.440 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +07:45:23.056 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +07:45:23.159 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +07:54:21.106 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +07:54:21.161 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +07:54:28.720 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +07:54:28.799 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +07:54:35.913 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +07:54:36.001 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +07:54:36.411 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +07:54:36.509 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +07:55:22.522 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +07:55:22.582 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +07:56:58.308 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +07:56:58.385 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +08:07:38.414 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +08:07:38.499 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +08:10:57.429 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +08:10:57.486 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +08:11:53.340 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +08:11:53.398 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +08:16:33.756 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +08:16:33.813 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +08:17:30.384 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +08:17:30.490 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +08:18:33.644 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +08:18:33.709 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +08:19:50.951 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +08:19:51.034 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +08:21:37.950 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +08:21:38.023 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +08:22:05.386 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +08:22:05.441 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +08:23:13.075 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +08:23:13.153 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +08:24:49.462 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +08:24:49.563 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +08:26:08.181 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +08:26:08.282 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +08:26:49.301 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +08:26:49.374 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +08:27:41.833 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +08:27:41.893 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +08:28:50.023 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +08:28:50.083 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +08:29:21.179 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +08:29:21.387 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +08:30:19.839 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +08:30:20.031 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +08:31:11.560 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +08:31:11.723 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +08:33:16.896 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +08:33:17.108 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +08:33:51.413 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +08:33:51.479 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +08:35:10.447 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +08:35:10.505 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +08:36:24.091 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +08:36:24.187 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +08:37:18.392 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +08:37:18.507 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +08:38:42.906 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 106.55.206.109:0 - "GET /auth/verify HTTP/1.1" 200 OK +08:38:43.148 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 43.140.60.33:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +08:49:31.793 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 106.55.206.109:0 - "GET /auth/verify HTTP/1.1" 200 OK +08:49:31.991 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 43.140.60.33:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +08:51:30.837 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +08:51:30.938 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +08:55:28.363 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +08:55:28.421 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +08:56:53.353 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +08:56:53.419 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +09:00:01.531 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +09:00:01.590 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +09:00:07.397 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +09:00:07.486 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +09:00:47.232 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +09:00:47.306 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +09:01:19.694 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +09:01:19.959 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +09:02:46.650 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +09:02:46.718 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +09:03:29.115 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +09:03:29.384 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +09:05:12.089 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +09:05:12.145 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +09:10:24.333 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +09:10:24.586 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +09:11:14.693 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +09:11:14.781 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +09:11:45.666 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +09:11:45.755 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +09:12:29.573 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +09:12:29.813 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +10:36:24.208 - INFO - verify_token user={'user_id': '689cac0d-062e-45df-88bf-d7c4b16ff226', 'openid': 'obKSz7bHxzDFWnLHmOrnY_-8D6fI', 'phone': '13810887276', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI2ODljYWMwZC0wNjJlLTQ1ZGYtODhiZi1kN2M0YjE2ZmYyMjYiLCJleHAiOjE3NTIwMzM3NzV9.Go3vld8ijajTsYSwzA45YS47O9thXdnLtrF0G7x1BAM', 'balance': 0, 'status': 1, 'last_login_time': 1751428975, 'create_time': 1748951630, 'create_date': datetime.datetime(2025, 6, 3, 19, 53, 50), 'update_time': 1751428975, 'update_date': datetime.datetime(2025, 7, 2, 12, 2, 55)} +INFO: 223.104.41.17:0 - "GET /auth/verify HTTP/1.1" 200 OK +10:36:24.285 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 223.104.41.17:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +10:36:34.664 - INFO - verify_token user={'user_id': '689cac0d-062e-45df-88bf-d7c4b16ff226', 'openid': 'obKSz7bHxzDFWnLHmOrnY_-8D6fI', 'phone': '13810887276', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI2ODljYWMwZC0wNjJlLTQ1ZGYtODhiZi1kN2M0YjE2ZmYyMjYiLCJleHAiOjE3NTIwMzM3NzV9.Go3vld8ijajTsYSwzA45YS47O9thXdnLtrF0G7x1BAM', 'balance': 0, 'status': 1, 'last_login_time': 1751428975, 'create_time': 1748951630, 'create_date': datetime.datetime(2025, 6, 3, 19, 53, 50), 'update_time': 1751428975, 'update_date': datetime.datetime(2025, 7, 2, 12, 2, 55)} +INFO: 223.104.41.17:0 - "GET /auth/verify HTTP/1.1" 200 OK +10:36:34.746 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 223.104.41.17:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +10:36:38.066 - INFO - Creating TTS request: {'text': '铜条形驭马器,商,长18.3厘米,宽1.4厘米,高2.2厘米,2021年邵家棚M109出土,安阳市文物考古研究院藏。\n', 'session_id': '0b4cdbaeaf9111efa53df171065841e8', 'delay_gen_audio': True, 'tts_sample_rate': 48000, 'tts_stream_format': 'mp3', 'model_name': 'cosyvoice-v1/longyuan@Tongyi-Qianwen', 'sample_rate': 48000, 'stream_format': 'mp3'} +INFO: 223.104.41.17:0 - "POST /tts/chats/39e9a2ba5a4711f0865bbb55c66f9471/tts?device_id=17507669394291632844 HTTP/1.1" 200 OK +INFO: ('1.13.185.116', 36130) - "WebSocket /tts/chats/39e9a2ba5a4711f0865bbb55c66f9471/tts/de035ae4-81d8-408b-b551-2eac17b07e50" [accepted] +10:36:38.312 - INFO - 新连接建立: 733148f7-d2ac-4fa2-ad3a-b41fb684d08e +10:36:38.312 - INFO - 已经启动 start tts task {audio_stream_id} +INFO: connection open +10:36:38.313 - INFO - ---begin--init QwenTTS-- mp3 48000 cosyvoice-v1/longyuan@Tongyi-Qianwen cosyvoice-v1/longyuan +10:36:38.313 - INFO - QwenTTS--get_audio_format-- mp3 48000 +10:36:38.313 - INFO - setup_tts longyuan MP3 with 48000Hz sample rate, mono channel, 256kbps +10:36:38.425 - INFO - Websocket connected +10:36:38.560 - INFO - Qwen CosyVoice tts open +10:36:42.366 - INFO - --data_handler on_complete +10:36:42.366 - INFO - Qwen CosyVoice tts close +10:36:42.366 - INFO - --tts task event set error = None +INFO: connection closed +10:36:49.363 - INFO - verify_token user={'user_id': '689cac0d-062e-45df-88bf-d7c4b16ff226', 'openid': 'obKSz7bHxzDFWnLHmOrnY_-8D6fI', 'phone': '13810887276', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI2ODljYWMwZC0wNjJlLTQ1ZGYtODhiZi1kN2M0YjE2ZmYyMjYiLCJleHAiOjE3NTIwMzM3NzV9.Go3vld8ijajTsYSwzA45YS47O9thXdnLtrF0G7x1BAM', 'balance': 0, 'status': 1, 'last_login_time': 1751428975, 'create_time': 1748951630, 'create_date': datetime.datetime(2025, 6, 3, 19, 53, 50), 'update_time': 1751428975, 'update_date': datetime.datetime(2025, 7, 2, 12, 2, 55)} +INFO: 223.104.41.17:0 - "GET /auth/verify HTTP/1.1" 200 OK +10:36:49.440 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 223.104.41.17:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +10:36:53.851 - INFO - Creating TTS request: {'text': '铜铙,商,①高18厘米,口长13.2厘米,口宽10.1厘米;②高13厘米,口长11.6厘米,口宽8.7厘米;③高11.9厘米,口长9.4厘米,口宽7厘米;1983年戚家庄东地出土,安阳市文物考古研究院藏。\n', 'session_id': '0b4cdbaeaf9111efa53df171065841e8', 'delay_gen_audio': True, 'tts_sample_rate': 48000, 'tts_stream_format': 'mp3', 'model_name': 'cosyvoice-v1/longyuan@Tongyi-Qianwen', 'sample_rate': 48000, 'stream_format': 'mp3'} +INFO: 223.104.41.17:0 - "POST /tts/chats/39e9a2ba5a4711f0865bbb55c66f9471/tts?device_id=17507669394291632844 HTTP/1.1" 200 OK +INFO: ('1.13.185.116', 49668) - "WebSocket /tts/chats/39e9a2ba5a4711f0865bbb55c66f9471/tts/c6355d58-9d94-4726-bf00-0581bcdb0e28" [accepted] +10:36:54.094 - INFO - 新连接建立: fc397484-625d-4db9-9fba-6d5a8949a57a +10:36:54.094 - INFO - 已经启动 start tts task {audio_stream_id} +10:36:54.094 - INFO - ---begin--init QwenTTS-- mp3 48000 cosyvoice-v1/longyuan@Tongyi-Qianwen cosyvoice-v1/longyuan +10:36:54.094 - INFO - QwenTTS--get_audio_format-- mp3 48000 +10:36:54.094 - INFO - setup_tts longyuan MP3 with 48000Hz sample rate, mono channel, 256kbps +INFO: connection open +10:36:54.202 - INFO - Websocket connected +10:36:54.341 - INFO - Qwen CosyVoice tts open +10:37:05.913 - INFO - --data_handler on_complete +10:37:05.914 - INFO - Qwen CosyVoice tts close +10:37:05.914 - INFO - --tts task event set error = None +INFO: connection closed +INFO: ('1.13.185.116', 59460) - "WebSocket /tts/chats/39e9a2ba5a4711f0865bbb55c66f9471/tts/9d5e77cc-ad9b-4a60-812f-880d2f182543" [accepted] +10:37:23.067 - INFO - 新连接建立: d3f6a319-1597-4283-8330-a26c2582c91c +INFO: connection open +10:37:23.121 - INFO - 代理文本流: completions_url=http://localhost:9380/api/v1/chats/39e9a2ba5a4711f0865bbb55c66f9471/completions {'question': '请介绍铜铙', 'stream': True, 'tts_model': 'cosyvoice-v1/longyuan@Tongyi-Qianwen', 'tts_sample_rate': 8000, 'tts_stream_format': 'mp3', 'tts_disable': True} +10:37:23.121 - INFO - ---begin--init QwenTTS-- mp3 8000 cosyvoice-v1/longyuan@Tongyi-Qianwen cosyvoice-v1/longyuan +10:37:23.121 - INFO - QwenTTS--get_audio_format-- mp3 8000 +10:37:23.121 - INFO - setup_tts longyuan MP3 with 8000Hz sample rate, mono channel, 128kbps +10:37:26.511 - INFO - HTTP Request: POST http://localhost:9380/api/v1/chats/39e9a2ba5a4711f0865bbb55c66f9471/completions "HTTP/1.1 200 OK" +10:37:26.511 - INFO - 响应状态: HTTP 200 +10:37:26.511 - INFO - 开始处理SSE流 +10:37:26.651 - INFO - Websocket connected +10:37:26.775 - INFO - Qwen CosyVoice tts open +INFO: ('1.13.185.116', 49122) - "WebSocket /tts/chats/39e9a2ba5a4711f0865bbb55c66f9471/tts/64880966-3b94-4fae-ac7e-e37e16768c2a" [accepted] +10:37:26.927 - INFO - 新连接建立: f666fc5e-48bd-4451-ac13-65c656784d3f +INFO: connection open +10:37:39.969 - INFO - SSE流处理完成,事件数: 23 +INFO: connection closed +INFO: connection closed +10:37:45.801 - INFO - 发送时检测到断开连接: f666fc5e-48bd-4451-ac13-65c656784d3f, +10:38:00.989 - ERROR - error from callback >: {"header":{"task_id":"8ed5859fd51746068babf1776c7c84df","event":"task-failed","error_code":"InvalidParameter","error_message":"request timeout after 23 seconds.","attributes":{}},"payload":{}} +10:38:00.989 - ERROR - error from callback >: websocket closed due to websocket closed due to {"header":{"task_id":"8ed5859fd51746068babf1776c7c84df","event":"task-failed","error_code":"InvalidParameter","error_message":"request timeout after 23 seconds.","attributes":{}},"payload":{}} +10:38:00.989 - INFO - tearing down on exception websocket closed due to websocket closed due to websocket closed due to {"header":{"task_id":"8ed5859fd51746068babf1776c7c84df","event":"task-failed","error_code":"InvalidParameter","error_message":"request timeout after 23 seconds.","attributes":{}},"payload":{}} +10:38:00.989 - ERROR - close status: 1007 +Qwen tts error {"header":{"task_id":"8ed5859fd51746068babf1776c7c84df","event":"task-failed","error_code":"InvalidParameter","error_message":"request timeout after 23 seconds.","attributes":{}},"payload":{}} +websocket closed due to {"header":{"task_id":"8ed5859fd51746068babf1776c7c84df","event":"task-failed","error_code":"InvalidParameter","error_message":"request timeout after 23 seconds.","attributes":{}},"payload":{}} +websocket closed due to websocket closed due to {"header":{"task_id":"8ed5859fd51746068babf1776c7c84df","event":"task-failed","error_code":"InvalidParameter","error_message":"request timeout after 23 seconds.","attributes":{}},"payload":{}} +websocket closed due to websocket closed due to websocket closed due to {"header":{"task_id":"8ed5859fd51746068babf1776c7c84df","event":"task-failed","error_code":"InvalidParameter","error_message":"request timeout after 23 seconds.","attributes":{}},"payload":{}} +INFO: 183.163.60.214:0 - "GET /auth/get_museum_list HTTP/1.1" 401 Unauthorized +Exception in thread Thread-1544 (_process_tasks): +Traceback (most recent call last): + File "/usr/lib/python3.10/threading.py", line 1016, in _bootstrap_inner + self.run() + File "/usr/lib/python3.10/threading.py", line 953, in run + self._target(*self._args, **self._kwargs) + File "/home/ubuntu/ragflow/asr-monitor-test/app/tts_service.py", line 192, in _process_tasks + session['tts_model'].end_streaming_call() + File "/home/ubuntu/ragflow/asr-monitor-test/app/tts_service.py", line 1005, in end_streaming_call + self.synthesizer.streaming_complete() + File "/home/ubuntu/ragflow/asr-monitor/venv/lib/python3.10/site-packages/dashscope/audio/tts_v2/speech_synthesizer.py", line 374, in streaming_complete + self.__send_str(request) + File "/home/ubuntu/ragflow/asr-monitor/venv/lib/python3.10/site-packages/dashscope/audio/tts_v2/speech_synthesizer.py", line 285, in __send_str + self.ws.send(data) + File "/home/ubuntu/ragflow/asr-monitor/venv/lib/python3.10/site-packages/websocket/_app.py", line 291, in send + raise WebSocketConnectionClosedException("Connection is already closed.") +websocket._exceptions.WebSocketConnectionClosedException: Connection is already closed. +INFO: 183.163.60.214:0 - "GET /auth/get_museum_list HTTP/1.1" 401 Unauthorized +INFO: 123.115.202.182:0 - "GET /auth/get_museum_list HTTP/1.1" 401 Unauthorized +11:23:13.037 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +11:23:13.104 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +11:25:03.125 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +11:25:03.187 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +11:26:37.995 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +11:26:38.077 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +11:27:45.347 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +11:27:45.695 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +11:28:25.996 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 61.141.77.65:0 - "GET /auth/verify HTTP/1.1" 200 OK +11:28:26.062 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 61.141.77.65:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +11:29:40.340 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +11:29:40.639 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +11:30:15.537 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 61.141.77.65:0 - "GET /auth/verify HTTP/1.1" 200 OK +11:30:15.603 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 61.141.77.65:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +11:30:52.210 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 61.141.77.65:0 - "GET /auth/verify HTTP/1.1" 200 OK +11:30:52.288 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 61.141.77.65:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +11:35:02.399 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +11:35:02.737 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +11:36:02.938 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +11:36:03.034 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +11:37:17.719 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +11:37:17.802 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +11:37:47.273 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 61.141.77.65:0 - "GET /auth/verify HTTP/1.1" 200 OK +11:37:47.367 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 61.141.77.65:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +11:39:13.999 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +11:39:14.337 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +11:41:51.134 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +11:41:51.495 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +11:43:01.977 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 163.125.202.178:0 - "GET /auth/verify HTTP/1.1" 200 OK +11:43:02.050 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 163.125.202.178:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +11:44:30.162 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 43.144.107.210:0 - "GET /auth/verify HTTP/1.1" 200 OK +11:44:30.345 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 43.140.60.44:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK +11:45:40.101 - INFO - verify_token user={'user_id': '76538cf0-a6cf-4aa8-8440-382dd2330384', 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE', 'phone': '18676776176', 'email': None, 'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NjUzOGNmMC1hNmNmLTRhYTgtODQ0MC0zODJkZDIzMzAzODQiLCJleHAiOjE3NTI1ODI1ODN9.GONQPAzcWnYQN2i14zfNhjU8OT1to7fWn6ZA6lNdSHY', 'balance': 0, 'status': 1, 'last_login_time': 1751977783, 'create_time': 1748960538, 'create_date': datetime.datetime(2025, 6, 3, 22, 22, 18), 'update_time': 1751977783, 'update_date': datetime.datetime(2025, 7, 8, 20, 29, 43)} +INFO: 43.144.107.210:0 - "GET /auth/verify HTTP/1.1" 200 OK +11:45:40.260 - INFO - get_museum_id_auth=[1, 2, 3] +INFO: 43.140.60.44:0 - "GET /auth/get_museum_id_auth HTTP/1.1" 200 OK diff --git a/asr-monitor-test/app/chat_service.py b/asr-monitor-test/app/chat_service.py new file mode 100644 index 00000000..06fe6834 --- /dev/null +++ b/asr-monitor-test/app/chat_service.py @@ -0,0 +1,148 @@ +import array +import asyncio +import base64 +import binascii +import datetime +import gzip +import io +import json +import logging +import os +import queue +import re +import threading +import time +import time +import uuid +from collections import deque +from concurrent.futures import ThreadPoolExecutor +from copy import deepcopy +from datetime import timedelta +from io import BytesIO +from threading import Lock, Thread +from timeit import default_timer as timer +from typing import Optional, Dict, Any + +from fastapi import FastAPI, UploadFile, File, Form, Header +from fastapi import WebSocket, APIRouter, WebSocketDisconnect, Request, Body, Query, Depends +from fastapi.responses import StreamingResponse, JSONResponse, Response +from openai import OpenAI +chat_router = APIRouter() + +# 从环境变量读取 OpenAI API 密钥 +openai_api_key = os.getenv("DASHSCOPE_API_KEY") +if not openai_api_key: + raise RuntimeError("DASHSCOPE_API_KEY environment variable not set") + +class MillisecondsFormatter(logging.Formatter): + """自定义日志格式器,添加毫秒时间戳""" + def formatTime(self, record, datefmt=None): + # 将时间戳转换为本地时间元组 + ct = self.converter(record.created) + # 格式化为 "小时:分钟:秒" + t = time.strftime("%H:%M:%S", ct) + # 添加毫秒(3位) + return f"{t}.{int(record.msecs):03d}" + +# 配置全局日志格式 +def configure_logging(): + # 创建 Formatter + log_format = "%(asctime)s - %(levelname)s - %(message)s" + formatter = MillisecondsFormatter(log_format) + + # 获取根 Logger 并清除已有配置 + root_logger = logging.getLogger() + root_logger.handlers = [] + + # 创建并配置 Handler(输出到控制台) + console_handler = logging.StreamHandler() + console_handler.setFormatter(formatter) + + # 设置日志级别并添加 Handler + root_logger.setLevel(logging.INFO) + root_logger.addHandler(console_handler) + +# 调用配置函数(程序启动时运行一次) +configure_logging() + +# 简单的 API 密钥验证(可选) +def verify_api_key(request: Request): + """简单的 API 密钥验证""" + if request.headers.get("X-API-KEY") != os.getenv("APP_API_KEY", "default_key"): + raise HTTPException(status_code=401, detail="Invalid API key") + return True + + +@chat_router.post("/completion") +async def chat_completion(request: Request): + """ + 与大语言模型进行对话 + + 请求体示例: + { + "messages": [ + {"role": "system", "content": "你是一个有帮助的助手"}, + {"role": "user", "content": "你好,请介绍一下FastAPI"} + ], + "model": "gpt-3.5-turbo", + "temperature": 0.7, + "max_tokens": 500 + } + """ + try: + # 手动解析请求体 + request_data = await request.json() + + # 验证必需字段 + if "messages" not in request_data or not isinstance(request_data["messages"], list): + raise HTTPException(status_code=400, detail="Missing or invalid 'messages' field") + + # 提取参数并提供默认值 + messages = request_data["messages"] + model = request_data.get("model", "qwen-plus-latest") + temperature = float(request_data.get("temperature", 0.7)) + max_tokens = int(request_data.get("max_tokens", 500)) + + # 验证消息结构 + for msg in messages: + if "role" not in msg or "content" not in msg: + raise HTTPException(status_code=400, detail="Invalid message structure") + if not isinstance(msg["role"], str) or not isinstance(msg["content"], str): + raise HTTPException(status_code=400, detail="Message content must be strings") + + logging.info(f"Received chat request: model={model}, messages={len(messages)}") + client = OpenAI( + # 若没有配置环境变量,请用百炼API Key将下行替换为:api_key="sk-xxx", + api_key=os.getenv("DASHSCOPE_API_KEY"), + base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", + ) + + # 调用 OpenAI API + response = client.chat.completions.create( + model=model, + messages=messages, + temperature=temperature + ) + + # 处理响应 + if not response.choices: + raise HTTPException(status_code=500, detail="No response from AI model") + + choice = response.choices[0] + message = choice.message + + return { + "message": { + "role": message.role, + "content": message.content + }, + "finish_reason": choice.finish_reason + } + + except json.JSONDecodeError: + raise HTTPException(status_code=400, detail="Invalid JSON format") + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + logging.exception("Internal server error") + raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}") \ No newline at end of file diff --git a/asr-monitor-test/app/database.py b/asr-monitor-test/app/database.py index b69ae5b4..bc6fad8e 100644 --- a/asr-monitor-test/app/database.py +++ b/asr-monitor-test/app/database.py @@ -5,9 +5,9 @@ from contextlib import contextmanager from config import DATABASE_CONFIG from datetime import datetime,timedelta import logging -from typing import Optional,Union +from typing import Union, List, Dict, Optional from tenacity import retry, stop_after_attempt, wait_fixed, retry_if_exception_type - +from dateutil.relativedelta import relativedelta # 配置日志 logging.basicConfig(level=logging.INFO) @@ -389,7 +389,7 @@ def create_user(data: dict): # 查询用户(多条件) -def get_users(status: int = None, email: str = None, phone: str = None): +def get_users(status: int = None, email: str = None, phone: str = None,openid: str = None): base_sql = "SELECT * FROM rag_flow.users_info WHERE 1=1" params = [] @@ -404,7 +404,10 @@ def get_users(status: int = None, email: str = None, phone: str = None): if phone: base_sql += " AND phone = %s" params.append(phone) - + + if openid: + base_sql += " AND openid = %s" + params.append(openid) base_sql += " ORDER BY create_time DESC" return execute_query(base_sql, tuple(params)) @@ -531,4 +534,672 @@ def update_login_info(user_id: str, token: str): ) execute_query(sql, params) - return get_user_by_id(user_id) \ No newline at end of file + return get_user_by_id(user_id) + + +# database.py +#------------------------------------------------------------ +def get_museum_subscriptions_by_museum(museum_id: int) -> list: + """ + 获取指定博物馆的所有有效订阅套餐 + + 功能说明: + - 查询指定博物馆的所有可用订阅套餐 + - 返回结果包含博物馆订阅信息和关联的模板信息 + + 参数说明: + - museum_id: 博物馆ID + + 返回: + - 包含订阅信息的字典列表 + + 重要逻辑: + - 只返回 is_active=1 的有效订阅 + - 通过 JOIN 关联 subscription_templates 表获取模板信息 + - 结果按有效期类型排序,便于前端展示 + """ + sql = """ + SELECT + ms.id, + ms.museum_id, + ms.template_id, + ms.price, + ms.sub_id, + ms.is_active, + ms.created_date, + ms.updated_date, + st.name AS template_name, + st.description AS template_description, + st.validity_type + FROM museum_subscriptions ms + JOIN subscription_templates st ON ms.template_id = st.id + WHERE ms.museum_id = %s AND ms.is_active = 1 + ORDER BY st.validity_type + """ + return execute_query(sql, (museum_id,)) + + +def get_museum_subscription_by_id(subscription_id: str) -> dict: + """ + 根据ID获取博物馆订阅套餐的详细信息 + + 功能说明: + - 通过订阅ID获取完整的订阅信息 + - 包含关联的模板信息 + + 参数说明: + - subscription_id: 博物馆订阅ID + + 返回: + - 订阅信息的字典,如果不存在则返回None + + 重要逻辑: + - 使用内连接获取模板信息 + - 确保返回完整的订阅+模板数据 + """ + sql = """ + SELECT + ms.*, + st.name AS template_name, + st.description AS template_description, + st.validity_type + FROM museum_subscriptions ms + JOIN subscription_templates st ON ms.template_id = st.id + WHERE ms.sub_id = %s + """ + result = execute_query(sql, (subscription_id,)) + return result[0] if result else None + + +def create_order(order_data: dict) -> int: + """ + 创建新的订阅订单 + + 功能说明: + - 在 subscription_orders 表中插入新订单记录 + + 参数说明: + - order_data: 包含订单数据的字典,字段包括: + order_id: 订单号 (必需) + user_id: 用户ID (必需) + museum_subscription_id: 博物馆订阅ID (必需) + amount: 订单金额 (默认0.00) + status: 订单状态 (默认'created') + transaction_id: 支付交易号 (可选) + create_date: 创建时间 (默认当前时间) + pay_time: 支付时间 (可选) + + 返回: + - 执行结果的行数 + + 重要逻辑: + - 为可选字段提供默认值 + - 使用参数化查询防止SQL注入 + - 处理所有必需的订单字段 + """ + sql = """ + INSERT INTO subscription_orders ( + order_id, + user_id, + museum_subscription_id, + amount, + status, + transaction_id, + create_date, + pay_time + ) VALUES ( + %s, %s, %s, %s, %s, %s, %s, %s + ) + """ + # 设置默认值 + params = ( + order_data.get("order_id"), + order_data.get("user_id"), + order_data.get("museum_subscription_id"), + order_data.get("amount", 0.00), + order_data.get("status", "created"), + order_data.get("transaction_id"), + order_data.get("create_date", datetime.now()), + order_data.get("pay_time") + ) + return execute_query(sql, params, autocommit=True) + + +def update_order(order_id: str, update_data: dict) -> int: + """ + 更新订单信息 + + 功能说明: + - 动态更新订单的字段 + - 只更新提供的字段 + + 参数说明: + - order_id: 要更新的订单ID + - update_data: 包含更新字段的字典,可选字段包括: + status: 订单状态 + transaction_id: 支付交易号 + amount: 订单金额 + pay_time: 支付时间 + + 返回: + - 受影响的行数 + + 重要逻辑: + - 只允许更新预定义的字段 + - 防止更新不允许的字段 + - 使用参数化查询确保安全 + """ + # 定义允许更新的字段及其SQL部分 + allowed_fields = { + 'status': 'status = %s', + 'transaction_id': 'transaction_id = %s', + 'amount': 'amount = %s', + 'pay_time': 'pay_time = %s' + } + + update_fields = [] + params = [] + + # 收集要更新的字段 + for field, sql_part in allowed_fields.items(): + if field in update_data: + update_fields.append(sql_part) + params.append(update_data[field]) + + # 如果没有提供有效更新字段,直接返回 + if not update_fields: + return 0 + + # 构建动态SQL + set_clause = ", ".join(update_fields) + sql = f"UPDATE subscription_orders SET {set_clause} WHERE order_id = %s" + params.append(order_id) + + return execute_query(sql, tuple(params), autocommit=True) + + +from typing import Union, List, Dict, Optional + +def get_order_by_id(order_id: str = None, user_id: str = None,combined = None) -> Union[Dict, List[Dict], None]: + """ + 根据订单ID或用户ID查询订单信息 + + 功能说明: + - 支持通过 order_id 或 user_id 查询订单信息 + - 当传入 order_id 时,返回单个订单(字典) + - 当传入 user_id 时,返回该用户的所有订单(列表) + + 参数说明: + - order_id: 订单号(字符串) + - user_id: 用户ID(字符串) + + 返回: + - 如果传入 order_id: 返回单个订单的字典(如果存在) + - 如果传入 user_id: 返回该用户的所有订单列表(可能为空) + - 如果两个参数都未传入,返回 None + + 重要逻辑: + - 使用参数化查询防止 SQL 注入 + - 当同时传入 order_id 和 user_id 时,优先使用 order_id + """ + if not order_id and not user_id: + return None # 两个参数都未传入,直接返回 None + sql_wo_condition= """ + SELECT + o.order_id, + o.user_id, + u.phone, + u.openid, + o.museum_subscription_id AS subscription_id, + ms.museum_id, + ms.template_id, + t.name AS template_name, + t.description AS template_desc, + t.validity_type, + ms.price AS subscription_price, + o.amount AS order_amount, + o.status AS order_status, + o.transaction_id, + o.create_date AS order_create_time, + o.pay_time, + us.start_date, + us.end_date, + us.is_active AS subscription_active, + mo.name AS museum_name + FROM + rag_flow.subscription_orders o + LEFT JOIN rag_flow.users_info u ON o.user_id = u.user_id + LEFT JOIN rag_flow.museum_subscriptions ms ON o.museum_subscription_id = ms.sub_id + LEFT JOIN rag_flow.subscription_templates t ON ms.template_id = t.id + LEFT JOIN rag_flow.user_subscriptions us ON o.order_id = us.order_id + LEFT JOIN rag_flow.mesum_overview mo ON ms.museum_id = mo.id + """ + # 优先使用 order_id 查询 + if order_id and not combined: + sql = "SELECT * FROM subscription_orders WHERE order_id = %s" + result = execute_query(sql, (order_id,)) + return result[0] if result and len(result) > 0 else None # 返回单个订单 + + # 如果 order_id 不存在,使用 user_id 查询 + if user_id and not combined: + sql = "SELECT * FROM subscription_orders WHERE user_id = %s" + result = execute_query(sql, (user_id,)) + return result if result else [] # 返回所有订单(列表) + if user_id and combined: + sql = sql_wo_condition + f"\n WHERE o.user_id = %s" + result = execute_query(sql, (user_id,)) + return result if result else [] # 返回所有订单(列表) + if order_id and combined: + sql = sql_wo_condition + f"\n WHERE o.order_id = %s" + result = execute_query(sql, (order_id,)) + return result[0] if result and len(result) > 0 else None # 返回单个订单 + +def create_user_subscription(data: dict) -> int: + """ + 创建用户订阅记录 + + 功能说明: + - 在 user_subscriptions 表中插入新记录 + - 表示用户购买并激活了一个订阅 + + 参数说明: + - data: 包含订阅数据的字典,字段包括: + user_id: 用户ID (必需) + museum_subscription_id: 博物馆订阅ID (必需) + order_id: 关联的订单ID (必需) + start_date: 开始时间 (默认当前时间) + end_date: 结束时间 (必需) + is_active: 是否激活 (默认1) + create_date: 创建时间 (默认当前时间) + + 返回: + - 执行结果的行数 + + 重要逻辑: + - 为可选字段提供默认值 + - 确保所有必需字段都有值 + - 处理时间字段的默认值 + """ + sql = """ + INSERT INTO user_subscriptions ( + user_id, + museum_subscription_id, + order_id, + start_date, + end_date, + is_active, + create_date + ) VALUES ( + %s, %s, %s, %s, %s, %s, %s + ) + """ + # 设置默认值 + params = ( + data.get("user_id"), + data.get("museum_subscription_id"), + data.get("order_id"), + data.get("start_date", datetime.now()), + data.get("end_date"), + data.get("is_active", 1), + data.get("create_date", datetime.now()) + ) + return execute_query(sql, params, autocommit=True) + + +def deactivate_previous_subscriptions(user_id: str, museum_subscription_id: str) -> int: + """ + 禁用用户在同一博物馆的旧订阅 + + 功能说明: + - 将用户在同一博物馆的所有激活订阅设为非激活状态 + - 确保同一博物馆只有一个激活订阅 + + 参数说明: + - user_id: 用户ID + - museum_id: 博物馆ID + + 返回: + - 受影响的行数 + + 重要逻辑: + - 通过JOIN关联博物馆订阅表 + - 只更新同一博物馆的订阅 + - 保持历史订阅记录,只修改激活状态 + """ + sql = """ + UPDATE user_subscriptions us + JOIN museum_subscriptions ms ON us.museum_subscription_id = ms.sub_id + SET us.is_active = 0 + WHERE us.user_id = %s AND us.museum_subscription_id = %s AND us.is_active = 1 + """ + return execute_query(sql, (user_id, museum_subscription_id), autocommit=True) + + +def get_user_by_id(user_id: str) -> dict: + """ + 根据用户ID获取用户信息 + + 功能说明: + - 通过用户ID查询用户基本信息 + + 参数说明: + - user_id: 用户ID + + 返回: + - 用户信息的字典,如果不存在则返回None + + 重要逻辑: + - 直接查询用户表的所有字段 + """ + sql = "SELECT * FROM users_info WHERE user_id = %s" + result = execute_query(sql, (user_id,)) + return result[0] if result else None + + +def get_subscription_template_by_id(template_id: int) -> dict: + """ + 根据模板ID获取订阅模板信息 + + 功能说明: + - 通过模板ID查询订阅模板详情 + + 参数说明: + - template_id: 模板ID + + 返回: + - 模板信息的字典,如果不存在则返回None + """ + sql = "SELECT * FROM subscription_templates WHERE id = %s" + result = execute_query(sql, (template_id,)) + return result[0] if result else None + + +def get_active_user_subscription(user_id: str, museum_id: int) -> dict: + """ + 获取用户在指定博物馆的有效订阅 + + 功能说明: + - 查询用户在特定博物馆的当前有效订阅 + - 有效订阅定义为: 已激活且未过期 + + 参数说明: + - user_id: 用户ID + - museum_id: 博物馆ID + + 返回: + - 订阅信息的字典,如果不存在则返回None + + 重要逻辑: + - 通过JOIN关联博物馆订阅表 + - 检查 is_active=1 和 end_date > NOW() + - 返回最新到期的订阅 + """ + sql = """ + SELECT us.* + FROM user_subscriptions us + JOIN museum_subscriptions ms ON us.museum_subscription_id = ms.id + WHERE us.user_id = %s + AND ms.museum_id = %s + AND us.is_active = 1 + AND us.end_date > NOW() + ORDER BY us.end_date DESC + LIMIT 1 + """ + result = execute_query(sql, (user_id, museum_id)) + return result[0] if result else None + + +def get_user_subscription_history(user_id: str) -> list: + """ + 获取用户的订阅历史记录 + + 功能说明: + - 查询用户的所有订阅记录(包括历史和当前) + - 返回完整的订阅详情,包含博物馆和模板信息 + + 参数说明: + - user_id: 用户ID + + 返回: + - 包含订阅历史记录的字典列表 + + 重要逻辑: + - 通过多层JOIN关联所有相关表 + - 包含博物馆名称、模板信息等 + - 按开始时间倒序排列,最新订阅在前 + """ + sql = """ + SELECT + us.*, + ms.price, + ms.sub_id, + st.name AS template_name, + st.description AS template_description, + st.validity_type, + mo.name AS museum_name + FROM user_subscriptions us + JOIN museum_subscriptions ms ON us.museum_subscription_id = ms.id + JOIN subscription_templates st ON ms.template_id = st.id + JOIN mesum_overview mo ON ms.museum_id = mo.id + WHERE us.user_id = %s + ORDER BY us.start_date DESC + """ + return execute_query(sql, (user_id,)) + +def get_user_subscription_by_order(order_id: str) -> dict: + """ + 根据订单ID获取用户订阅信息 + + 功能说明: + - 通过订单ID查询关联的用户订阅 + + 参数说明: + - order_id: 订单ID + + 返回: + - 订阅信息的字典,如果不存在则返回None + + 重要逻辑: + - 用于支付回调后验证订阅是否已创建 + """ + sql = "SELECT * FROM user_subscriptions WHERE order_id = %s" + result = execute_query(sql, (order_id,)) + return result[0] if result else None + + +def activate_free_subscription(user_id: str, museum_id: int) -> str: + """ + 激活免费订阅 + + 功能说明: + - 为用户在指定博物馆激活免费订阅 + - 创建订单记录和订阅记录 + + 参数说明: + - user_id: 用户ID + - museum_id: 博物馆ID + + 返回: + - 创建的订单ID + + 重要逻辑: + - 查找博物馆的免费订阅 + - 创建订单记录 (状态为activated) + - 创建订阅记录 (有效期为7天) + """ + # 1. 获取博物馆的免费订阅 + sql = """ + SELECT ms.id + FROM museum_subscriptions ms + JOIN subscription_templates st ON ms.template_id = st.id + WHERE ms.museum_id = %s + AND st.validity_type = 'free' + AND ms.is_active = 1 + LIMIT 1 + """ + result = execute_query(sql, (museum_id,)) + if not result: + raise ValueError("该博物馆没有可用的免费订阅") + + subscription_id = result[0]["id"] + + # 2. 创建免费订单 + order_id = f"FREE_{int(time.time())}" + create_order({ + "order_id": order_id, + "user_id": user_id, + "museum_subscription_id": subscription_id, + "amount": 0, + "status": "activated", + "create_date": datetime.now() + }) + + # 3. 创建用户订阅记录 (免费订阅有效期为7天) + start_date = datetime.now() + end_date = start_date + timedelta(days=7) + + create_user_subscription({ + "user_id": user_id, + "museum_subscription_id": subscription_id, + "order_id": order_id, + "start_date": start_date, + "end_date": end_date, + "is_active": True + }) + + # 4. 禁用同一博物馆的旧订阅 + deactivate_previous_subscriptions(user_id, subscription_id) + + return order_id + + +def activate_user_subscription( + user_id: str, + museum_subscription_id: str, + order_id: str +) -> bool: + """ + 激活用户订阅服务 + + 参数: + - user_id: 用户ID + - museum_subscription_id: 博物馆订阅套餐ID + - order_id: 关联的订单ID + + 返回: + - 激活成功返回True,失败返回False + + 主要逻辑: + 1. 获取博物馆订阅信息 + 2. 禁用同一博物馆的旧订阅 + 3. 计算订阅有效期 + 4. 创建用户订阅记录 + 5. 处理重复激活和并发请求 + """ + try: + # 1. 获取博物馆订阅信息 + museum_sub = get_museum_subscription_by_id(museum_subscription_id) + if not museum_sub: + logger.error(f"博物馆订阅不存在: {museum_subscription_id}") + return False + + # 2. 获取关联的订阅模板 + template = get_subscription_template_by_id(museum_sub["template_id"]) + if not template: + logger.error(f"订阅模板不存在: {museum_sub['template_id']}") + return False + + # 3. 禁用同一博物馆的旧订阅 + deactivated_count = deactivate_previous_subscriptions( + user_id=user_id, + museum_subscription_id=museum_subscription_id + ) + logger.info(f"已禁用{deactivated_count}个同一博物馆的旧订阅") + + # 4. 计算订阅有效期 + start_date = datetime.now() + end_date = calculate_subscription_expiry(start_date,template["validity_type"]) + + # 5. 检查是否已激活过(防止重复激活) + existing_sub = get_user_subscription_by_order(order_id) + if existing_sub: + logger.warning(f"订阅已激活过,跳过重复激活. Order: {order_id}") + return True + + # 6. 创建用户订阅记录 + subscription_data = { + "user_id": user_id, + "museum_subscription_id": museum_subscription_id, + "order_id": order_id, + "start_date": start_date, + "end_date": end_date, + "is_active": 1 + } + + create_user_subscription(subscription_data) + + logger.info(f"订阅激活成功. 用户: {user_id}, 套餐: {museum_subscription_id}, " + f"有效期: {start_date} 至 {end_date}") + return True + + except Exception as e: + logger.exception(f"激活订阅失败: {str(e)}") + return False + +def check_user_subscription(user_id: str, museum_id: int) -> dict: + """ + 检查用户是否拥有指定博物馆的有效订阅 + + 功能说明: + - 检查用户是否有指定博物馆的未过期激活订阅 + + 参数说明: + - user_id: 用户ID + - museum_id: 博物馆ID + + 返回: + - 订阅信息字典,如果没有则返回None + + 重要逻辑: + - 优先检查当前有效的订阅 + - 如果没有,检查免费订阅是否可用 + """ + # 1. 检查当前有效订阅 + active_sub = get_active_user_subscription(user_id, museum_id) + if active_sub: + return active_sub + + # 2. 检查是否有免费订阅可用 + # (这里可以扩展更多逻辑,如试用期检查等) + + return None + + + +def calculate_subscription_expiry(start_date: datetime, validity_type: str) -> datetime: + """ + 根据有效期类型计算到期日期 + + 参数: + - start_date: 订阅开始日期 + - validity_type: 有效期类型 (free, 1month, 1year, permanent) + + 返回: + - 到期日期 + """ + if validity_type == "free": + # 免费套餐通常有较短有效期(例如7天) + return start_date + timedelta(days=7) + elif validity_type == "1month": + # 下个月的同一天(自动处理月末情况) + return start_date + relativedelta(months=1) + elif validity_type == "1year": + # 下一年的同一天 + return start_date + relativedelta(years=1) + elif validity_type == "permanent": + # 永久有效设置为100年后 + return start_date + relativedelta(years=100) + else: + # 未知类型默认30天 + logger.warning(f"未知有效期类型: {validity_type}, 使用默认30天") + return start_date + timedelta(days=30) diff --git a/asr-monitor-test/app/login_service.py b/asr-monitor-test/app/login_service.py index bfd7f3c9..0598f003 100644 --- a/asr-monitor-test/app/login_service.py +++ b/asr-monitor-test/app/login_service.py @@ -1,12 +1,12 @@ from fastapi import WebSocket, APIRouter,WebSocketDisconnect,Request,Body,Query -from fastapi import FastAPI, UploadFile, File, Form, Header,Depends +from fastapi import FastAPI, UploadFile, File, Form, Header, Depends from fastapi.responses import StreamingResponse,JSONResponse from fastapi.security import OAuth2PasswordBearer from jose import JWTError, jwt import logging from fastapi import HTTPException from Crypto.Cipher import AES -import base64,uuid +import base64,uuid,asyncio import requests from datetime import datetime,timedelta from database import * @@ -17,10 +17,10 @@ logger = logging.getLogger("login") oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") # tokenUrl 对应登录接口路径 # 需要配置的参数(从环境变量获取) -WX_APPID = "wxed388cef83f109a3" # 小程序appid -WX_SECRET = "f687afd2c8fae49b4aed2e4a8dd76e6e" # 小程序密钥 +WX_APPID = "wx446813bfb3a6985a" #"wxed388cef83f109a3" # 小程序appid +WX_SECRET = "a7455fca777ad59ce96cc154d62f795f" #"f687afd2c8fae49b4aed2e4a8dd76e6e" # 小程序密钥 WX_API_URL = "https://api.weixin.qq.com/sns/jscode2session" -JWT_SECRET_KEY="3e5b8d7f1a9c2b6d4e0f1a9c2b6d4e0f1a9c2b6d4e0f1a9c2b6d4e0f1a9c2b6d" +JWT_SECRET_KEY = "3e5b8d7f1a9c2b6d4e0f1a9c2b6d4e0f1a9c2b6d4e0f1a9c2b6d4e0f1a9c2b6d" ALGORITHM = "HS256" # 伪数据库 @@ -45,7 +45,7 @@ def create_jwt(user_id: str) -> str: return jwt.encode(payload, JWT_SECRET_KEY, algorithm=ALGORITHM) -def decrypt_phone(encrypted_data: str, session_key: str, iv: str) -> dict: +def decrypt_data(encrypted_data: str, session_key: str, iv: str) -> dict: try: # Base64解码 @@ -65,15 +65,17 @@ def decrypt_phone(encrypted_data: str, session_key: str, iv: str) -> dict: # 解析JSON result = json.loads(decrypted.decode('utf-8')) - if 'purePhoneNumber' not in result: - raise ValueError("解密数据不包含手机号") - - return result['purePhoneNumber'] + logging.info(f"解密数据: {result}") + return result + except json.JSONDecodeError: + # 特定错误类型识别 + logging.info(f"解密过程失败: {str(e)}") + raise ValueError("SESSION_KEY_MISMATCH") except Exception as e: - raise ValueError(f"解密过程失败: {str(e)}") - except Exception as e: - raise ValueError(f"解密失败: {str(e)}") - + logging.info(f"解密过程失败: {str(e)}") + if "padding" in str(e).lower(): + raise ValueError("SESSION_KEY_EXPIRED") + raise async def get_wx_session(code: str): """ @@ -124,7 +126,6 @@ async def get_wx_session(code: str): status_code=401, detail="微信认证失败:缺少openid" ) - logging.info(f"wx_data {wx_data}") return wx_data @@ -140,24 +141,61 @@ async def wechat_login(request: Request): if not all(k in data for k in required_fields): raise HTTPException(400, "Missing required fields") code = data.get('code') - # 调用微信接口获取session信息 - try: - wx_data = await get_wx_session(code) - logging.info(f"wechat login wx_data={wx_data}") - except HTTPException as e: - raise e # 直接抛出已处理过的异常 + encrypted_data = data['encryptedData'] + iv = data['iv'] - # 伪解密手机号 - try: - #phone_number = "138" + str(hash(data["encryptedData"]))[-8:] - phone_number=decrypt_phone(data["encryptedData"],wx_data['session_key'],data["iv"]) - except: - raise HTTPException(400, "Decrypt failed") + logging.info(f"wechat login data={data}") + # 关键修改:增加重试机制 + max_retries = 2 + for attempt in range(max_retries): + try: + # 每次尝试都重新获取 session_key + # 微信小程序登录 code 具有一次性特征(有效期约5分钟) + # 添加冷启动保护延迟 + await asyncio.sleep(0.5) # 关键延迟 - logging.info(f"decrypt_phone return {phone_number}") + wx_data = await get_wx_session(code) + session_key = wx_data["session_key"] + openid = wx_data["openid"] + logging.info(f"get_wx_session return {wx_data}") + # 解密数据 + try: + result = decrypt_data(encrypted_data, session_key, iv) + except ValueError as e: + # 特定错误处理 + if "SESSION_KEY_" in str(e): + raise HTTPException(418, "SESSION_KEY_INVALID") + raise + + if 'purePhoneNumber' not in result: + logging.warning("解密数据不包含手机号") + phone_number = None + else: + phone_number = result['purePhoneNumber'] + break # 成功则跳出循环 + + except HTTPException as e: + if attempt < max_retries - 1: + # 特定错误时刷新 code + if "invalid session_key" in str(e).lower(): + logging.warning("Session key 过期,尝试刷新") + # 触发前端重新获取 code + raise HTTPException(401, "SESSION_KEY_EXPIRED") + else: + logging.error(f"尝试 {attempt + 1} 失败: {str(e)}") + await asyncio.sleep(1) # 短暂等待后重试 + else: + logging.error(f"最终解密失败: {str(e)}") + raise HTTPException(400, "Decrypt failed") + except Exception as e: + logging.error(f"解密异常: {str(e)}") + if attempt == max_retries - 1: + raise HTTPException(400, "Decrypt failed") + + logging.info(f"decrypt_data return {phone_number}") # ========== 数据库操作开始 ========== # 使用数据库查询替代内存查询 - db_users = get_users(phone=phone_number) + db_users = get_users(openid=openid) user = db_users[0] if db_users else None # 用户不存在时创建新用户 @@ -184,20 +222,8 @@ async def wechat_login(request: Request): } updated_user = update_user(user["user_id"], update_data) # ========== 数据库操作结束 ========== - """ - # 查找或创建用户 - user = next((u for u in fake_db["users"] if u["phone"] == phone_number), None) - if not user: - user = { - "id": str(uuid.uuid4()), - "openid": wx_data["openid"], - "phone": phone_number, - "museums": [1, 2, 3] # 伪权限数据 - } - fake_db["users"].append(user) - logging.info(f"login return {user}") - """ + logging.info(f"login return {user}") # 生成token return JSONResponse({ "token": create_jwt(user["user_id"]), @@ -302,32 +328,6 @@ async def optional_current_user(token: str = Depends(oauth2_scheme)): except (JWTError, StopIteration): return None - -async def get_current_user(token: str = Depends(oauth2_scheme)) : - # 身份验证核心逻辑 - credentials_exception = HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="无法验证凭证", - headers={"WWW-Authenticate": "Bearer"}, - ) - - try: - # 1. 解码并验证JWT - payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - user_id: str = payload.get("sub") - if user_id is None: - raise credentials_exception - - # 2. 从数据库获取完整用户信息 - user = get_user_by_id(user_id) - if user is None: - raise credentials_exception - - return user - - except JWTError: - raise credentials_exception - @login_router.get("/verify") async def verify_token(user: dict = Depends(optional_current_user)): """ diff --git a/asr-monitor-test/app/main.py b/asr-monitor-test/app/main.py index 49824f91..79d67fa7 100644 --- a/asr-monitor-test/app/main.py +++ b/asr-monitor-test/app/main.py @@ -1,3 +1,4 @@ +import os from fastapi import FastAPI from fastapi.staticfiles import StaticFiles from fastapi.middleware.cors import CORSMiddleware @@ -6,10 +7,10 @@ from jose import JWTError, jwt from datetime import datetime, timedelta import json from contextlib import asynccontextmanager -from app.asr_service import asr_router -from app.monitor_service import monitor_router -from app.tts_service import tts_router -from app.login_service import login_router + +from dotenv import load_dotenv + + import uvicorn @asynccontextmanager @@ -25,6 +26,16 @@ async def lifespan(app: FastAPI): print(" Service Stopped Cleanly") print("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") +# 加载 .env 文件中的环境变量 +load_dotenv() # 默认加载项目根目录的 .env 文件 + +from app.asr_service import asr_router +from app.monitor_service import monitor_router +from app.tts_service import tts_router +from app.login_service import login_router +from app.chat_service import chat_router +from app.payment_service import payment_router + # 创建应用实例 app = FastAPI(lifespan=lifespan) @@ -35,6 +46,7 @@ app.add_middleware( allow_credentials=True, # 是否允许发送 cookies allow_methods=["*"], # 允许所有方法,也可以指定具体的方法,例如 ["GET", "POST"] allow_headers=["*"], # 允许所有头信息,也可以指定具体的头信息 + expose_headers=["Content-Range", "Content-Length"] # 关键添加 ) # 挂载子路由 @@ -42,6 +54,8 @@ app.include_router(asr_router, prefix="/asr") app.include_router(monitor_router, prefix="/monitor") app.include_router(tts_router, prefix="/tts") app.include_router(login_router, prefix="/auth") +app.include_router(chat_router, prefix="/chat") +app.include_router(payment_router, prefix="/payment") # 挂载静态文件(可选) # app.mount("/static", StaticFiles(directory="static"), name="static") diff --git a/asr-monitor-test/app/payment_service.py b/asr-monitor-test/app/payment_service.py new file mode 100644 index 00000000..6f42c295 --- /dev/null +++ b/asr-monitor-test/app/payment_service.py @@ -0,0 +1,1061 @@ +from fastapi import APIRouter, Depends, HTTPException, status, Request, Response +from fastapi.responses import StreamingResponse, JSONResponse +from fastapi.security import OAuth2PasswordBearer +import hashlib +import random +import string +import xml.etree.ElementTree as ET +import requests +import time +from datetime import datetime, timedelta, date +from decimal import Decimal +from uuid import UUID +import json +import logging +from database import * +from jose import JWTError, jwt +from database import * +import base64 +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import padding +from cryptography.hazmat.primitives.hashes import SHA256 +from cryptography.hazmat.backends import default_backend +import httpx + +class CustomJSONResponse(JSONResponse): + """ + 自定义 JSON 响应类,处理特殊类型: + - datetime: 转换为 ISO 8601 字符串 + - date: 转换为 ISO 8601 字符串 + - Decimal: 转换为 float + """ + + def render(self, content: any) -> bytes: + """ + 重写渲染方法,使用自定义编码器 + """ + class EnhancedJSONEncoder(json.JSONEncoder): + def default(self, obj): + """ + 增强型 JSON 编码器,处理多种特殊类型: + - datetime: 转换为 ISO 8601 字符串 + - date: 转换为 ISO 8601 字符串 + - time: 转换为 ISO 8601 字符串 + - Decimal: 转换为 float + - UUID: 转换为字符串 + - numpy 类型: 转换为 Python 原生类型 + """ + # 处理日期时间类型 + if isinstance(obj, datetime): + return obj.isoformat() + + if isinstance(obj, date): + return obj.isoformat() + + # 处理 Decimal 类型 + if isinstance(obj, Decimal): + return float(obj) + + # 处理 UUID 类型 + if isinstance(obj, UUID): + return str(obj) + """ + # 处理 numpy 类型 + if isinstance(obj, np.integer): + return int(obj) + if isinstance(obj, np.floating): + return float(obj) + if isinstance(obj, np.ndarray): + return obj.tolist() + """ + # 处理其他自定义类型 + if hasattr(obj, '__json__'): + return obj.__json__() + + # 默认处理 + return super().default(obj) + + return json.dumps( + content, + ensure_ascii=False, + allow_nan=False, + indent=None, + separators=(",", ":"), + cls=EnhancedJSONEncoder + ).encode("utf-8") + + +payment_router = APIRouter() +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") +logger = logging.getLogger("payment") + +# 微信支付配置 +WX_APPID = "wx446813bfb3a6985a" +WX_MCH_ID = "1721301006" +WX_PAY_KEY = "7xK9pR2qY5vN3zW8bL1cD4fG6hJ7mQ0t" +WX_PAY_KEY_V3 = "7xK9pR2qY5vN3zW8bL1cD4fG6hJ7mQ0t" +WX_MCH_CERT_SERIAL_NO = "6292D0A4D7B092E361D6DD22C2FD7831479D48D9" # 证书序列号 v3使用 +WX_PAY_NOTIFY_URL = "https://ragflow.szzysztech.com/apitest2/payment/wx_notify" +JWT_SECRET_KEY = "3e5b8d7f1a9c2b6d4e0f1a9c2b6d4e0f1a9c2b6d4e0f1a9c2b6d4e0f1a9c2b6d" +ALGORITHM = "HS256" +WX_PLATFORM_CERTS = {} +# 加载商户私钥 +def load_private_key(): + """ + 从PEM格式文件加载商户私钥 + 假设私钥文件名为 apiclient_key.pem 并位于项目根目录 + """ + try: + # 从文件加载 - 实际部署时建议使用环境变量或安全存储 + key_path = "./payment_cert/apiclient_key.pem" + with open(key_path, "rb") as key_file: + private_key = serialization.load_pem_private_key( + key_file.read(), + password=None, + backend=default_backend() + ) + return private_key + except Exception as e: + logging.error(f"加载商户私钥失败: {str(e)}") + # 备用方案:从环境变量加载 + key_str = os.getenv("WX_MCH_PRIVATE_KEY") + if key_str: + return serialization.load_pem_private_key( + key_str.encode(), + password=None, + backend=default_backend() + ) + raise Exception("无法加载微信支付商户私钥") + +# 全局加载一次私钥 +try: + MERCHANT_PRIVATE_KEY = load_private_key() +except Exception as e: + logging.error(f"初始化商户私钥失败: {str(e)}") + MERCHANT_PRIVATE_KEY = None + + +def get_wx_platform_public_key(serial_no: str): + """获取微信平台公钥""" + if serial_no in WX_PLATFORM_CERTS: + return WX_PLATFORM_CERTS[serial_no] + + # 实际应用中应从微信API获取证书,这里简化处理 + # 建议实现自动下载证书的机制 + cert_path = "./payment_cert/apiclient_key.pem" + try: + with open(cert_path, "rb") as cert_file: + public_key = serialization.load_pem_public_key( + cert_file.read(), + backend=default_backend() + ) + WX_PLATFORM_CERTS[serial_no] = public_key + return public_key + except Exception as e: + logger.error(f"加载平台证书失败: {str(e)}") + return None + + +async def get_current_user(token: str = Depends(oauth2_scheme)): + """ + 可选用户依赖项(不抛出401错误) + 返回: 用户对象 或 None + """ + try: + payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=[ALGORITHM]) + user_id = payload.get("sub") + return get_user_by_id(user_id) + except (JWTError, StopIteration): + return None + + +@payment_router.post("/create_order") +async def create_order_api( + request: Request, + current_user: dict = Depends(get_current_user) +): + if not current_user: + raise HTTPException(status_code=401, detail="请先登录") + + data = await request.json() + museum_id = data.get("museum_id") + sub_id = data.get("sub_id") # 参数名改为 subscription_id + + # 获取博物馆订阅信息 + museum_subscription = get_museum_subscription_by_id(sub_id) + if not museum_subscription: + raise HTTPException(status_code=404, detail="订阅不存在") + + # 免费订阅直接激活 + if museum_subscription["price"] == 0: + order_id = f"FREE_{int(time.time())}" + + # 创建订单记录 - 使用数据库辅助函数 + create_order({ + "order_id": order_id, + "user_id": current_user["user_id"], + "museum_subscription_id": subscription_id, + "amount": 0, + "status": "activated", + "create_date": datetime.now() + }) + + # 激活订阅 - 使用数据库辅助函数 + activate_user_subscription( + user_id=current_user["user_id"], + museum_subscription_id=sub_id, + order_id=order_id + ) + + return JSONResponse({ + "code": 0, + "msg": "订阅激活成功", + "data": {"order_id": order_id} + }) + + # 付费订阅创建待支付订单 + order_id = f"hxb_{int(time.time())}{random.randint(1000, 9999)}" + + # 创建订单记录 - 使用数据库辅助函数 + create_order({ + "order_id": order_id, + "user_id": current_user["user_id"], + "museum_subscription_id": sub_id, + "amount": museum_subscription["price"], + "status": "created", + "create_date": datetime.now() + }) + + return JSONResponse({ + "code": 0, + "msg": "订单创建成功", + "data": { + "order_id": order_id, + "amount": float(museum_subscription["price"]) + } + }) + + +# 获取支付参数 +@payment_router.post("/prepay") +async def get_payment_params( + request: Request, + current_user: dict = Depends(get_current_user) +): + try: + data = await request.json() + order_id = data.get("order_id") + version = data.get("version", "v2") # 默认为 v3 + + if not order_id: + raise HTTPException(status_code=400, detail="缺少order_id参数") + + # 获取订单信息 - 使用数据库辅助函数 + order = get_order_by_id(order_id = order_id) + if not order: + raise HTTPException(status_code=404, detail="订单不存在") + + if order["user_id"] != current_user["user_id"]: + raise HTTPException(status_code=403, detail="无权访问此订单") + + if order["status"] != "created": + raise HTTPException(status_code=400, detail="订单状态异常") + + # 获取用户openid + user = get_user_by_id(current_user["user_id"]) + if not user or not user.get("openid"): + raise HTTPException(status_code=400, detail="用户信息不完整") + + # 获取订阅信息以获取产品名称 + museum_subscription = get_museum_subscription_by_id(order["museum_subscription_id"]) + if not museum_subscription: + raise HTTPException(status_code=404, detail="关联订阅不存在") + + # 获取模板名称 + template = get_subscription_template_by_id(museum_subscription["template_id"]) + if not template: + raise HTTPException(status_code=404, detail="订阅模板不存在") + + product_name = f"博物馆讲解服务-{template['name']}" + amount_in_cents = int(order["amount"] * 100) # 转换为分 + + # 根据版本调用不同的支付接口 + if version.lower() == "v2": + # V2 支付 + logging.info(f"使用微信支付V2接口") + prepay_params = await generate_wx_prepay_params_v2( + order_id=order["order_id"], + total_fee=amount_in_cents, + openid=user["openid"], + body=product_name + ) + + # 生成支付签名 + pay_sign = generate_pay_sign( + appid=WX_APPID, + timestamp=prepay_params["timestamp"], + noncestr=prepay_params["noncestr"], + prepay_id=prepay_params["prepay_id"] + ) + + response_data = { + "appid": prepay_params["appId"], + "timeStamp": prepay_params["timestamp"], + "nonceStr": prepay_params["noncestr"], + "package": f"prepay_id={prepay_params['prepay_id']}", + "signType": "MD5", + "paySign": pay_sign + } + + else: # 默认为 v3 + # V3 支付 + logging.info(f"使用微信支付V3接口") + prepay_params = await generate_wx_prepay_params_v3( + order_id=order["order_id"], + total_fee=amount_in_cents, + openid=user["openid"], + body=product_name + ) + + # V3 已返回完整的支付参数 + response_data = { + "appId": prepay_params["appId"], + "timeStamp": prepay_params["timeStamp"], + "nonceStr": prepay_params["nonceStr"], + "package": prepay_params["package"], + "signType": "RSA", # V3 使用 RSA 签名 + "paySign": prepay_params["paySign"] + } + + return JSONResponse({ + "code": 0, + "msg": "success", + "data":response_data + }) + + except Exception as e: + logger.error(f"获取支付参数失败: {str(e)}", exc_info=True) + raise HTTPException(status_code=500, detail=f"获取支付参数失败: {str(e)}") + +# 微信支付回调处理(兼容V2和V3) +@payment_router.post("/wx_notify") +async def wx_payment_notify(request: Request): + headers = request.headers + body = await request.body() + + # 判断支付版本(V3有特定头信息) + if "Wechatpay-Signature" in headers: + return await handle_v3_notify(headers, body) + else: + return await handle_v2_notify(body) + + +async def handle_v3_notify(headers: dict, body: bytes): + """处理V3回调""" + # 1. 验证签名 + if not verify_wx_signature_v3(headers, body): + logger.warning("V3签名验证失败") + return generate_json_response("FAIL", "签名验证失败") + + try: + # 2. 解析JSON数据 + data = json.loads(body) + resource = data.get("resource", {}) + + # 3. 解密资源数据 + decrypted_data = decrypt_v3_resource(resource) + if not decrypted_data: + logger.error("V3回调数据解密失败") + return generate_json_response("FAIL", "数据解密失败") + + logger.info(f"V3解密数据: {json.dumps(decrypted_data, indent=2)}") + + # 4. 检查支付状态 + if decrypted_data.get("trade_state") != "SUCCESS": + logger.warning(f"支付未成功: {decrypted_data.get('trade_state_desc')}") + return generate_json_response("FAIL", "支付未成功") + + # 5. 提取订单信息 + order_id = decrypted_data.get("out_trade_no") + transaction_id = decrypted_data.get("transaction_id") + total_fee = float(decrypted_data["amount"]["payer_total"]) / 100 # 转换为元 + + # 6. 处理支付成功逻辑 + success, msg = await process_payment_success(order_id, transaction_id, total_fee) + if not success: + return generate_json_response("FAIL", msg) + + return generate_json_response("SUCCESS", "OK") + + except Exception as e: + logger.exception(f"V3回调处理异常: {str(e)}") + return generate_json_response("FAIL", f"处理失败: {str(e)}") + + +async def handle_v2_notify(body: bytes): + """处理V2回调""" + try: + logging.info(f"V2 wx_notify回调数据: {body}") + data = xml_to_dict(body.decode('utf-8')) + + # 1. 验证签名 + if not verify_wx_signature_v2(data, WX_PAY_KEY): + logger.warning("V2签名验证失败") + return generate_xml_response("FAIL", "签名验证失败") + + # 2. 检查支付状态 + if data.get("return_code") != "SUCCESS": + logger.warning(f"支付失败: {data.get('return_msg')}") + return generate_xml_response("FAIL", "支付失败") + + # 3. 提取订单信息 + order_id = data.get("out_trade_no") + transaction_id = data.get("transaction_id") + total_fee = float(data.get("total_fee", 0)) / 100 # 转换为元 + + # 4. 处理支付成功逻辑 + success, msg = await process_payment_success(order_id, transaction_id, total_fee) + if not success: + return generate_xml_response("FAIL", msg) + + return generate_xml_response("SUCCESS", "OK") + + except Exception as e: + logger.exception(f"V2回调处理异常: {str(e)}") + return generate_xml_response("FAIL", f"处理失败: {str(e)}") + + +@payment_router.post("/check_payment") +async def check_payment_status( + request: Request, + current_user: dict = Depends(get_current_user) +): + if not current_user: + raise HTTPException(status_code=401, detail="请先登录") + + data = await request.json() + order_id = data.get("order_id") + + if not order_id: + raise HTTPException(status_code=400, detail="缺少订单号") + + # 查询订单状态 + order = get_order_by_id(order_id = order_id) + if not order: + return JSONResponse({"code": 404, "msg": "订单不存在"}) + + # 检查订单是否属于当前用户 + if order["user_id"] != current_user["user_id"]: + return JSONResponse({"code": 403, "msg": "无权访问此订单"}) + + # 返回支付状态 + return JSONResponse({ + "code": 0, + "msg": "success", + "data": { + "order_id": order_id, + "paid": order["status"] in ["paid", "activated"], + "status": order["status"] + } + }) + +@payment_router.get("/get_subscriptions/{museum_id}") +async def get_museum_subscriptions_by_museum_id( + museum_id: str, + request: Request, + current_user: dict = Depends(get_current_user) +): + result = get_museum_subscriptions_by_museum(museum_id) + return CustomJSONResponse(result) + +@payment_router.get("/get_order_list") +async def get_order_list( + request: Request, + current_user: dict = Depends(get_current_user) +): + result = get_order_by_id(user_id = current_user["user_id"],combined=True) + return CustomJSONResponse({ + "code": 0, + "msg": "success", + "data": result}) + +@payment_router.get("/get_order_detail/{order_id}") +async def get_order_detial( + order_id: str, + request: Request, + current_user: dict = Depends(get_current_user) +): + result = get_order_by_id(order_id = order_id,combined=True) + return CustomJSONResponse({ + "code": 0, + "msg": "success", + "data": result}) +# --- 支付工具函数 --- + +async def generate_wx_prepay_params_v2(order_id: str, total_fee: int, openid: str, body: str): + """生成微信预支付参数 - 修复签名错误""" + try: + # 准备请求参数 + nonce_str = generate_nonce_str(32) + params = { + "appid": WX_APPID, + "mch_id": WX_MCH_ID, + "nonce_str": nonce_str, + "body": body[:128], # 商品描述 + "out_trade_no": order_id, + "total_fee": str(total_fee), # 确保字符串格式 + "spbill_create_ip": "127.0.0.1", # 终端IP + "notify_url": WX_PAY_NOTIFY_URL, + "trade_type": "JSAPI", + "openid": openid + } + + params1={'appid': 'wx446813bfb3a6985a', + 'mch_id': '1721301006', + 'nonce_str': '7qi8x7npD2S5JCD8LPV2mHEX5FgeWa1U', + 'body': '博物馆讲解服务-1个月有效', + 'out_trade_no': 'PAY_17515540373111', + 'total_fee': '500', + 'spbill_create_ip': '1.13.185.116', + 'notify_url': 'https://yourdomain.com/api/payment/wx_notify', + 'trade_type': 'JSAPI', + 'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE'} + + logging.info(f"generate_wx_prepay_params--0 {params}") + + # 生成签名 - 添加详细日志 + sign_str_before = '&'.join([f"{k}={v}" for k, v in sorted(params.items())]) + + params["sign"] = generate_sign_v2(params, WX_PAY_KEY) + logging.info(f"Generated sign: {params['sign']}") + + # 转换为XML + xml_data = dict_to_xml(params) + + + # 调用微信统一下单接口 + try: + response = requests.post( + "https://api.mch.weixin.qq.com/pay/unifiedorder", + data=xml_data.encode('utf-8'), + headers={"Content-Type": "application/xml"}, + timeout=10 + ) + #logging.info(f"微信支付接口响应状态码: {response.status_code}") + #logging.info(f"微信支付接口响应内容: {response.text}") + except Timeout: + logging.error("微信支付接口请求超时") + raise Exception("微信支付接口请求超时") + except ConnectionError: + logging.error("无法连接到微信支付服务器") + raise Exception("网络连接失败,请检查网络设置") + except RequestException as req_err: + logging.error(f"微信支付请求异常: {str(req_err)}") + raise Exception(f"微信支付请求失败: {str(req_err)}") + + # 检查HTTP响应状态 + if response.status_code != 200: + logging.error(f"微信支付接口返回错误状态码: {response.status_code}") + raise Exception(f"微信支付接口错误: HTTP {response.status_code}") + + # 解析响应 + response.encoding = 'utf-8' + try: + response_data = xml_to_dict(response.text) + except Exception as parse_err: + logging.error(f"解析微信响应XML失败: {parse_err}") + raise Exception("微信支付返回数据格式错误") + + logging.info(f"generate_wx_prepay_params--3 响应数据: {response_data}") + + # 检查返回结果 + if response_data.get("return_code") != "SUCCESS": + error_msg = response_data.get("return_msg", "未知错误") + # 尝试解码可能的乱码 + try: + error_msg = error_msg.encode('latin1').decode('utf-8') + except: + pass + logging.error(f"微信支付下单失败(return_code): {error_msg}") + raise Exception(f"微信支付下单失败: {error_msg}") + + if response_data.get("result_code") != "SUCCESS": + error_code = response_data.get("err_code", "") + error_msg = response_data.get("err_code_des", "未知错误") + # 尝试解码可能的乱码 + try: + error_msg = error_msg.encode('latin1').decode('utf-8') + except: + pass + logging.error(f"微信支付下单失败(result_code): [{error_code}] {error_msg}") + raise Exception(f"微信支付下单失败: {error_msg} (错误代码: {error_code})") + + # 检查必须的预支付ID + prepay_id = response_data.get("prepay_id") + if not prepay_id: + logging.error("微信支付返回缺少prepay_id") + raise Exception("微信支付返回数据不完整") + + # 返回预支付参数 + return { + "appId": params["appid"], + "prepay_id": prepay_id, + "timestamp": str(int(time.time())), + "noncestr": nonce_str + } + + except Exception as e: + logging.error(f"generate_wx_prepay_params 发生异常: {str(e)}", exc_info=True) + raise + + +def generate_sign_v2(params: dict, api_key: str) -> str: + """生成微信支付签名 - 修复版""" + # 1. 参数按ASCII码排序 + sorted_params = sorted(params.items(), key=lambda x: x[0]) + + # 2. 拼接成URL参数字符串(仅排除None和sign字段) + sign_str = '&'.join([f"{k}={v}" for k, v in sorted_params + if k != "sign" and v is not None]) # 移除非空检查 + + # 3. 拼接API密钥 + sign_str += f"&key={api_key}" + + # 记录详细的签名字符串(调试用) + # logging.info(f"Sign string: {sign_str}") + #appid=wx446813bfb3a6985a&body=博物馆讲解服务-1个月有效&mch_id=1721301006&nonce_str=7qi8x7npD2S5JCD8LPV2mHEX5FgeWa1U¬ify_url=https://yourdomain.com/api/payment/wx_notify&openid=obKSz7V6a-avAF-vtQrnk_rnuSGE&out_trade_no=PAY_17515540373111&spbill_create_ip=127.0.0.1&total_fee=500&trade_type=JSAPI + + # 4. MD5加密并转为大写 + md5 = hashlib.md5() + md5.update(sign_str.encode('utf-8')) + return md5.hexdigest().upper() + + +def generate_pay_sign(appid: str, timestamp: str, noncestr: str, prepay_id: str) -> str: + """生成支付签名 (用于小程序端)""" + params = { + "appId": appid, + "timeStamp": timestamp, + "nonceStr": noncestr, + "package": f"prepay_id={prepay_id}", + "signType": "MD5" + } + return generate_sign_v2(params, WX_PAY_KEY) + + +def generate_nonce_str(length=32): + """生成随机字符串""" + return ''.join(random.choices(string.ascii_letters + string.digits, k=length)) + + +def dict_to_xml(data: dict) -> str: + """字典转XML - 修复特殊字符处理""" + xml = "" + for key, value in data.items(): + # 确保所有值都转为字符串 + str_value = str(value) + # 对非数字类型使用CDATA包裹 + if not str_value.isdigit(): + xml += f"<{key}>" + else: + xml += f"<{key}>{str_value}" + xml += "" + return xml + + +def xml_to_dict(xml_str: str) -> dict: + """XML转字典 - 增强错误处理""" + try: + root = ET.fromstring(xml_str) + result = {} + for child in root: + result[child.tag] = child.text + return result + except ET.ParseError as e: + logging.error(f"XML解析失败: {e}") + return {} + + +def verify_wx_signature_v2(data: dict, api_key: str) -> bool: + """验证微信签名""" + if "sign" not in data: + return False + + sign = data.pop("sign") + calculated_sign = generate_sign_v2(data, api_key) + return sign == calculated_sign + + +def generate_xml_response(return_code: str, return_msg: str) -> Response: + """生成支付回调XML响应""" + response_data = { + "return_code": return_code, + "return_msg": return_msg + } + xml_content = dict_to_xml(response_data) + return Response( + content=xml_content, + media_type="application/xml", + headers={"Cache-Control": "no-cache"} + ) + + +def generate_json_response(code: str, message: str) -> JSONResponse: + return JSONResponse( + content={"code": code, "message": message}, + status_code=200 + ) + + +# 新增的V3相关函数 +def verify_wx_signature_v3(headers: dict, body: bytes) -> bool: + """验证V3回调签名""" + signature = headers.get("Wechatpay-Signature") + timestamp = headers.get("Wechatpay-Timestamp") + nonce = headers.get("Wechatpay-Nonce") + serial_no = headers.get("Wechatpay-Serial") + + if not all([signature, timestamp, nonce, serial_no]): + logger.error("V3回调缺少必要头信息") + return False + + # 构造验签名串 + sign_str = f"{timestamp}\n{nonce}\n{body.decode('utf-8')}\n" + logger.debug(f"V3签名串: {sign_str}") + + try: + # 获取微信平台公钥 + public_key = get_wx_platform_public_key(serial_no) + if not public_key: + logger.error("无法获取微信平台公钥") + return False + + # 验证签名 + public_key.verify( + base64.b64decode(signature), + sign_str.encode('utf-8'), + padding.PKCS1v15(), + SHA256() + ) + return True + except Exception as e: + logger.error(f"V3签名验证失败: {str(e)}") + return False + + +def decrypt_v3_resource(resource: dict) -> dict: + """解密V3回调资源数据""" + try: + ciphertext = resource.get('ciphertext') + nonce = resource.get('nonce') + associated_data = resource.get('associated_data', '') + + if not all([ciphertext, nonce]): + logger.error("V3回调缺少解密参数") + return {} + + # 使用AES-GCM解密 + key_bytes = WX_PAY_KEY_V3.encode('utf-8') + ciphertext_bytes = base64.b64decode(ciphertext) + + # 创建解密器 + cipher = Cipher( + algorithms.AES(key_bytes), + modes.GCM(nonce.encode('utf-8'), min_tag_length=16), + backend=default_backend() + ) + decryptor = cipher.decryptor() + + # 添加附加认证数据 + if associated_data: + decryptor.authenticate_additional_data(associated_data.encode('utf-8')) + + # 解密数据 + decrypted = decryptor.update(ciphertext_bytes) + decryptor.finalize() + return json.loads(decrypted) + except Exception as e: + logger.error(f"V3解密失败: {str(e)}") + return {} + + +async def process_payment_success(order_id: str, transaction_id: str, total_fee: float): + """处理支付成功逻辑""" + # 更新订单状态 + update_order(order_id, { + "status": "paid", + "transaction_id": transaction_id, + "pay_time": datetime.now(), + "amount": total_fee + }) + logging.info(f"支付成功,更新订单状态 {order_id} {transaction_id} {total_fee}") + + # 获取订单信息 + order = get_order_by_id(order_id = order_id) + if not order: + logger.error(f"订单不存在: {order_id}") + return False, "订单不存在" + + # 激活订阅 + success = activate_user_subscription( + user_id=order["user_id"], + museum_subscription_id=order["museum_subscription_id"], + order_id=order_id + ) + if success: + # 更新订单状态为已激活 + update_order(order_id, {"status": "activated"}) + + return True, "OK" + + +# 套餐激活函数 +def activate_user_product(user_id: str, product_id: int, museum_id: int, order_id: str): + product = get_product_by_id(product_id) + if not product: + return + + # 计算有效期 + start_time = datetime.now() + end_time = calculate_expiry(start_time, product["validity_type"]) + + # 创建用户套餐记录 + create_user_product({ + "user_id": user_id, + "product_id": product_id, + "museum_id": museum_id, + "order_id": order_id, + "start_time": start_time, + "end_time": end_time, + "is_active": True + }) + + # 禁用同一博物馆的旧套餐 + deactivate_previous_products(user_id, museum_id) + + +def calculate_expiry(start_time: datetime, validity_type: str) -> datetime: + if validity_type == "free": + return start_time + timedelta(days=7) # 免费套餐7天 + elif validity_type == "1month": + return start_time + timedelta(days=30) + elif validity_type == "1year": + return start_time + timedelta(days=365) + else: # permanent + return datetime(2999, 12, 31) + + +async def generate_wx_prepay_params_v3(order_id: str, total_fee: int, openid: str, body: str): + """微信支付v3统一下单""" + if MERCHANT_PRIVATE_KEY is None: + raise Exception("商户私钥未初始化,无法进行签名") + + try: + # 确保序列号格式正确 + cert_serial_no = WX_MCH_CERT_SERIAL_NO + if not cert_serial_no: + raise Exception("商户证书序列号格式错误") + + # 1. 构造请求参数 + nonce_str = generate_nonce_str(32) + params = { + "appid": WX_APPID, + "mchid": WX_MCH_ID, + "description": body[:128], + "out_trade_no": order_id, + "notify_url": WX_PAY_NOTIFY_URL, + "amount": { + "total": total_fee, # 单位:分 + "currency": "CNY" + }, + "payer": { + "openid": openid + }, + "scene_info": { + "payer_client_ip": "1.13.185.116", + "device_id": "013467007045764", + "store_info": { + "id": "0005", + "name": "深圳博物馆", + "area_code": "440305", + "address": "广东省深圳市南山区科技中一道10000号" + } + }, + } + + # 2. 生成签名 + timestamp = str(int(time.time())) # 确保是10位整数 + method = "POST" + url_path = "/v3/pay/transactions/jsapi" + body_str = json.dumps(params, separators=(',', ':')) # 无空格JSON + + # 记录请求参数 + logging.info(f"V3统一下单参数: {json.dumps(params, indent=2)}") + logging.info(f"V3请求体: {body_str}") + + signature = generate_v3_sign(method, url_path, timestamp, nonce_str, body_str) + + # 3. 构造认证头 + auth_header = f'WECHATPAY2-SHA256-RSA2048 ' \ + f'mchid="{WX_MCH_ID}",' \ + f'nonce_str="{nonce_str}",' \ + f'signature="{signature}",' \ + f'timestamp="{timestamp}",' \ + f'serial_no="{cert_serial_no}"' + + #logging.info(f"V3 Authorization头: {auth_header}") + + # 4. 发送请求 - 使用content确保body完全一致 + async with httpx.AsyncClient() as client: + response = await client.post( + "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi", + headers={ + "Authorization": auth_header, + "Content-Type": "application/json", + "Accept": "application/json", + "User-Agent": "MyServer/1.0" + }, + content=body_str.encode('utf-8'), # 使用手动序列化的body + timeout=15 + ) + + # 5. 处理响应 + if response.status_code != 200: + error_detail = response.text + logging.error(f"V3接口错误: {response.status_code} {error_detail}") + + # 尝试解析错误信息 + try: + error_data = response.json() + error_code = error_data.get("code", "UNKNOWN") + error_msg = error_data.get("message", "未知错误") + raise Exception(f"微信支付V3接口错误: [{error_code}] {error_msg}") + except: + raise Exception(f"微信支付V3接口错误: HTTP {response.status_code}") + + resp_data = response.json() + logging.info(f"V3统一下单响应: {json.dumps(resp_data, indent=2)}") + + if "prepay_id" not in resp_data: + error_code = resp_data.get("code", "UNKNOWN") + error_msg = resp_data.get("message", "未知错误") + logging.error(f"V3下单失败: [{error_code}] {error_msg}") + raise Exception(f"微信支付V3下单失败: {error_msg}") + + # 6. 生成小程序支付参数 + prepay_id = resp_data["prepay_id"] + pay_sign = generate_v3_pay_sign(WX_APPID, timestamp, nonce_str, prepay_id) + + await query_order_by_out_trade_no(order_id) + return { + "appId": WX_APPID, + "timeStamp": timestamp, + "nonceStr": nonce_str, + "package": f"prepay_id={prepay_id}", + "signType": "RSA", + "paySign": pay_sign + } + + except httpx.TimeoutException: + logging.error("微信支付V3接口请求超时") + raise Exception("微信支付接口请求超时") + except httpx.RequestError as req_err: + logging.error(f"微信支付V3网络请求失败: {str(req_err)}") + raise Exception(f"网络连接失败: {str(req_err)}") + except Exception as e: + logging.error(f"V3统一下单异常: {str(e)}", exc_info=True) + raise + + +def generate_v3_sign(method: str, url: str, timestamp: str, nonce: str, body: str = "") -> str: + """生成V3请求签名""" + # 构造签名字符串 (必须严格按照此格式) + message = f"{method}\n{url}\n{timestamp}\n{nonce}\n{body}\n" + + # 记录签名串用于调试 + logging.info(f"V3签名串: {message}") + + # 使用私钥签名 + signature = MERCHANT_PRIVATE_KEY.sign( + message.encode('utf-8'), + padding.PKCS1v15(), + SHA256() + ) + return base64.b64encode(signature).decode('utf-8') + + +def generate_v3_pay_sign(appid: str, timestamp: str, nonce_str: str, prepay_id: str) -> str: + """生成小程序支付签名(V3)""" + # 构造签名字符串 (必须严格按照此格式) + sign_str = f"{appid}\n{timestamp}\n{nonce_str}\nprepay_id={prepay_id}\n" + + # 记录签名串用于调试 + logging.info(f"V3支付签名串: {sign_str}") + + # 使用私钥签名 + signature = MERCHANT_PRIVATE_KEY.sign( + sign_str.encode('utf-8'), + padding.PKCS1v15(), + SHA256() + ) + return base64.b64encode(signature).decode('utf-8') + + +async def query_order_by_out_trade_no(out_trade_no: str): + """通过商户订单号查询订单状态""" + try: + # 准备请求参数 + nonce_str = generate_nonce_str(32) + timestamp = str(int(time.time())) + url_path = f"/v3/pay/transactions/out-trade-no/{out_trade_no}?mchid={WX_MCH_ID}" + + # 生成签名 + signature = generate_v3_sign("GET", url_path, timestamp, nonce_str) + + # 构造认证头 + auth_header = f'WECHATPAY2-SHA256-RSA2048 ' \ + f'mchid="{WX_MCH_ID}",' \ + f'nonce_str="{nonce_str}",' \ + f'signature="{signature}",' \ + f'timestamp="{timestamp}",' \ + f'serial_no="{WX_MCH_CERT_SERIAL_NO}"' + + # 发送请求 + async with httpx.AsyncClient() as client: + response = await client.get( + f"https://api.mch.weixin.qq.com{url_path}", + headers={ + "Authorization": auth_header, + "Accept": "application/json", + "User-Agent": "MyServer/1.0" + }, + timeout=10 + ) + + # 处理响应 + if response.status_code != 200: + error_detail = response.text + logging.error(f"订单查询失败: {response.status_code} {error_detail}") + return None + else: + logging.info(f"订单查询成功: {response.text}") + return response.json() + + except Exception as e: + logging.error(f"订单查询异常: {str(e)}", exc_info=True) + return None +""" +需要在 database.py 中实现以下函数: + +get_product_by_id + +create_order + + +update_order + +get_user_by_id + +create_user_product + +deactivate_user_products +""" diff --git a/asr-monitor-test/app/tts_service.py b/asr-monitor-test/app/tts_service.py index 8f25419a..6e2de9e5 100644 --- a/asr-monitor-test/app/tts_service.py +++ b/asr-monitor-test/app/tts_service.py @@ -4,27 +4,32 @@ from copy import deepcopy from timeit import default_timer as timer import datetime from datetime import timedelta -import threading, time,queue,uuid,time,array +import threading, time, queue, uuid, time, array from threading import Lock, Thread from concurrent.futures import ThreadPoolExecutor import base64, gzip -import os,io, re, json +import os, io, re, json from io import BytesIO from typing import Optional, Dict, Any -import asyncio +import asyncio, httpx +from collections import deque -from fastapi import WebSocket, APIRouter,WebSocketDisconnect,Request,Body,Query +from fastapi import WebSocket, APIRouter, WebSocketDisconnect, Request, Body, Query from fastapi import FastAPI, UploadFile, File, Form, Header -from fastapi.responses import StreamingResponse,JSONResponse,Response -TTS_SAMPLERATE = 44100 # 22050 # 16000 +from fastapi.responses import StreamingResponse, JSONResponse, Response + +TTS_SAMPLERATE = 44100 # 22050 # 16000 FORMAT = "mp3" CHANNELS = 1 # 单声道 SAMPLE_WIDTH = 2 # 16-bit = 2字节 tts_router = APIRouter() + + # logger = logging.getLogger(__name__) class MillisecondsFormatter(logging.Formatter): """自定义日志格式器,添加毫秒时间戳""" + def formatTime(self, record, datefmt=None): # 将时间戳转换为本地时间元组 ct = self.converter(record.created) @@ -33,6 +38,7 @@ class MillisecondsFormatter(logging.Formatter): # 添加毫秒(3位) return f"{t}.{int(record.msecs):03d}" + # 配置全局日志格式 def configure_logging(): # 创建 Formatter @@ -51,30 +57,60 @@ def configure_logging(): root_logger.setLevel(logging.INFO) root_logger.addHandler(console_handler) + # 调用配置函数(程序启动时运行一次) configure_logging() + class StreamSessionManager: def __init__(self): self.sessions = {} # {session_id: {'tts_model': obj, 'buffer': queue, 'task_queue': Queue}} self.lock = threading.Lock() self.executor = ThreadPoolExecutor(max_workers=30) # 固定大小线程池 self.gc_interval = 300 # 5分钟清理一次 - self.gc_tts = 3 # 3s - def create_session(self, tts_model,sample_rate =8000, stream_format='mp3'): - session_id = str(uuid.uuid4()) + self.streaming_call_timeout = 15 # 20s + self.gc_tts = 3 # 3s + self.sentence_timeout = 1.5 # 1500ms句子超时 + self.sentence_endings = set('。?!;.?!;') # 中英文结束符 + # 增强版正则表达式:匹配中英文句子结束符(包含全角) + self.sentence_pattern = re.compile( + r'([,,。?!;.?!;?!;…]+["\'”’]?)(?=\s|$|[^,,。?!;.?!;?!;…])' + ) + + def create_session(self, tts_model, sample_rate=8000, stream_format='mp3', session_id=None, streaming_call=False): + if not session_id: + session_id = str(uuid.uuid4()) with self.lock: + # 创建TTS实例并设置流式回调 + tts_instance = tts_model + + # 定义音频数据回调函数 + def on_data(data: bytes): + if data: + try: + self.sessions[session_id]['last_active'] = time.time() + self.sessions[session_id]['buffer'].put(data) + except queue.Full: + logging.warning(f"Audio buffer full for session {session_id}") + + # 设置TTS流式传输 + tts_instance.setup_tts(on_data) + self.sessions[session_id] = { 'tts_model': tts_model, 'buffer': queue.Queue(maxsize=300), # 线程安全队列 'task_queue': queue.Queue(), 'active': True, 'last_active': time.time(), - 'audio_chunk_count':0, + 'audio_chunk_count': 0, 'finished': threading.Event(), # 添加事件对象 - 'sample_rate':sample_rate, - 'stream_format':stream_format, - "tts_chunk_data_valid":False + 'sample_rate': sample_rate, + 'stream_format': stream_format, + "tts_chunk_data_valid": False, + "text_buffer": "", # 新增文本缓冲区 + "last_text_time": time.time(), # 最后文本到达时间 + "streaming_call": streaming_call, + "tts_stream_started": False # 标记是否已启动流 } # 启动任务处理线程 threading.Thread(target=self._process_tasks, args=(session_id,), daemon=True).start() @@ -84,6 +120,9 @@ class StreamSessionManager: with self.lock: session = self.sessions.get(session_id) if not session: return + # 更新文本缓冲区和时间戳 + session['text_buffer'] += text + session['last_text_time'] = time.time() # 将文本放入任务队列(非阻塞) try: session['task_queue'].put(text, block=False) @@ -92,53 +131,94 @@ class StreamSessionManager: def _process_tasks(self, session_id): """任务处理线程(每个会话独立)""" - while True: - session = self.sessions.get(session_id) - if not session or not session['active']: + session = self.sessions.get(session_id) + if not session or not session['active']: + return + gen_tts_audio_func = self._generate_audio + if session.get('streaming_call'): + gen_tts_audio_func = self._stream_audio + while session['active']: + current_time = time.time() + text_to_process = "" + + # 直接处理缓冲区文本(无中间变量) + with self.lock: + if session['text_buffer']: + text_to_process = session['text_buffer'] + session['text_buffer'] = "" # 清空缓冲区 + + if text_to_process: + # 分割完整句子 + complete_sentences, remaining_text = self._split_and_extract(text_to_process) + # 保存剩余文本 + if remaining_text: + with self.lock: + session['text_buffer'] = remaining_text + session['text_buffer'] + + # 合并并处理完整句子 + if complete_sentences: + # 智能合并句子(最长300字符) + buffer = [] + current_length = 0 + + for sentence in complete_sentences: + sent_length = len(sentence) + + # 添加到当前缓冲区 + if current_length + sent_length <= 300: + buffer.append(sentence) + current_length += sent_length + else: + # 处理已缓冲的文本 + if buffer: + gen_tts_audio_func(session_id, "".join(buffer)) + buffer = [sentence] + current_length = sent_length + + # 处理剩余的缓冲文本 + if buffer: + gen_tts_audio_func(session_id, "".join(buffer)) + + # 检查超时未处理的文本 + if current_time - session['last_text_time'] > self.sentence_timeout: + with self.lock: + if session['text_buffer']: + # 直接处理剩余文本 + gen_tts_audio_func(session_id, session['text_buffer']) + session['text_buffer'] = "" + + if current_time - session['last_active'] > self.streaming_call_timeout: + if session.get('streaming_call'): + session['tts_model'].end_streaming_call() + session['streaming_call'] = False + + # 会话超时检查 + if current_time - session['last_active'] > self.gc_interval: + with self.lock: + if session['text_buffer']: + gen_tts_audio_func(session_id, session['text_buffer']) + session['text_buffer'] = "" + self.close_session(session_id) break - try: - # 合并多个文本块(最多等待50ms) - texts = [] - while len(texts) < 5: # 最大合并5个文本块 - try: - text = session['task_queue'].get(timeout=0.05) - texts.append(text) - except queue.Empty: - break - if texts: - # 提交到线程池处理 - future=self.executor.submit( - self._generate_audio, - session_id, - ' '.join(texts) # 合并文本减少请求次数 - ) - future.result() # 等待转换任务执行完毕 - # 会话超时检查 - if time.time() - session['last_active'] > self.gc_interval: - self.close_session(session_id) - break - if time.time() - session['last_active'] > self.gc_tts: - session['finished'].set() - break - - except Exception as e: - logging.error(f"Task processing error: {str(e)}") + # 休眠避免CPU空转 + time.sleep(0.05) # 50ms检查间隔 def _generate_audio(self, session_id, text): """实际生成音频(线程池执行)""" session = self.sessions.get(session_id) if not session: return - # logging.info(f"_generate_audio:{text}") + logging.info(f"_generate_audio:{text}") first_chunk = True # logging.info(f"转换开始!!! {text}") try: - for chunk in session['tts_model'].tts(text,session['sample_rate'],session['stream_format']): + """ + for chunk in session['tts_model'].tts(text, session['sample_rate'], session['stream_format']): if session['stream_format'] == 'wav': if first_chunk: chunk_len = len(chunk) if chunk_len > 2048: - session['buffer'].put(audio_fade_in(chunk,1024)) + session['buffer'].put(audio_fade_in(chunk, 1024)) else: session['buffer'].put(audio_fade_in(chunk, chunk_len)) first_chunk = False @@ -146,19 +226,76 @@ class StreamSessionManager: session['buffer'].put(chunk) else: session['buffer'].put(chunk) - session['last_active'] = time.time() - session['audio_chunk_count'] = session['audio_chunk_count'] + 1 - if session['tts_chunk_data_valid'] is False: - session['tts_chunk_data_valid'] = True #20250510 增加,表示连接TTS后台已经返回,可以通知前端了 - logging.info(f"转换结束!!! {session['audio_chunk_count'] }") + """ + session['tts_model'].text_tts_call(text) + session['last_active'] = time.time() + session['audio_chunk_count'] = session['audio_chunk_count'] + 1 + if session['tts_chunk_data_valid'] is False: + session['tts_chunk_data_valid'] = True # 20250510 增加,表示连接TTS后台已经返回,可以通知前端了 + # logging.info(f"转换结束!!! {session['audio_chunk_count']}") except Exception as e: session['buffer'].put(f"ERROR:{str(e)}") - logging.info(f"--_generate_audio--error {str(e)}") + def _stream_audio(self, session_id, text): + """流式传输文本到TTS服务""" + session = self.sessions.get(session_id) + if not session: + return + # logging.info(f"Streaming text to TTS: {text}") + + try: + # 使用流式调用发送文本 + session['tts_model'].streaming_call(text) + session['last_active'] = time.time() + except Exception as e: + logging.error(f"Error in streaming_call: {str(e)}") + session['buffer'].put(f"ERROR:{str(e)}".encode()) + + async def get_tts_buffer_data(self, session_id): + """异步流式返回 TTS 音频数据(适配同步 queue.Queue,带 10 秒超时)""" + session = self.sessions.get(session_id) + if not session: + raise ValueError(f"Session {session_id} not found") + + buffer = session['buffer'] # 这里是 queue.Queue + last_data_time = time.time() # 记录最后一次获取数据的时间 + + while session['active']: + try: + # 使用 run_in_executor + wait_for 设置 10 秒超时 + data = await asyncio.wait_for( + asyncio.get_event_loop().run_in_executor(None, buffer.get), + timeout=10.0 # 10 秒超时 + ) + last_data_time = time.time() # 更新最后数据时间 + yield data + + except asyncio.TimeoutError: + # 10 秒内没有新数据,检查是否超时 + if time.time() - last_data_time >= 10.0: + break + else: + continue # 未超时,继续等待 + + except asyncio.CancelledError: + logging.info(f"Session {session_id} stream cancelled") + break + + except Exception as e: + logging.error(f"Error in get_tts_buffer_data: {e}") + break def close_session(self, session_id): with self.lock: if session_id in self.sessions: + # 结束流式传输 + try: + # if self.sessions[session_id].get('streaming_call'): + # self.sessions[session_id]['tts_model'].end_streaming_call() + logging.info(f"Ended streaming for session {session_id}") + except Exception as e: + logging.error(f"Error ending streaming call: {str(e)}") + # 标记会话为不活跃 self.sessions[session_id]['active'] = False # 延迟2秒后清理资源 @@ -167,11 +304,75 @@ class StreamSessionManager: def _clean_session(self, session_id): with self.lock: if session_id in self.sessions: + # 确保流完全关闭 + try: + self.sessions[session_id]['tts_model'].end_streaming_call() + except: + pass del self.sessions[session_id] def get_session(self, session_id): return self.sessions.get(session_id) + def _has_sentence_ending(self, text): + """检测文本是否包含句子结束符""" + if not text: + return False + + # 检查常见结束符(包含全角字符) + if any(char in self.sentence_endings for char in text[-3:]): + return True + + # 检查中文段落结束(换行符前有结束符) + if '\n' in text and any(char in self.sentence_endings for char in text.split('\n')[-2:-1]): + return True + + return False + + def _split_and_extract(self, text): + """ + 增强型句子分割器 + 返回: (完整句子列表, 剩余文本) + """ + # 特殊处理:如果文本以逗号开头,先处理前面的部分 + if text.startswith((",", ",")): + return [text[0]], text[1:] + + # 1. 查找所有可能的句子结束位置 + matches = list(self.sentence_pattern.finditer(text)) + + if not matches: + return [], text # 没有找到结束符 + + # 2. 确定最后一个完整句子的结束位置 + last_end = 0 + complete_sentences = [] + + for match in matches: + end_pos = match.end() + sentence = text[last_end:end_pos].strip() + + # 跳过空句子 + if not sentence: + last_end = end_pos + continue + + # 检查是否为有效句子(最小长度或包含结束符) + if len(sentence) > 6 or any(char in "。.?!?!" for char in sentence): + complete_sentences.append(sentence) + last_end = end_pos + else: + # 短文本但包含结束符,可能是特殊符号 + if any(char in "。.?!?!" for char in sentence): + complete_sentences.append(sentence) + last_end = end_pos + + # 3. 提取剩余文本 + remaining_text = text[last_end:].strip() + + return complete_sentences, remaining_text + + stream_manager = StreamSessionManager() @@ -184,66 +385,95 @@ audio_text_cache = {} cache_lock = Lock() CACHE_EXPIRE_SECONDS = 600 # 10分钟过期 -""" -@manager.route('/tts_stream/', methods=['GET']) -def tts_stream(session_id): - session = stream_manager.sessions.get(session_id) - logging.info(f"--tts_stream {session}") - if session is None: - return get_error_data_result(message="Audio stream not found or expired.") - def generate(): - count = 0; - finished_event = session['finished'] +# WebSocket 连接管理 +class ConnectionManager: + def __init__(self): + self.active_connections = {} + + async def connect(self, websocket: WebSocket, connection_id: str): + await websocket.accept() + self.active_connections[connection_id] = websocket + logging.info(f"新连接建立: {connection_id}") + + async def disconnect(self, connection_id: str, code=1000, reason: str = ""): + if connection_id in self.active_connections: + try: + # 尝试正常关闭连接(非阻塞) + await self.active_connections[connection_id].close(code=code, reason=reason) + except: + pass # 忽略关闭错误 + finally: + del self.active_connections[connection_id] + + def is_connected(self, connection_id: str) -> bool: + """检查连接是否仍然活跃""" + return connection_id in self.active_connections + + async def _safe_send(self, connection_id: str, send_func, *args): + """安全发送的通用方法(核心修改)""" + # 1. 检查连接是否存在 + if connection_id not in self.active_connections: + logging.warning(f"尝试向不存在的连接发送数据: {connection_id}") + return False + + websocket = self.active_connections[connection_id] + try: - while not finished_event.is_set(): - if not session or not session['active']: - break - try: - chunk = session['buffer'].get_nowait() # - count = count + 1 - if isinstance(chunk, str) and chunk.startswith("ERROR"): - logging.info(f"---tts stream error!!!! {chunk}") - yield f"data:{{'error':'{chunk[6:]}'}}\n\n" - break - if session['stream_format'] == "wav": - gzip_base64_data = encode_gzip_base64(chunk) + "\r\n" - yield gzip_base64_data - else: - yield chunk - retry_count = 0 # 成功收到数据重置重试计数器 - except queue.Empty: - if session['stream_format'] == "wav": - pass - else: - pass - except Exception as e: - logging.info(f"tts streag get error2 {e} ") + # 2. 检查连接状态(关键修改) + if websocket.client_state.name != "CONNECTED": + logging.warning(f"连接 {connection_id} 状态为 {websocket.client_state.name}") + await self.disconnect(connection_id) + return False + + # 3. 执行发送操作 + await send_func(websocket, *args) + return True + + except (WebSocketDisconnect, RuntimeError) as e: + # 4. 处理连接断开异常 + logging.info(f"发送时检测到断开连接: {connection_id}, {str(e)}") + await self.disconnect(connection_id) + return False + except Exception as e: + # 5. 处理其他异常 + logging.error(f"发送数据出错: {connection_id}, {str(e)}") + await self.disconnect(connection_id) + return False + + async def send_bytes(self, connection_id: str, data: bytes): + """安全发送字节数据""" + return await self._safe_send( + connection_id, + lambda ws, d: ws.send_bytes(d), + data + ) + + async def send_text(self, connection_id: str, message: str): + """安全发送文本数据""" + return await self._safe_send( + connection_id, + lambda ws, m: ws.send_text(m), + message + ) + + async def send_json(self, connection_id: str, data: dict): + """安全发送JSON数据""" + return await self._safe_send( + connection_id, + lambda ws, d: ws.send_json(d), + data + ) - finally: - # 确保流结束后关闭会话 - if session: - # 延迟关闭会话,确保所有数据已发送 - stream_manager.close_session(session_id) - logging.info(f"Session {session_id} closed.") - # 关键响应头设置 +manager = ConnectionManager() - if session['stream_format'] == "wav": - resp = Response(stream_with_context(generate()), mimetype="audio/wav") - else: - resp = Response(stream_with_context(generate()), mimetype="audio/mpeg") - resp.headers.add_header("Cache-Control", "no-cache") - resp.headers.add_header("Connection", "keep-alive") - resp.headers.add_header("X-Accel-Buffering", "no") - return resp -""" def generate_mp3_header( - sample_rate: int, - bitrate_kbps: int, - channels: int = 1, - layer: str = "III" # 新增参数,支持 "I"/"II"/"III" + sample_rate: int, + bitrate_kbps: int, + channels: int = 1, + layer: str = "III" # 新增参数,支持 "I"/"II"/"III" ) -> bytes: """ 动态生成 MP3 帧头(4字节),支持 Layer I/II/III @@ -387,38 +617,39 @@ def generate_mp3_header( # ---------------------------------- # 组合帧头字段(修正层编码) # ---------------------------------- - sync = 0x7FF << 21 # 同步字 11位 (0x7FF = 0b11111111111) + sync = 0x7FF << 21 # 同步字 11位 (0x7FF = 0b11111111111) version = mpeg_version << 19 # MPEG 版本 2位 layer_bits = layer_code << 17 # Layer 编码(I:0b11, II:0b10, III:0b01) - protection = 0 << 16 # 无 CRC + protection = 0 << 16 # 无 CRC bitrate_bits = bitrate_index << 12 sample_rate_bits = sample_rate_index << 10 - padding = 0 << 9 # 无填充 + padding = 0 << 9 # 无填充 private = 0 << 8 mode = channel_mode << 6 - mode_ext = 0 << 4 # 扩展模式(单声道无需设置) + mode_ext = 0 << 4 # 扩展模式(单声道无需设置) copyright = 0 << 3 original = 0 << 2 - emphasis = 0b00 # 无强调 + emphasis = 0b00 # 无强调 frame_header = ( - sync | - version | - layer_bits | - protection | - bitrate_bits | - sample_rate_bits | - padding | - private | - mode | - mode_ext | - copyright | - original | - emphasis + sync | + version | + layer_bits | + protection | + bitrate_bits | + sample_rate_bits | + padding | + private | + mode | + mode_ext | + copyright | + original | + emphasis ) return frame_header.to_bytes(4, byteorder='big') + # ------------------------------------------------ def audio_fade_in(audio_data, fade_length): # 假设音频数据是16位单声道PCM @@ -449,8 +680,6 @@ def parse_markdown_json(json_string): return {'success': False, 'data': 'not a valid markdown json string'} - - audio_text_cache = {} cache_lock = Lock() CACHE_EXPIRE_SECONDS = 600 # 10分钟过期 @@ -581,14 +810,28 @@ def test_qwen_chat(): print(f"错误信息:{response.message}") print("请参考文档:https://help.aliyun.com/zh/model-studio/developer-reference/error-code") + ALI_KEY = "sk-a47a3fb5f4a94f66bbaf713779101c75" +from dashscope.api_entities.dashscope_response import SpeechSynthesisResponse +from dashscope.audio.tts import ( + ResultCallback as TTSResultCallback, + SpeechSynthesizer as TTSSpeechSynthesizer, + SpeechSynthesisResult as TTSSpeechSynthesisResult, +) +# cyx 2025 01 19 测试cosyvoice 使用tts_v2 版本 +from dashscope.audio.tts_v2 import ( + ResultCallback as CosyResultCallback, + SpeechSynthesizer as CosySpeechSynthesizer, + AudioFormat, +) + class QwenTTS: - def __init__(self, key,format="mp3",sample_rate=44100, model_name="cosyvoice-v1/longxiaochun", base_url=""): + def __init__(self, key, format="mp3", sample_rate=44100, model_name="cosyvoice-v1/longxiaochun"): import dashscope import ssl - print("---begin--init QwenTTS--") # cyx - self.model_name = model_name + logging.info(f"---begin--init QwenTTS-- {format} {sample_rate} {model_name} {model_name.split('@')[0]}") # cyx + self.model_name = model_name.split('@')[0] dashscope.api_key = key ssl._create_default_https_context = ssl._create_unverified_context # 禁用验证 self.synthesizer = None @@ -597,122 +840,120 @@ class QwenTTS: self.voice = "" self.format = format self.sample_rate = sample_rate - if '/' in model_name: - parts = model_name.split('/', 1) + self.first_chunk = True + if '/' in self.model_name: + parts = self.model_name.split('/', 1) # 返回分离后的两个字符串parts[0], parts[1] if parts[0] == 'cosyvoice-v1': self.is_cosyvoice = True self.voice = parts[1] - def tts(self, text): - from dashscope.api_entities.dashscope_response import SpeechSynthesisResponse - if self.is_cosyvoice is False: - from dashscope.audio.tts import ResultCallback, SpeechSynthesizer, SpeechSynthesisResult - from collections import deque - else: - # cyx 2025 01 19 测试cosyvoice 使用tts_v2 版本 - from dashscope.audio.tts_v2 import ResultCallback, SpeechSynthesizer, AudioFormat # , SpeechSynthesisResult - from dashscope.audio.tts import SpeechSynthesisResult - from collections import deque + class Callback(TTSResultCallback): + def __init__(self) -> None: + self.dque = deque() - print(f"--QwenTTS--tts_stream begin-- {text} {self.is_cosyvoice} {self.voice}") # cyx + def _run(self): + while True: + if not self.dque: + time.sleep(0) + continue + val = self.dque.popleft() + if val: + yield val + else: + break - class Callback(ResultCallback): - def __init__(self) -> None: - self.dque = deque() + def on_open(self): + pass - def _run(self): - while True: - if not self.dque: - time.sleep(0) - continue - val = self.dque.popleft() - if val: - yield val - else: - break + def on_complete(self): + self.dque.append(None) - def on_open(self): - pass + def on_error(self, response: SpeechSynthesisResponse): + print("Qwen tts error", str(response)) + raise RuntimeError(str(response)) - def on_complete(self): - self.dque.append(None) + def on_close(self): + pass - def on_error(self, response: SpeechSynthesisResponse): - print("Qwen tts error", str(response)) - raise RuntimeError(str(response)) + def on_event(self, result: TTSSpeechSynthesisResult): + if result.get_audio_frame() is not None: + self.dque.append(result.get_audio_frame()) - def on_close(self): - pass + # -------------------------- - def on_event(self, result: SpeechSynthesisResult): - if result.get_audio_frame() is not None: - self.dque.append(result.get_audio_frame()) + class Callback_Cosy(CosyResultCallback): + def __init__(self, data_callback=None) -> None: + self.dque = deque() + self.data_callback = data_callback - # -------------------------- + def _run(self): + while True: + if not self.dque: + time.sleep(0) + continue + val = self.dque.popleft() + if val: + yield val + else: + break - class Callback_v2(ResultCallback): - def __init__(self) -> None: - self.dque = deque() + def on_open(self): + logging.info("Qwen CosyVoice tts open ") + pass - def _run(self): - while True: - if not self.dque: - time.sleep(0) - continue - val = self.dque.popleft() - if val: - yield val - else: - break + def on_complete(self): + self.dque.append(None) + if self.data_callback: + self.data_callback(None) # 发送结束信号 - def on_open(self): - logging.info("Qwen tts open") - pass + def on_error(self, response: SpeechSynthesisResponse): + print("Qwen tts error", str(response)) + if self.data_callback: + self.data_callback(f"ERROR:{str(response)}".encode()) + raise RuntimeError(str(response)) - def on_complete(self): - self.dque.append(None) + def on_close(self): + # print("---Qwen call back close") # cyx + logging.info("Qwen CosyVoice tts close") + pass - def on_error(self, response: SpeechSynthesisResponse): - print("Qwen tts error", str(response)) - raise RuntimeError(str(response)) + """ canceled for test 语音大模型CosyVoice + def on_event(self, result: SpeechSynthesisResult): + if result.get_audio_frame() is not None: + self.dque.append(result.get_audio_frame()) + """ - def on_close(self): - # print("---Qwen call back close") # cyx - logging.info("Qwen tts close") - pass + def on_event(self, message): + # logging.info(f"recv speech synthsis message {message}") + pass - """ canceled for test 语音大模型CosyVoice - def on_event(self, result: SpeechSynthesisResult): - if result.get_audio_frame() is not None: - self.dque.append(result.get_audio_frame()) - """ - - def on_event(self, message): - # print(f"recv speech synthsis message {message}") - pass - - # 以下适合语音大模型CosyVoice - def on_data(self, data: bytes) -> None: - if len(data) > 0: + # 以下适合语音大模型CosyVoice + def on_data(self, data: bytes) -> None: + if len(data) > 0: + if self.data_callback: + self.data_callback(data) + else: self.dque.append(data) - # -------------------------- + # -------------------------- + def tts(self, text): + print(f"--QwenTTS--tts_stream begin-- {text} {self.is_cosyvoice} {self.voice}") # cyx # text = self.normalize_text(text) try: # if self.model_name != 'cosyvoice-v1': if self.is_cosyvoice is False: - self.callback = Callback() - SpeechSynthesizer.call(model=self.model_name, - text=text, - callback=self.callback, - format="wav") # format="mp3") + self.callback = self.Callback() + TTSSpeechSynthesizer.call(model=self.model_name, + text=text, + callback=self.callback, + format="wav") # format="mp3") else: - self.callback = Callback_v2() - format =self.get_audio_format(self.format,self.sample_rate) - self.synthesizer = SpeechSynthesizer( + self.callback = self.Callback_Cosy() + format = self.get_audio_format(self.format, self.sample_rate) + self.synthesizer = CosySpeechSynthesizer( model='cosyvoice-v1', # voice="longyuan", #"longfei", voice=self.voice, @@ -725,7 +966,7 @@ class QwenTTS: # ----------------------------------- try: for data in self.callback._run(): - #logging.info(f"dashcope return data {len(data)}") + # logging.info(f"dashcope return data {len(data)}") yield data # print(f"---Qwen return data {num_tokens_from_string(text)}") # yield num_tokens_from_string(text) @@ -733,6 +974,36 @@ class QwenTTS: except Exception as e: raise RuntimeError(f"**ERROR**: {e}") + def setup_tts(self, on_data): + """设置 TTS 回调,返回配置好的 synthesizer""" + if not self.is_cosyvoice: + raise NotImplementedError("Only CosyVoice supported") + + # 创建 CosyVoice 回调 + self.callback = self.Callback_Cosy(on_data) + format_val = self.get_audio_format(self.format, self.sample_rate) + logging.info(f"setup_tts {self.voice} {format_val}") + self.synthesizer = CosySpeechSynthesizer( + model='cosyvoice-v1', + voice=self.voice, # voice="longyuan", #"longfei", + callback=self.callback, + format=format_val + ) + return self.synthesizer + + def text_tts_call(self, text): + if self.synthesizer: + self.synthesizer.call(text) + + def streaming_call(self, text): + if self.synthesizer: + self.synthesizer.streaming_call(text) + + def end_streaming_call(self): + if self.synthesizer: + # logging.info(f"---dale end_streaming_call") + self.synthesizer.streaming_complete() + def get_audio_format(self, format: str, sample_rate: int): """动态获取音频格式""" from dashscope.audio.tts_v2 import AudioFormat @@ -748,16 +1019,428 @@ class QwenTTS: (44100, 'mp3'): AudioFormat.MP3_44100HZ_MONO_256KBPS, (44100, 'pcm'): AudioFormat.PCM_44100HZ_MONO_16BIT, (44100, 'wav'): AudioFormat.WAV_44100HZ_MONO_16BIT, - (48800, 'mp3'): AudioFormat.MP3_48000HZ_MONO_256KBPS, - (48800, 'pcm'): AudioFormat.PCM_48000HZ_MONO_16BIT, - (48800, 'wav'):AudioFormat.WAV_48000HZ_MONO_16BIT + (48000, 'mp3'): AudioFormat.MP3_48000HZ_MONO_256KBPS, + (48000, 'pcm'): AudioFormat.PCM_48000HZ_MONO_16BIT, + (48000, 'wav'): AudioFormat.WAV_48000HZ_MONO_16BIT } return format_map.get((sample_rate, format), AudioFormat.MP3_16000HZ_MONO_128KBPS) - def end_tts(self): - if self.synthesizer: - self.synthesizer.streaming_complete() + +import threading +import uuid +import time +import asyncio +import logging +from concurrent.futures import ThreadPoolExecutor +from io import BytesIO + +import threading +import uuid +import time +import asyncio +import logging +from concurrent.futures import ThreadPoolExecutor +from collections import deque +from io import BytesIO + + +class UnifiedTTSEngine: + def __init__(self): + self.lock = threading.Lock() + self.tasks = {} + self.executor = ThreadPoolExecutor(max_workers=10) + self.cache_expire = 300 # 5分钟缓存 + # 启动清理过期任务的定时器 + self.cleanup_timer = None + self.start_cleanup_timer() + + def _cleanup_old_tasks(self): + """清理过期任务""" + now = time.time() + with self.lock: + expired_ids = [task_id for task_id, task in self.tasks.items() + if now - task['created_at'] > self.cache_expire] + for task_id in expired_ids: + self._remove_task(task_id) + + def _remove_task(self, task_id): + """移除任务""" + if task_id in self.tasks: + task = self.tasks.pop(task_id) + # 取消可能的后台任务 + if 'future' in task and not task['future'].done(): + task['future'].cancel() + # 其他资源在任务被移除后会被垃圾回收 + # 资源释放机制总结: + # 移除任务引用:self.tasks.pop() 解除任务对象引用,触发垃圾回收。 + # 取消后台线程:future.cancel() 终止未完成线程,释放线程资源。 + # 自动内存回收:Python GC 回收任务对象及其队列、缓冲区占用的内存。 + # 线程池管理:执行器自动回收线程至池中,避免资源泄漏。 + + def create_tts_task(self, text, format, sample_rate, model_name, key, delay_gen_audio=False): + """创建TTS任务(同步方法)""" + self._cleanup_old_tasks() + audio_stream_id = str(uuid.uuid4()) + + # 创建任务数据结构 + task_data = { + 'id': audio_stream_id, + 'text': text, + 'format': format, + 'sample_rate': sample_rate, + 'model_name': model_name, + 'key': key, + 'delay_gen_audio': delay_gen_audio, + 'created_at': time.time(), + 'status': 'pending', + 'data_queue': deque(), + 'event': threading.Event(), + 'completed': False, + 'error': None + } + with self.lock: + self.tasks[audio_stream_id] = task_data + + # 如果不是延迟模式,立即启动任务 + if not delay_gen_audio: + self._start_tts_task(audio_stream_id) + + return audio_stream_id + + def _start_tts_task(self, audio_stream_id): + # 启动TTS任务(后台线程) + + task = self.tasks.get(audio_stream_id) + if not task or task['status'] != 'pending': + return + logging.info("已经启动 start tts task {audio_stream_id}") + task['status'] = 'processing' + + # 在后台线程中执行TTS + future = self.executor.submit(self._run_tts_sync, audio_stream_id) + task['future'] = future + + # 如果需要等待任务完成 + if not task.get('delay_gen_audio', True): + try: + # 等待任务完成(最多5分钟) + future.result(timeout=300) + logging.info(f"TTS任务 {audio_stream_id} 已完成") + self._merge_audio_data(audio_stream_id) + except concurrent.futures.TimeoutError: + task['error'] = "TTS生成超时" + task['completed'] = True + logging.error(f"TTS任务 {audio_stream_id} 超时") + except Exception as e: + task['error'] = f"ERROR:{str(e)}" + task['completed'] = True + logging.exception(f"TTS任务执行异常: {str(e)}") + + def _run_tts_sync(self, audio_stream_id): + # 同步执行TTS生成 在后台线程中执行 + task = self.tasks.get(audio_stream_id) + if not task: + return + + try: + # 创建TTS实例 + tts = QwenTTS( + key=task['key'], + format=task['format'], + sample_rate=task['sample_rate'], + model_name=task['model_name'] + ) + + # 定义同步数据处理函数 + def data_handler(data): + if data is None: # 结束信号 + task['completed'] = True + task['event'].set() + logging.info(f"--data_handler on_complete") + elif data.startswith(b"ERROR"): # 错误信号 + task['error'] = data.decode() + task['completed'] = True + task['event'].set() + else: # 音频数据 + task['data_queue'].append(data) + + # 设置并执行TTS + synthesizer = tts.setup_tts(data_handler) + synthesizer.call(task['text']) + + # 等待完成或超时 + if not task['event'].wait(timeout=300): # 5分钟超时 + task['error'] = "TTS generation timeout" + task['completed'] = True + + logging.info(f"--tts task event set error = {task['error']}") + + except Exception as e: + task['error'] = f"ERROR:{str(e)}" + task['completed'] = True + + def _merge_audio_data(self, audio_stream_id): + """将任务的所有音频数据合并到ByteIO缓冲区""" + task = self.tasks.get(audio_stream_id) + if not task or not task.get('completed'): + return + + try: + logging.info(f"开始合并音频数据: {audio_stream_id}") + + # 创建内存缓冲区 + buffer = io.BytesIO() + + # 合并所有数据块 + for data_chunk in task['data_queue']: + buffer.write(data_chunk) + + # 重置指针位置以便读取 + buffer.seek(0) + + # 保存到任务对象 + task['buffer'] = buffer + logging.info(f"音频数据合并完成,总大小: {buffer.getbuffer().nbytes} 字节") + + # 可选:清理原始数据队列以节省内存 + task['data_queue'].clear() + + except Exception as e: + logging.error(f"合并音频数据失败: {str(e)}") + task['error'] = f"合并错误: {str(e)}" + + async def get_audio_stream(self, audio_stream_id): + """获取音频流(异步生成器)""" + task = self.tasks.get(audio_stream_id) + if not task: + raise RuntimeError("Audio stream not found") + + # 如果是延迟任务且未启动,现在启动 status 为 pending + if task['delay_gen_audio'] and task['status'] == 'pending': + self._start_tts_task(audio_stream_id) + + # 等待任务启动 + while task['status'] == 'pending': + await asyncio.sleep(0.1) + + # 流式返回数据 + while not task['completed'] or task['data_queue']: + while task['data_queue']: + data = task['data_queue'].popleft() + # logging.info(f"yield data {len(data)}") + yield data + + # 短暂等待新数据 + await asyncio.sleep(0.05) + + # 检查错误 + if task['error']: + raise RuntimeError(task['error']) + + def start_cleanup_timer(self): + """启动定时清理任务""" + if self.cleanup_timer: + self.cleanup_timer.cancel() + + self.cleanup_timer = threading.Timer(30.0, self.cleanup_task) # 每30秒清理一次 + self.cleanup_timer.daemon = True # 设置为守护线程 + self.cleanup_timer.start() + + def cleanup_task(self): + """执行清理任务""" + try: + self._cleanup_old_tasks() + except Exception as e: + logging.error(f"清理任务时出错: {str(e)}") + finally: + self.start_cleanup_timer() # 重新启动定时器 + + +# 全局 TTS 引擎实例 +tts_engine = UnifiedTTSEngine() + + +def replace_domain(url: str) -> str: + """替换URL中的域名为本地地址,不使用urllib.parse""" + # 定义需要替换的域名列表 + domains_to_replace = [ + "http://1.13.185.116:9380", + "https://ragflow.szzysztech.com", + "1.13.185.116:9380", + "ragflow.szzysztech.com" + ] + + # 尝试替换每个可能的域名 + for domain in domains_to_replace: + if domain in url: + # 直接替换域名部分 + return url.replace(domain, "http://localhost:9380", 1) + + # 如果未匹配到特定域名,尝试智能替换 + if "://" in url: + # 分割协议和路径 + protocol, path = url.split("://", 1) + + # 查找第一个斜杠位置来确定域名结束位置 + slash_pos = path.find("/") + if slash_pos > 0: + # 替换域名部分 + return f"http://localhost:9380{path[slash_pos:]}" + else: + # 没有路径部分,直接返回本地地址 + return "http://localhost:9380" + else: + # 没有协议部分,直接添加本地地址 + return f"http://localhost:9380/{url}" + + +async def proxy_aichat_audio_stream(client_id: str, audio_url: str): + """代理外部音频流请求""" + try: + # 替换域名为本地地址 + local_url = audio_url + logging.info(f"代理音频流: {audio_url} -> {local_url}") + + async with httpx.AsyncClient(timeout=60.0) as client: + async with client.stream("GET", local_url) as response: + # 流式转发音频数据 + async for chunk in response.aiter_bytes(): + if not await manager.send_bytes(client_id, chunk): + logging.warning(f"Audio proxy interrupted for {client_id}") + return + except Exception as e: + logging.error(f"Audio proxy failed: {str(e)}") + await manager.send_text(client_id, json.dumps({ + "type": "error", + "message": f"音频流获取失败: {str(e)}" + })) + + +# 代理函数 - 文本流 +async def proxy_aichat_text_stream(client_id: str, completions_url: str, payload: dict): + """代理大模型文本流请求 - 兼容现有Flask实现""" + try: + logging.info(f"代理文本流: completions_url={completions_url} {payload}") + logging.debug(f"请求负载: {json.dumps(payload, ensure_ascii=False)}") + + headers = { + "Content-Type": "application/json", + 'Authorization': 'Bearer ragflow-NhZTY5Y2M4YWQ1MzExZWY4Zjc3MDI0Mm' + } + # 创建TTS实例 + tts_model = QwenTTS( + key=ALI_KEY, + format=payload.get('tts_stream_format', 'mp3'), + sample_rate=payload.get('tts_sample_rate', 48000), + model_name=payload.get('tts_model', 'cosyvoice-v1/longyuan@Tongyi-Qianwen') + ) + + # 创建流会话 + tts_stream_session_id = stream_manager.create_session( + tts_model=tts_model, + sample_rate=payload.get('tts_sample_rate', 48000), + stream_format=payload.get('tts_stream_format', 'mp3'), + session_id=None, + streaming_call=True + ) + # logging.info(f"---tts_stream_session_id = {tts_stream_session_id}") + tts_stream_session_id_sent = False + # 使用更长的超时时间 (5分钟) + timeout = httpx.Timeout(300.0, connect=60.0) + async with httpx.AsyncClient(timeout=timeout) as client: + # 关键修改:使用流式请求模式 + async with client.stream( # <-- 使用stream方法 + "POST", + completions_url, + json=payload, + headers=headers + ) as response: + logging.info(f"响应状态: HTTP {response.status_code}") + + if response.status_code != 200: + # 读取错误信息(非流式) + error_content = await response.aread() + error_msg = f"后端错误: HTTP {response.status_code}" + error_msg += f" - {error_content[:200].decode()}" if error_content else "" + await manager.send_text(client_id, json.dumps({"type": "error", "message": error_msg})) + return + + # 验证SSE流 + content_type = response.headers.get("content-type", "").lower() + if "text/event-stream" not in content_type: + logging.warning("非流式响应,转发完整内容") + full_content = await response.aread() + await manager.send_text(client_id, json.dumps({ + "type": "text", + "data": full_content.decode('utf-8') + })) + return + + logging.info("开始处理SSE流") + event_count = 0 + # 使用异步迭代器逐行处理 + async for line in response.aiter_lines(): + # 跳过空行和注释行 + if not line or line.startswith(':'): + continue + + # 处理SSE事件 + if line.startswith("data:"): + data_str = line[5:].strip() + if data_str: # 过滤空数据 + try: + # 解析并提取增量文本 + data_obj = json.loads(data_str) + delta_text = None + if isinstance(data_obj, dict) and isinstance(data_obj.get('data', None), dict): + delta_text = data_obj.get('data', None).get('delta_ans', "") + if tts_stream_session_id_sent is False: + data_obj.get('data')['audio_stream_url'] = f"/tts_stream/{tts_stream_session_id}" + data_str = json.dumps(data_obj) + tts_stream_session_id_sent = True + + # 直接转发原始数据 + await manager.send_text(client_id, json.dumps({ + "type": "text", + "data": data_str + })) + # 这里构建{"type":"text",'data':"data_str"}) 是为了前端websocket进行数据解析 + if delta_text: + # 追加到会话管理器 + stream_manager.append_text(tts_stream_session_id, delta_text) + # logging.info(f"文本代理转发: {data_str}") + event_count += 1 + except Exception as e: + logging.error(f"事件发送失败: {str(e)}") + + # 保持连接活性 + await asyncio.sleep(0.001) # 避免CPU空转 + + logging.info(f"SSE流处理完成,事件数: {event_count}") + + # 发送结束信号 + await manager.send_text(client_id, json.dumps({"type": "end"})) + + except httpx.ReadTimeout: + logging.error("读取后端服务超时") + await manager.send_text(client_id, json.dumps({ + "type": "error", + "message": "后端服务响应超时" + })) + except httpx.ConnectError as e: + logging.error(f"连接后端服务失败: {str(e)}") + await manager.send_text(client_id, json.dumps({ + "type": "error", + "message": f"无法连接到后端服务: {str(e)}" + })) + except Exception as e: + logging.exception(f"文本代理失败: {str(e)}") + await manager.send_text(client_id, json.dumps({ + "type": "error", + "message": f"文本流获取失败: {str(e)}" + })) + @tts_router.get("/audio/pcm_mp3") async def stream_mp3(): @@ -804,174 +1487,138 @@ def generate_silence_header(duration_ms: int = 500) -> bytes: # ------------------------ API路由 ------------------------ @tts_router.post("/chats/{chat_id}/tts") async def create_tts_request(chat_id: str, request: Request): - """创建TTS音频流""" try: data = await request.json() - logging.info(f"API--create_tts_request1-- {data}") + logging.info(f"Creating TTS request: {data}") + # 参数校验 text = data.get("text", "").strip() if not text: - raise HTTPException(400, detail="文本内容不能为空") + raise HTTPException(400, detail="Text cannot be empty") format = data.get("tts_stream_format", "mp3") if format not in ["mp3", "wav", "pcm"]: - raise HTTPException(400, detail="不支持的音频格式") + raise HTTPException(400, detail="Unsupported audio format") sample_rate = data.get("tts_sample_rate", 48000) + if sample_rate not in [8000, 16000, 22050, 44100, 48000]: + raise HTTPException(400, detail="Unsupported sample rate") - if sample_rate not in [8000, 16000, 22050, 44100]: - raise HTTPException(400, detail="不支持的采样率") - model_name = data.get("model_name","cosyvoice-v1/longxiaochun") - delay_gen_audio = data.get('delay_gen_audio',False) - format ="mp3" + model_name = data.get("model_name", "cosyvoice-v1/longxiaochun") + delay_gen_audio = data.get('delay_gen_audio', False) - #sample_rate = 48000 - # 生成音频流 - audio_stream_id = str(uuid.uuid4()) - buffer = None - tts_info = { - "text" :text, - "buffer": buffer, - "format": format, - "sample_rate": sample_rate, - "model_name" : model_name, - "created_at": datetime.datetime.now() - } + # 创建TTS任务 + audio_stream_id = tts_engine.create_tts_task( + text=text, + format=format, + sample_rate=sample_rate, + model_name=model_name, + key=ALI_KEY, + delay_gen_audio=delay_gen_audio + ) - if delay_gen_audio is False: - logging.info("--begin generate tts --") - buffer = io.BytesIO() - tts = QwenTTS(ALI_KEY,format,sample_rate,model_name.split("@")[0]) - try: - for chunk in tts.tts(text): - buffer.write(chunk) - buffer.seek(0) - except Exception as e: - logging.error(f"TTS生成失败: {str(e)}") - raise HTTPException(500, detail="音频生成失败") - tts_info['buffer'] =buffer - tts_info['size'] = buffer.getbuffer().nbytes - - # 存储到缓存 - with cache_lock: - audio_text_cache[audio_stream_id] = tts_info - logging.info(f" tts return {audio_text_cache[audio_stream_id]}") return JSONResponse( status_code=200, content={ - "tts_url":f"/chats/{chat_id}/tts/{audio_stream_id}", + "tts_url": f"/chats/{chat_id}/tts/{audio_stream_id}", "url": f"/chats/{chat_id}/tts/{audio_stream_id}", - "expires_at": (datetime.datetime.now() + timedelta(seconds=CACHE_EXPIRE_SECONDS)).isoformat() + "ws_url": f"/chats/{chat_id}/tts/{audio_stream_id}", # WebSocket URL 2025 0622新增 + "expires_at": (datetime.datetime.now() + datetime.timedelta(seconds=300)).isoformat() } ) except Exception as e: - logging.error(f"请求处理失败: {str(e)}") - raise HTTPException(500, detail="服务器内部错误") + logging.error(f"Request failed: {str(e)}") + raise HTTPException(500, detail="Internal server error") + executor = ThreadPoolExecutor() + + @tts_router.get("/chats/{chat_id}/tts/{audio_stream_id}") async def get_tts_audio( chat_id: str, audio_stream_id: str, range: str = Header(None) ): - """获取音频流""" - # 清理过期缓存 - cleanup_cache() + try: + # 获取任务信息 + task = tts_engine.tasks.get(audio_stream_id) + if not task: + # 返回友好的错误信息而不是抛出异常 + return JSONResponse( + status_code=404, + content={ + "error": "Audio stream not found", + "message": f"The requested audio stream ID '{audio_stream_id}' does not exist or has expired", + "suggestion": "Please create a new TTS request and try again" + } + ) - # 获取缓存 - with cache_lock: - tts_info = audio_text_cache.get(audio_stream_id) + # 获取媒体类型 + format = task['format'] + media_type = { + "mp3": "audio/mpeg", + "wav": "audio/wav", + "pcm": f"audio/L16; rate={task['sample_rate']}; channels=1" + }[format] - if not tts_info: - #raise HTTPException(404, detail="音频流不存在或已过期") - logging.warning("音频流不存在或已过期") - return - # 准备响应参数 - buffer = tts_info.get("buffer",None) - format = tts_info.get("format","mp3") - sample_rate = tts_info.get("sample_rate", 44100) - total_size = tts_info.get('size',0) - model_name = tts_info['model_name'] - text = tts_info['text'] - media_type = { - "mp3": "audio/mpeg", - "wav": "audio/wav", - "pcm": f"audio/L16; rate={tts_info['sample_rate']}; channels=1" - }[format] - logging.info(f"enter get_tts_audio1 buffer={buffer} {format} {sample_rate} range {range}") + # 如果任务已完成且有完整缓冲区,处理Range请求 + logging.info(f"get_tts_audio task = {task.get('completed', 'None')} {task.get('buffer', 'None')}") - def generate_audio(): - logging.info(f"get_tts_audio1 generate_audio {format} {sample_rate} {model_name} range{range}") - tts = QwenTTS(ALI_KEY, format , sample_rate,model_name.split("@")[0]) - try: - for chunk in tts.tts(text): - if format == 'wav': - yield add_wav_header(chunk, sample_rate) - else: - yield chunk - finally: - # 恢复全局变量 - pass - def read_buffer(): - print(f"read_buffer {audio_stream} {audio_stream.closed}") - audio_stream.seek(0) # 关键重置操作 - data = audio_stream.read(1024) - while data: - yield data - data = audio_stream.read(1024) + # 创建响应内容生成器 + def buffer_read(buffer): + content_length = buffer.getbuffer().nbytes + remaining = content_length + chunk_size = 4096 + buffer.seek(0) + while remaining > 0: + read_size = min(remaining, chunk_size) + data = buffer.read(read_size) + if not data: + break + yield data + remaining -= len(data) - async def generate_audio_async(): - logging.info(f"get_tts_audio1 generate_audio_async {format} {sample_rate} {model_name}") - tts = QwenTTS(ALI_KEY, format , sample_rate,model_name.split("@")[0]) - #前端有可能传入 cosyvoice-v1/longyuan@Tongyi-Qianwen 通过.split("@")[0] 取@之前部分 - loop = asyncio.get_event_loop() - yield_len = 0 - try: - # 将同步函数放入线程池执行 - sync_generator = await loop.run_in_executor(executor, lambda: tts.tts(text)) - for chunk in sync_generator: - yield_len = yield_len + len(chunk) - yield chunk - except (BrokenPipeError, ConnectionResetError): - logging.warning("客户端主动断开连接") - finally: - # 恢复全局变量 - logging.info(f"yield len={yield_len}") - pass - # 处理范围请求 - if buffer is not None: - if range: - return handle_range_request(range, buffer, total_size, media_type) - buffer.seek(0) - # 完整文件响应 + if task.get('completed') and task.get('buffer') is not None: + buffer = task['buffer'] + total_size = buffer.getbuffer().nbytes + # 强制小文件使用流式传输(避免206响应问题) + + if total_size < 1024 * 120: # 小于300KB + range = None + + if range: + # 处理范围请求 + return handle_range_request(range, buffer, total_size, media_type) + else: + return StreamingResponse( + buffer_read(buffer), + media_type=media_type, + headers={ + "Accept-Ranges": "bytes", + "Cache-Control": "no-store", + "Transfer-Encoding": "chunked" + }) + + # 创建流式响应 + logging.info("tts_engine.get_audio_stream--0") return StreamingResponse( - iter(lambda: buffer.read(4096), b""), + tts_engine.get_audio_stream(audio_stream_id), media_type=media_type, headers={ "Accept-Ranges": "bytes", - "Content-Length": str(total_size), - "Cache-Control": f"max-age={CACHE_EXPIRE_SECONDS}" - } - ) - else: - return StreamingResponse( - #generate_audio(), - generate_audio_async(), - media_type=media_type, - headers={ - "Transfer-Encoding": "chunked", "Cache-Control": "no-store", - #"Content-Disposition": "inline", - #"Content-Length": str(1067890), - "Accept-Ranges": "bytes", - "ETag":audio_stream_id, + "Transfer-Encoding": "chunked" } ) + except Exception as e: + logging.error(f"Audio streaming failed: {str(e)}") + raise HTTPException(500, detail="Audio generation error") -def handle_range_request(range_header: str, buffer:BytesIO, total_size: int, media_type: str): + +def handle_range_request(range_header: str, buffer: BytesIO, total_size: int, media_type: str): """处理 HTTP Range 请求""" try: # 解析 Range 头部 (示例: "bytes=0-1023") @@ -982,22 +1629,39 @@ def handle_range_request(range_header: str, buffer:BytesIO, total_size: int, med start_str, end_str = range_spec.split('-') start = int(start_str) end = int(end_str) if end_str else total_size - 1 - + logging.info(f"handle_range_request--1 {start_str}-{end_str} {end}") # 验证范围有效性 if start >= total_size or end >= total_size: raise HTTPException(status_code=416, headers={ "Content-Range": f"bytes */{total_size}" }) - status_code = 206 - if start>0 and end == total_size-1: - status_code = 200 + + # 计算内容长度 + content_length = end - start + 1 + + # 设置状态码 + status_code = 206 # Partial Content + if start == 0 and end == total_size - 1: + status_code = 200 # Full Content + # 设置流读取位置 buffer.seek(start) - content_length = end - start + 1 + + # 创建响应内容生成器 + def content_generator(): + remaining = content_length + chunk_size = 4096 + while remaining > 0: + read_size = min(remaining, chunk_size) + data = buffer.read(read_size) + if not data: + break + yield data + remaining -= len(data) # 返回分块响应 return StreamingResponse( - iter(lambda: buffer.read(4096), b''), # 直接使用 iter 避免状态问题 + content_generator(), status_code=status_code, media_type=media_type, headers={ @@ -1011,6 +1675,91 @@ def handle_range_request(range_header: str, buffer:BytesIO, total_size: int, med except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + +@tts_router.websocket("/chats/{chat_id}/tts/{audio_stream_id}") +async def websocket_tts_endpoint( + websocket: WebSocket, + chat_id: str, + audio_stream_id: str +): + # 接收 header 参数 + headers = websocket.headers + service_type = headers.get("x-tts-type") # 注意:header 名称转为小写 + # audio_url = headers.get("x-audio-url") + """ + 前端示例 + websocketConnection = uni.connectSocket({ + url: url, + header: { + 'Authorization': token, + 'X-Tts-Type': 'AiChat', //'Ask' // 自定义参数1 + 'X-Device-Type': 'mobile', // 自定义参数2 + 'X-User-ID': '12345' // 自定义参数3 + }, + success: () => { + console.log('WebSocket connected'); + }, + fail: (err) => { + console.error('WebSocket connection failed:', err); + } + }); + """ + # 创建唯一连接 ID + connection_id = str(uuid.uuid4()) + # logging.info(f"---dale-- websocket connection_id = {connection_id} chat_id={chat_id}") + await manager.connect(websocket, connection_id) + + completed_successfully = False + try: + # 根据tts_type路由到不同的音频源 + if service_type == "AiChatTts": + # 音频代理服务 + audio_url = f"http://localhost:9380/api/v1/tts_stream/{audio_stream_id}" + # await proxy_aichat_audio_stream(connection_id, audio_url) + sample_rate = stream_manager.get_session(audio_stream_id).get('sample_rate') + await manager.send_json(connection_id, {"command": "sample_rate", "params": sample_rate}) + async for data in stream_manager.get_tts_buffer_data(audio_stream_id): + if not await manager.send_bytes(connection_id, data): + break + completed_successfully = True + + elif service_type == "AiChatText": + # 文本代理服务 + # 等待客户端发送初始请求数据 进行大模型对话代理时,需要前端连接后发送payload + payload = await websocket.receive_json() + completions_url = f"http://localhost:9380/api/v1/chats/{chat_id}/completions" + await proxy_aichat_text_stream(connection_id, completions_url, payload) + completed_successfully = True + else: + # 使用引擎的生成器直接获取音频流 + async for data in tts_engine.get_audio_stream(audio_stream_id): + if not await manager.send_bytes(connection_id, data): + logging.warning(f"Send failed, connection closed: {connection_id}") + break + + completed_successfully = True + + # 发送完成信号前检查连接状态 + if manager.is_connected(connection_id): + # 发送完成信号 + await manager.send_json(connection_id, {"status": "completed"}) + + # 添加短暂延迟确保消息送达 + await asyncio.sleep(0.1) + + # 主动关闭WebSocket连接 + await manager.disconnect(connection_id, code=1000, reason="Audio stream completed") + except WebSocketDisconnect: + logging.info(f"WebSocket disconnected: {connection_id}") + except Exception as e: + logging.error(f"WebSocket TTS error: {str(e)}") + if manager.is_connected(connection_id): + await manager.send_json(connection_id, {"error": str(e)}) + finally: + pass + # await manager.disconnect(connection_id) + + def cleanup_cache(): """清理过期缓存""" with cache_lock: @@ -1022,4 +1771,4 @@ def cleanup_cache(): del audio_text_cache[key] # 应用启动时启动清理线程 -# start_background_cleaner() \ No newline at end of file +# start_background_cleaner() diff --git a/asr-monitor-test/backup.txt b/asr-monitor-test/backup.txt new file mode 100644 index 00000000..14c0b965 --- /dev/null +++ b/asr-monitor-test/backup.txt @@ -0,0 +1,263 @@ +""" +ALI_KEY = "sk-a47a3fb5f4a94f66bbaf713779101c75" +class QwenTTS: + def __init__(self, key,format="mp3",sample_rate=44100, model_name="cosyvoice-v1/longxiaochun"): + import dashscope + print("---begin--init dialog_service QwenTTS--") # cyx + self.model_name = model_name + dashscope.api_key = key + self.synthesizer = None + self.callback = None + self.is_cosyvoice = False + self.voice = "" + self.format = format + self.sample_rate = sample_rate + if '/' in model_name: + parts = model_name.split('/', 1) + # 返回分离后的两个字符串parts[0], parts[1] + if parts[0] == 'cosyvoice-v1': + self.is_cosyvoice = True + self.voice = parts[1] + + def init_streaming_call(self, audio_call_back): + from dashscope.api_entities.dashscope_response import SpeechSynthesisResponse + # cyx 2025 01 19 测试cosyvoice 使用tts_v2 版本 + from dashscope.audio.tts_v2 import ResultCallback, SpeechSynthesizer, AudioFormat # , SpeechSynthesisResult + from dashscope.audio.tts import SpeechSynthesisResult + from collections import deque + + print(f"--QwenTTS--tts_stream begin-- {self.is_cosyvoice} {self.voice}") # cyx + + class Callback_v2(ResultCallback): + def __init__(self) -> None: + self.dque = deque() + + def _run(self): + while True: + if not self.dque: + time.sleep(0) + continue + val = self.dque.popleft() + if val: + yield val + else: + break + + def on_open(self): + logging.info("Qwen tts open") + pass + + def on_complete(self): + self.dque.append(None) + + def on_error(self, response: SpeechSynthesisResponse): + print("Qwen tts error", str(response)) + raise RuntimeError(str(response)) + + def on_close(self): + # print("---Qwen call back close") # cyx + logging.info("Qwen tts close") + pass + + # canceled for test 语音大模型CosyVoice + #def on_event(self, result: SpeechSynthesisResult): + # if result.get_audio_frame() is not None: + # self.dque.append(result.get_audio_frame()) + # + + def on_event(self, message): + # print(f"recv speech synthsis message {message}") + pass + + # 以下适合语音大模型CosyVoice + def on_data(self, data: bytes) -> None: + if len(data) > 0: + #self.dque.append(data) + if audio_call_back: + audio_call_back(data) + # -------------------------- + + # text = self.normalize_text(text) + + try: + self.callback = Callback_v2() + format =self.get_audio_format(self.format,self.sample_rate) + self.synthesizer = SpeechSynthesizer( + model='cosyvoice-v1', + # voice="longyuan", #"longfei", + voice=self.voice, + callback=self.callback, + format=format + ) + #self.synthesizer.call(text) + except Exception as e: + print(f"---dale---20 error {e}") # cyx + # ----------------------------------- + # print(f"---Qwen return data {num_tokens_from_string(text)}") + # yield num_tokens_from_string(text) + + except Exception as e: + raise RuntimeError(f"**ERROR**: {e}") + + def get_audio_format(self, format: str, sample_rate: int): + # 动态获取音频格式 + from dashscope.audio.tts_v2 import AudioFormat + logging.info(f"QwenTTS--get_audio_format-- {format} {sample_rate}") + format_map = { + (8000, 'mp3'): AudioFormat.MP3_8000HZ_MONO_128KBPS, + (8000, 'pcm'): AudioFormat.PCM_8000HZ_MONO_16BIT, + (8000, 'wav'): AudioFormat.WAV_8000HZ_MONO_16BIT, + (16000, 'pcm'): AudioFormat.PCM_16000HZ_MONO_16BIT, + (22050, 'mp3'): AudioFormat.MP3_22050HZ_MONO_256KBPS, + (22050, 'pcm'): AudioFormat.PCM_22050HZ_MONO_16BIT, + (22050, 'wav'): AudioFormat.WAV_22050HZ_MONO_16BIT, + (44100, 'mp3'): AudioFormat.MP3_44100HZ_MONO_256KBPS, + (44100, 'pcm'): AudioFormat.PCM_44100HZ_MONO_16BIT, + (44100, 'wav'): AudioFormat.WAV_44100HZ_MONO_16BIT, + (48800, 'mp3'): AudioFormat.MP3_48000HZ_MONO_256KBPS, + (48800, 'pcm'): AudioFormat.PCM_48000HZ_MONO_16BIT, + (48800, 'wav'):AudioFormat.WAV_48000HZ_MONO_16BIT + + } + return format_map.get((sample_rate, format), AudioFormat.MP3_16000HZ_MONO_128KBPS) + def streaming_call(self,text): + if self.synthesizer: + self.synthesizer.streaming_call(text) + def end_streaming_call(self): + if self.synthesizer: + self.synthesizer.streaming_complete() + +class StreamSessionManager1: + def __init__(self): + self.sessions = {} # {session_id: {'tts_model': obj, 'buffer': queue, 'task_queue': Queue}} + self.lock = threading.Lock() + self.executor = ThreadPoolExecutor(max_workers=30) # 固定大小线程池 + self.gc_interval = 300 # 5分钟清理一次 5 x 60 300秒 + self.gc_tts = 10 # 10s 大模型开始输出文本有可能需要比较久,2025年5 24 从3s->10s + self.inited = False + self.tts_model = None + def create_session(self, tts_model,sample_rate =8000, stream_format='mp3'): + session_id = str(uuid.uuid4()) + with self.lock: + self.sessions[session_id] = { + 'tts_model': tts_model, + 'buffer': queue.Queue(maxsize=300), # 线程安全队列 + 'task_queue': queue.Queue(), + 'active': True, + 'last_active': time.time(), + 'audio_chunk_count':0, + 'finished': threading.Event(), # 添加事件对象 + 'sample_rate':sample_rate, + 'stream_format':stream_format, + "tts_chunk_data_valid":False + } + # 启动任务处理线程 + threading.Thread(target=self._process_tasks, args=(session_id,), daemon=True).start() + + + return session_id + + def append_text(self, session_id, text): + with self.lock: + session = self.sessions.get(session_id) + if not session: return + # 将文本放入任务队列(非阻塞) + #logging.info(f"StreamSessionManager append_text {text}") + try: + session['task_queue'].put(text, block=False) + except queue.Full: + logging.warning(f"Session {session_id} task queue full") + + def _process_tasks(self, session_id): + #任务处理线程(每个会话独立) + session = self.sessions.get(session_id) + def audio_call_back(chunk): + logging.info(f"audio_call_back {len(chunk)}") + if session['stream_format'] == 'wav': + if first_chunk: + chunk_len = len(chunk) + if chunk_len > 2048: + session['buffer'].put(audio_fade_in(chunk, 1024)) + else: + session['buffer'].put(audio_fade_in(chunk, chunk_len)) + first_chunk = False + else: + session['buffer'].put(chunk) + else: + session['buffer'].put(chunk) + session['last_active'] = time.time() + session['audio_chunk_count'] = session['audio_chunk_count'] + 1 + if session['tts_chunk_data_valid'] is False: + session['tts_chunk_data_valid'] = True # 20250510 增加,表示连接TTS后台已经返回,可以通知前端了 + while True: + if not session or not session['active']: + break + if not self.inited: + self.inited = True + self.tts_model = QwenTTS(ALI_KEY, session['stream_format'], session['sample_rate']) + self.tts_model.init_streaming_call(audio_call_back) + try: + #logging.info(f"StreamSessionManager _process_tasks {session['task_queue'].qsize()}") + # 合并多个文本块(最多等待50ms) + texts = [] + while len(texts) < 5: # 最大合并5个文本块 + try: + text = session['task_queue'].get(timeout=0.1) + #logging.info(f"StreamSessionManager _process_tasks --0 {len(texts)}") + texts.append(text) + except queue.Empty: + break + + if texts: + session['last_active'] = time.time() # 如果有处理文本,重置活跃时间 + # 提交到线程池处理 + future=self.executor.submit( + self._generate_audio, + session_id, + ' '.join(texts) # 合并文本减少请求次数 + ) + future.result() # 等待转换任务执行完毕 + session['last_active'] = time.time() + # 会话超时检查 + if time.time() - session['last_active'] > self.gc_interval: + self.close_session(session_id) + break + if time.time() - session['last_active'] > self.gc_tts: + session['finished'].set() + if self.tts_model: + self.tts_model.end_streaming_call() + break + + except Exception as e: + logging.error(f"Task processing error: {str(e)}") + + def _generate_audio(self, session_id, text): + #实际生成音频(线程池执行) + session = self.sessions.get(session_id) + if not session: return + # logging.info(f"_generate_audio:{text}") + first_chunk = True + + try: + self.tts_model.streaming_call(text) + except Exception as e: + session['buffer'].put(f"ERROR:{str(e)}") + logging.info(f"--streaming_call--error {str(e)}") + + + def close_session(self, session_id): + with self.lock: + if session_id in self.sessions: + # 标记会话为不活跃 + self.sessions[session_id]['active'] = False + # 延迟2秒后清理资源 + threading.Timer(1, self._clean_session, args=[session_id]).start() + + def _clean_session(self, session_id): + with self.lock: + if session_id in self.sessions: + del self.sessions[session_id] + + def get_session(self, session_id): + return self.sessions.get(session_id) +""" diff --git a/asr-monitor-test/payment_cert/apiclient_cert.p12 b/asr-monitor-test/payment_cert/apiclient_cert.p12 new file mode 100644 index 00000000..bef9df54 Binary files /dev/null and b/asr-monitor-test/payment_cert/apiclient_cert.p12 differ diff --git a/asr-monitor-test/payment_cert/apiclient_cert.pem b/asr-monitor-test/payment_cert/apiclient_cert.pem new file mode 100644 index 00000000..8cbc815f --- /dev/null +++ b/asr-monitor-test/payment_cert/apiclient_cert.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEKzCCAxOgAwIBAgIUYpLQpNewkuNh1t0iwv14MUedSNkwDQYJKoZIhvcNAQEL +BQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT +FFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg +Q0EwHhcNMjUwNzA3MjMwMTM4WhcNMzAwNzA2MjMwMTM4WjCBhDETMBEGA1UEAwwK +MTcyMTMwMTAwNjEbMBkGA1UECgwS5b6u5L+h5ZWG5oi357O757ufMTAwLgYDVQQL +DCfljY7lsI/ljZrvvIjljJfkuqzvvInnp5HmioDmnInpmZDlhazlj7gxCzAJBgNV +BAYTAkNOMREwDwYDVQQHDAhTaGVuWmhlbjCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBANp0L8eLiwxVMPWRWxEWbfLgg7V91CHIM09EnvepPIdgNfUdtBcv +89XVBjs+1iNhv/74YgW01xgZQbmYCVFmUSme8LzZx7gIFqZH0Hm5CNGET+IGNLIs +ENkI8I+4N0fRsYddFJEqQzkpFMFQACxVldna9fO57jTuJtNCFD97x450S6BEsriP +OmvsVJTwrS+oZx+Qkme8HDvmeeQUOxNuWfvxyc1wgCrOzgob3iVT4wd7eKjqQm0v +r3W8ptD5KMUsqJrBy5s75RGaeDyEJaVVv04tk4kQQ/Yj7RWZChxPExYlqotJAbE4 +jZT0Hfs4Z/0lSbutGlVofJhQCuZ7sH6E/KUCAwEAAaOBuTCBtjAJBgNVHRMEAjAA +MAsGA1UdDwQEAwID+DCBmwYDVR0fBIGTMIGQMIGNoIGKoIGHhoGEaHR0cDovL2V2 +Y2EuaXRydXMuY29tLmNuL3B1YmxpYy9pdHJ1c2NybD9DQT0xQkQ0MjIwRTUwREJD +MDRCMDZBRDM5NzU0OTg0NkMwMUMzRThFQkQyJnNnPUhBQ0M0NzFCNjU0MjJFMTJC +MjdBOUQzM0E4N0FEMUNERjU5MjZFMTQwMzcxMA0GCSqGSIb3DQEBCwUAA4IBAQBC +KwLHbLPFH2N7ge8SMVaSj/4wd2Yak0dC/786Rv+dHlVlOKhpEZuCRtIe+3D/PJWX +tdPZwjMICDSKMbbNt+UW4IPrpQeWqc3M8vx1nS3k/iYpOBNoy2e0IKhJi1inOlWq +4iRSpUIG/EKqPkjDs6GouROznzYXY85RNmVrCH/k19So4eSNrYe6D30rkQfklYGO +PUJMCvjZrcNrZa0Ka/xAI9VHRB814WXDvnBxEDaGHena8SQpPs5p0ikT0C5sVYt8 +QQ8D+ZZe7gkod5dknt8X8XXFSAMxYko4+O/aUIMql76kMFcw0XcDQ8G8miRhhMHJ +DTTchZFuZnYvYYuzl9JN +-----END CERTIFICATE----- diff --git a/asr-monitor-test/payment_cert/apiclient_key.pem b/asr-monitor-test/payment_cert/apiclient_key.pem new file mode 100644 index 00000000..01fd0ce4 --- /dev/null +++ b/asr-monitor-test/payment_cert/apiclient_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDadC/Hi4sMVTD1 +kVsRFm3y4IO1fdQhyDNPRJ73qTyHYDX1HbQXL/PV1QY7PtYjYb/++GIFtNcYGUG5 +mAlRZlEpnvC82ce4CBamR9B5uQjRhE/iBjSyLBDZCPCPuDdH0bGHXRSRKkM5KRTB +UAAsVZXZ2vXzue407ibTQhQ/e8eOdEugRLK4jzpr7FSU8K0vqGcfkJJnvBw75nnk +FDsTbln78cnNcIAqzs4KG94lU+MHe3io6kJtL691vKbQ+SjFLKiawcubO+URmng8 +hCWlVb9OLZOJEEP2I+0VmQocTxMWJaqLSQGxOI2U9B37OGf9JUm7rRpVaHyYUArm +e7B+hPylAgMBAAECggEAC8DQiUXit0kmFzA43BR+2eBmda9NaHvi9tLUUrLSRN5S +SNvOQqkbz1dkvG9HCiRKNiea7n+qSuug86FQbwa4OysH/MEi063rugqHFuVzqgIa +Yii/UQ654VNvzeu1XbHUgVnqr1y8vSiOq0/oknoQU+ZJ8VEBletyP0+A02GPfQR/ +RqTrNgUgXzb1dDNdtKIe9IFDhhsRCZ3qi5oJsHlCAYUgod7c4kCqvDX6mAOqgNAF +kWULrdq7Rh2hJf0S4M3AF/46wybXauL+Wn1xK9VChEboM67sJ/F8LKfLuUvqMUnN +46ilayIyCDdRLng1yEFAoUBNQEMsf1V7CGECSSSqwQKBgQD7W2JmPBKU7O4SQmnN +yxMyE5501V9OyRgolPQdeLoLubj3ZlNifrBNC3Dj6vfsumAZwyHwF14JXh0EzJHG +txnLceN2J68FyvbEOw1S98ifOJYHpvsOS5/9r+uyGdcEATQMuLw0whAcp1T3Y0BF +8HKsRI5tpGqGgi37ZL/VMjYnWQKBgQDefTXNpwIQMs7ff1YXAdrKkID/ROyepGXB +u/0G6jWBUKkOTQNcB8cEfRdjgOk2AyiOkV9neFkwtBdWPzJRzPmtjOSF2ajbFazC +2rVuiadh5ZH+aJuu2eEBGivv0R/0kRiXeOsbXKwPLIsn6n20h5zqPka3GvZa9+58 +RuwEYXBiLQKBgQCL0z081AQeNmMFY06KPYKzI7jNE3lOUY2P3bSixGryZOFhNtoB ++6nFYiztjONYHCGjkypI7ibQnTsVVVtuqKK/yt4W92JknZCCfrsdwVsoP4kuPpSA +Uk9xBzDdRYSX5Ld4sDd6Pc5KskcQy3SQs36HGCgHFCRyKO69X0Fbru/zGQKBgC6I +i5c+pdzTc5clH9FiDuus+33oYYDwq2OwuMQYeiZYw3L9QoWeDs7uhtTF4oDsejAP +UZ/neOgJ0pO0Vgbr0xCsViN0ma9wwhhi++1plvuPs1A9espAQaIkYiofWAqjyjvs +C2hGoqntzBEGJ1J5xqTrb4jed8Yg8t1FTBnCc2nlAoGABIwyFvJz/Lz/SOEUTFf1 +KTQN4AT9T1ywcqp0fFuuFMRAyQFwVArOV/KGu/ZUkG3Cndt4pzzYMTpsMsAhN7id +abWL0QyzeO2vsBKI5FEC4uj5zxeEvHH+QIfKf6qL2cWWXIoEDVsRv6jLI/nCj4Lt +ty2lFdcrOzSIFC36iURd0ps= +-----END PRIVATE KEY----- diff --git a/asr-monitor-test/payment_cert/cert_serial_no.txt b/asr-monitor-test/payment_cert/cert_serial_no.txt new file mode 100644 index 00000000..085e43a1 --- /dev/null +++ b/asr-monitor-test/payment_cert/cert_serial_no.txt @@ -0,0 +1 @@ +6292D0A4D7B092E361D6DD22C2FD7831479D48D9 \ No newline at end of file diff --git a/asr-monitor-test/payment_cert/璇佷功浣跨敤璇存槑.txt b/asr-monitor-test/payment_cert/璇佷功浣跨敤璇存槑.txt new file mode 100644 index 00000000..041befb4 --- /dev/null +++ b/asr-monitor-test/payment_cert/璇佷功浣跨敤璇存槑.txt @@ -0,0 +1,18 @@ +欢迎使用微信支付! +附件中的三份文件(证书pkcs12格式、证书pem格式、证书密钥pem格式),为接口中强制要求时需携带的证书文件。 +证书属于敏感信息,请妥善保管不要泄露和被他人复制。 +不同开发语言下的证书格式不同,以下为说明指引: + 证书pkcs12格式(apiclient_cert.p12) + 包含了私钥信息的证书文件,为p12(pfx)格式,由微信支付签发给您用来标识和界定您的身份 + 部分安全性要求较高的API需要使用该证书来确认您的调用身份 + windows上可以直接双击导入系统,导入过程中会提示输入证书密码,证书密码默认为您的商户号(如:1900006031) + 证书pem格式(apiclient_cert.pem) + 从apiclient_cert.p12中导出证书部分的文件,为pem格式,请妥善保管不要泄漏和被他人复制 + 部分开发语言和环境,不能直接使用p12文件,而需要使用pem,所以为了方便您使用,已为您直接提供 + 您也可以使用openssl命令来自己导出:openssl pkcs12 -clcerts -nokeys -in apiclient_cert.p12 -out apiclient_cert.pem + 证书密钥pem格式(apiclient_key.pem) + 从apiclient_cert.p12中导出密钥部分的文件,为pem格式 + 部分开发语言和环境,不能直接使用p12文件,而需要使用pem,所以为了方便您使用,已为您直接提供 + 您也可以使用openssl命令来自己导出:openssl pkcs12 -nocerts -in apiclient_cert.p12 -out apiclient_key.pem +备注说明: + 由于绝大部分操作系统已内置了微信支付服务器证书的根CA证书, 2018年3月6日后, 不再提供CA证书文件(rootca.pem)下载 \ No newline at end of file diff --git a/asr-monitor-test/requirements.txt b/asr-monitor-test/requirements.txt index 7d1238e9..cd6adae4 100644 --- a/asr-monitor-test/requirements.txt +++ b/asr-monitor-test/requirements.txt @@ -10,6 +10,7 @@ python-jose[cryptography]==3.3.0 # 兼容 Python3 的 JOSE 实现 pycryptodome==3.20.0 # 替代旧版 pycrypto 的现代加密库 requests>=2.25.1,<3.0.0 +httpx # MySQL 驱动 pymysql==1.1.0 @@ -18,4 +19,8 @@ pymysql==1.1.0 dbutils==3.1.1 tenacity>=8.2.3 # 推荐使用最新稳定版 +openai>=1.0.0 # 这是OpenAI官方Python SDK +python-dotenv>=0.19.0 # 用于加载.env文件 +cryptography>=42.0.0 +python-dateutil diff --git a/asr-monitor-test/run_app.sh b/asr-monitor-test/run_app.sh new file mode 100644 index 00000000..eae35d57 --- /dev/null +++ b/asr-monitor-test/run_app.sh @@ -0,0 +1,74 @@ +#!/bin/bash + +# 1. 终止占用9580端口的进程 +PORT=9580 +echo "➜ 正在检查端口 ${PORT} 的占用情况..." +PID=$(lsof -ti :${PORT}) + +if [ -n "$PID" ]; then + echo "➜ 发现占用端口的进程(PID: ${PID}),正在终止..." + kill -9 $PID + sleep 2 # 等待进程完全终止 + echo "✓ 已终止进程" +else + echo "✓ 端口 ${PORT} 未被占用" +fi + +# 2. 激活虚拟环境并设置PYTHONPATH +source venv/bin/activate +export PYTHONPATH=.:$PYTHONPATH + +# 3. 使用nohup运行并防止终端退出影响 +echo "➜ 启动应用进程..." +nohup python app/main.py > app.log 2>&1 & + +# 4. 验证新进程 +sleep 3 # 等待进程启动 +NEW_PID=$(lsof -ti :${PORT}) +if [ -n "$NEW_PID" ]; then + echo "✓ 应用启动成功!PID: ${NEW_PID}" + echo "➜ 日志输出已重定向到 app.log" + echo "➜ 使用以下命令查看日志:" + echo " tail -f app.log" +else + echo "✗ 应用启动失败,请检查日志" + exit 1 +fi + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/asr-monitor-test/start.sh b/asr-monitor-test/start.sh index 2767fdd3..12eeb3b7 100755 --- a/asr-monitor-test/start.sh +++ b/asr-monitor-test/start.sh @@ -1,2 +1,16 @@ +# 1. 终止占用9580端口的进程 +PORT=9580 +echo "➜ 正在检查端口 ${PORT} 的占用情况..." +PID=$(lsof -ti :${PORT}) + +if [ -n "$PID" ]; then + echo "➜ 发现占用端口的进程(PID: ${PID}),正在终止..." + kill -9 $PID + sleep 2 # 等待进程完全终止 + echo "✓ 已终止进程" +else + echo "✓ 端口 ${PORT} 未被占用" +fi + source venv/bin/activate export PYTHONPATH=.:$PYTHONPATH && python app/main.py diff --git a/rag/utils/minio_conn.py b/rag/utils/minio_conn.py index 11682988..69805b12 100644 --- a/rag/utils/minio_conn.py +++ b/rag/utils/minio_conn.py @@ -98,6 +98,50 @@ class RAGFlowMinio(object): time.sleep(1) return + def list_objects(self, bucket: str, prefix: str = "", recursive: bool = True) -> list[dict]: + """ + 列出存储桶中指定前缀的所有对象 + + :param bucket: 存储桶名称 + :param prefix: 对象前缀(目录路径) + :param recursive: 是否递归列出 + :return: 对象信息列表 [{"name": str, "size": int, "last_modified": datetime}, ...] + """ + objects = [] + for attempt in range(3): + try: + # 确保存储桶存在 + if not self.conn.bucket_exists(bucket): + logging.warning(f"存储桶不存在: {bucket}") + return [] + + # 列出对象 + result = self.conn.list_objects(bucket, prefix=prefix, recursive=recursive) + + # 收集对象信息 + for obj in result: + objects.append({ + "name": obj.object_name, + "size": obj.size, + "last_modified": obj.last_modified, + "etag": obj.etag, + "content_type": obj.content_type + }) + + return objects + except S3Error as e: + if e.code == "NoSuchBucket": + logging.warning(f"存储桶不存在: {bucket}") + return [] + logging.exception(f"列出对象时发生S3错误: {e}") + except Exception as e: + logging.exception(f"列出对象失败 (尝试 {attempt + 1}/3): {e}") + + # 重连并等待 + self.__open__() + time.sleep(1) + + return [] MINIO = RAGFlowMinio()