import gradio as gr import os # ===== 你的自定义模块 ===== from config import APP_TITLE, CHATBOT_HEIGHT, MIN_WIDTH_LEFT, MIN_WIDTH_RIGHT from styles import MAHJONG_THEME_CSS # Extra CSS to force dark & counter browser auto-invert EXTRA_FIX_CSS = '''/* === Force dark site and neutralize browser-side auto-invert === */ html, body{ background-color:#0b1220 !important; color-scheme: dark !important; } /* When browser/extension applies global invert, add a counter-invert on our app root */ html.force-dark-fix #app-root{ filter: invert(1) hue-rotate(180deg) !important; }''' # Redirect to ?__theme=dark FORCE_DARK_REDIRECT = '''''' # Detect browser force-dark and set html class DETECT_FORCE_DARK = '''''' from ai_service import design_mahjong_game # 优先尝试原生流式;没有则自动回退为伪流式 try: from ai_service import design_mahjong_game_stream except Exception: design_mahjong_game_stream = None # ====== 🔧 Hotfix: 兼容 gradio_client 对 additionalProperties(bool) 的解析 ====== try: import gradio_client.utils as _gc_utils _orig_get_type = _gc_utils.get_type def _safe_get_type(schema): if isinstance(schema, bool): return "any" if schema else "never" return _orig_get_type(schema) _gc_utils.get_type = _safe_get_type _orig_json2py = _gc_utils._json_schema_to_python_type def _safe_json2py(schema, defs): if isinstance(schema, bool): return "any" if schema else "never" if isinstance(schema, dict) and "additionalProperties" in schema: ap = schema["additionalProperties"] if isinstance(ap, bool): inner = "any" if ap else "never" return f"dict[str, {inner}]" return _orig_json2py(schema, defs) _gc_utils._json_schema_to_python_type = _safe_json2py _orig_json_schema_to_python_type = _gc_utils.json_schema_to_python_type def _safe_json_schema_to_python_type(schema): if isinstance(schema, bool): return "any" if schema else "never" return _orig_json_schema_to_python_type(schema) _gc_utils.json_schema_to_python_type = _safe_json_schema_to_python_type except Exception: pass # ====== 🔧 Hotfix 结束 ====== # ==================== 工具函数 ==================== def _messages_to_tuples(history): """ 将 Chatbot 的 messages 格式([{role, content}, ...])转换为 [(user, bot), ...]。 若已是 tuples,则原样返回。 """ if not history: return [] if isinstance(history, list) and history and isinstance(history[0], dict): pairs = [] last_user = None for msg in history: role = msg.get("role") content = msg.get("content", "") if role == "user": last_user = content elif role == "assistant": pairs.append((last_user or "", content)) last_user = None return pairs return history def _chunk_fake_stream(text, step=40): """把整段文本切成小块,伪流式输出。""" s = str(text or "") for i in range(0, len(s), step): yield s[i:i + step] def _load_base64_image(path): """读取本地图片并返回 data URI""" import base64 import pathlib p = pathlib.Path(path) if not p.exists(): return "" try: data = base64.b64encode(p.read_bytes()).decode("ascii") suffix = p.suffix.lower() mime = "image/png" if suffix == ".png" else "image/jpeg" return f"data:{mime};base64,{data}" except Exception: return "" HERO_TILE_PATHS = ["UI00001.jpg", "UI00002.jpg", "UI00003.jpg", "UI00004.jpg"] HERO_TILE_IMAGES = [_load_base64_image(p) for p in HERO_TILE_PATHS] def _render_hero_tiles(): tiles = [ f'' for idx, src in enumerate(HERO_TILE_IMAGES) if src ] return "".join(tiles) def clear_cache(): """清空缓存""" from cache_manager import file_cache, request_cache file_cache.clear() request_cache.clear() return "✅ 缓存已清空" def clear_files(): """清空文件上传""" return None def update_file_status(files): """更新文件状态显示""" if not files: return "📁 文件状态:未上传" file_list = files if isinstance(files, (list, tuple)) else [files] names = [] for f in file_list: if isinstance(f, str): names.append(os.path.basename(f)) else: names.append(os.path.basename(getattr(f, "name", str(f)))) head = "\n".join(f" • {n}" for n in names[:3]) tail = "\n ..." if len(names) > 3 else "" return f"📁 文件状态:已上传 {len(names)} 个文件\n{head}{tail}" def export_history_to_markdown(history): """将聊天历史导出为 Markdown 文件,并返回文件路径供下载""" import time import pathlib from datetime import datetime pairs = _messages_to_tuples(history) lines = ["# 对话记录", ""] lines.append(f"- 导出时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") lines.append("") for idx, (user_msg, bot_msg) in enumerate(pairs, start=1): lines.append(f"## 轮次 {idx}") if user_msg: lines.append("**用户**:\n") lines.append(user_msg) lines.append("") if bot_msg: lines.append("**助手**:\n") lines.append(bot_msg) lines.append("") content = "\n".join(lines).strip() + "\n" export_dir = pathlib.Path("exports") export_dir.mkdir(parents=True, exist_ok=True) filename = export_dir / f"chat_history_{int(time.time())}.md" with open(filename, "w", encoding="utf-8") as f: f.write(content) return str(filename) # ==================== 新增:提取 GDL 和自然语言描述 ==================== def extract_gdl_and_narrative(content): """提取 GDL 和自然语言部分(支持多种格式变体)""" import re # 定义多种可能的标记格式(按优先级排序,支持更多变体) gdl_patterns = [ r"##\s*m?GDL\s*描述", # ## GDL描述 / ## mGDL 描述 r"m?GDL\s*描述", # GDL描述 / mGDL 描述 r"##\s*GDL\s*Description", # ## GDL Description(英文) r"##\s*m?GDL", # ## GDL / ## mGDL r"m?GDL\s*规则代码", # mGDL 规则代码 r"m?GDL\s*规则", # mGDL 规则 ] narrative_patterns = [ r"##\s*自然语言规则说明", # ## 自然语言规则说明 r"##\s*自然语言规则", # ## 自然语言规则 r"自然语言规则说明", # 自然语言规则说明 r"自然语言规则", # 自然语言规则 r"##\s*规则说明", # ## 规则说明 r"规则说明", # 规则说明 ] # 尝试查找 GDL 部分(支持大小写不敏感) gdl_start = -1 for pattern in gdl_patterns: match = re.search(pattern, content, re.IGNORECASE) if match: gdl_start = match.start() break # 尝试查找自然语言部分(支持大小写不敏感) narrative_start = -1 for pattern in narrative_patterns: match = re.search(pattern, content, re.IGNORECASE) if match: narrative_start = match.start() break if gdl_start != -1 and narrative_start != -1: # 确保顺序正确(GDL应该在自然语言之前) if gdl_start >= narrative_start: gdl_start, narrative_start = narrative_start, gdl_start # 获取 GDL 部分(从标记开始到自然语言标记之前) gdl_content = content[gdl_start:narrative_start].strip() # 获取自然语言部分(从标记开始到结尾) narrative_content = content[narrative_start:].strip() return gdl_content, narrative_content elif gdl_start != -1: # 只找到GDL,将后面全部作为GDL print(f"⚠️ 仅找到GDL标记,将其后内容作为GDL") return content[gdl_start:].strip(), "" elif narrative_start != -1: # 只找到自然语言,将后面全部作为自然语言 print(f"⚠️ 仅找到自然语言标记,将其后内容作为自然语言") return "", content[narrative_start:].strip() else: # 都没找到,返回空 print(f"⚠️ 提取警告: 未找到GDL和自然语言标记") return "", "" def save_gdl_and_narrative(gdl_content, narrative_content): """保存 GDL 和自然语言内容到文件""" # 定义文件存储路径 export_dir = os.path.join("exports") os.makedirs(export_dir, exist_ok=True) gdl_file_path = os.path.join(export_dir, "gdl_output.txt") narrative_file_path = os.path.join(export_dir, "narrative_output.txt") # 保存 GDL with open(gdl_file_path, "w", encoding="utf-8") as f: f.write(gdl_content) # 保存自然语言 with open(narrative_file_path, "w", encoding="utf-8") as f: f.write(narrative_content) return gdl_file_path, narrative_file_path # ==================== Gradio 界面(Mahjong Skin) ==================== with gr.Blocks( theme=gr.themes.Soft(primary_hue='green', neutral_hue='slate'), title=APP_TITLE, css=MAHJONG_THEME_CSS + EXTRA_FIX_CSS, elem_id='app-root' ) as demo: gr.HTML(FORCE_DARK_REDIRECT) gr.HTML(DETECT_FORCE_DARK) # 顶部 Hero hero_tiles_html = _render_hero_tiles() gr.HTML(f"""