import os
import sys
import json
import re
import tempfile
import urllib.request
import urllib.error
import urllib.parse
import mysql.connector
import datetime
import random
import base64
import socket
from typing import Optional, List
from flask import Flask, render_template, request, jsonify, url_for, send_file, redirect
from html.parser import HTMLParser
from urllib.parse import urljoin, urlparse
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
def _load_dotenv_if_exists():
"""
允许把数据库/AI 等配置放在同目录 config.env 里(不改代码即可生效)。
重要:需要在读取 os.getenv(...) 之前执行。
"""
try:
from dotenv import load_dotenv # type: ignore
except Exception:
return
# 兼容源码运行 / PyInstaller 打包运行
candidates = [
os.path.join(BASE_DIR, "config.env"),
os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), "config.env"),
]
for p in candidates:
if os.path.exists(p):
load_dotenv(p, override=True)
break
_load_dotenv_if_exists()
def resource_path(*parts: str) -> str:
"""
资源路径兼容:
- 源码运行:基于当前文件目录
- PyInstaller 打包:基于 _MEIPASS
"""
base = getattr(sys, "_MEIPASS", BASE_DIR)
return os.path.join(base, *parts)
app = Flask(
__name__,
template_folder=resource_path("templates"),
static_folder=resource_path("static"),
)
# 可能的主键列名(不同库/表可能不一致)
_PK_CANDIDATE_COLS = ["id", "question_id", "pk_id", "qid"]
_PK_COLS_CACHE: Optional[List[str]] = None
# 数据库配置
# 默认值:按你刚刚给的 JDBC 配置来(也支持用环境变量覆盖)
DB_CONFIG = {
"host": os.getenv("DB_HOST", "rm-f8ze60yirdj8786u2wo.mysql.rds.aliyuncs.com"),
"port": int(os.getenv("DB_PORT", "3306")),
"database": os.getenv("DB_DATABASE", "math-conten-online2"),
"user": os.getenv("DB_USERNAME", "root"),
"password": os.getenv("DB_PASSWORD", "csqz@20255"),
"charset": "utf8mb4",
}
# 远程 PDF 生成接口(你提供的)
PDF_API_URL = os.getenv("PDF_API_URL", "https://teaching-content.chunsunqiuzhu.com/api/questions/pdf")
PDF_STUDENT_ID = os.getenv("PDF_STUDENT_ID", "44") # 默认写死 44(也支持用环境变量覆盖)
# AI 优化题干(通过环境变量配置,避免把 key 写进代码)
AI_API_KEY = os.getenv("AI_API_KEY", "").strip()
AI_BASE_URL = (os.getenv("AI_BASE_URL") or "").strip() # 为空则使用官方默认
AI_MODEL_NAME = (os.getenv("AI_MODEL_NAME") or "gpt-5.2").strip()
# 难度评分配置(使用相同的 AI 配置)
DIFFICULTY_SCORING_API_KEY = os.getenv("DIFFICULTY_SCORING_API_KEY", AI_API_KEY).strip()
DIFFICULTY_SCORING_MODEL = os.getenv("DIFFICULTY_SCORING_MODEL", AI_MODEL_NAME).strip()
DIFFICULTY_SCORING_TEMPERATURE = float(os.getenv("DIFFICULTY_SCORING_TEMPERATURE", "0.0"))
DIFFICULTY_SCORING_TOP_P = float(os.getenv("DIFFICULTY_SCORING_TOP_P", "0.3"))
DIFFICULTY_SCORING_PRESENCE_PENALTY = float(os.getenv("DIFFICULTY_SCORING_PRESENCE_PENALTY", "0.0"))
DIFFICULTY_SCORING_FREQUENCY_PENALTY = float(os.getenv("DIFFICULTY_SCORING_FREQUENCY_PENALTY", "0.0"))
# 默认难度评分提示词
DIFFICULTY_SCORING_PROMPT = """你是一名数学题库难度建模专家。你的任务不是解题,而是基于题目文本、公式及图像信息,对题目难度进行结构化量化评估,用于企业级题库难度算法。本评估包含四个评分维度,四个维度等权重参与最终得分,但在判断时需遵循重要性顺序:推理与运算步数最高,知识点数量第二,抽象与构造要求第三,题型常规性最低。每个维度的取值只能是 0.33、0.66 或 1.00,不允许出现其他数值。(1)推理与运算步数:直接或少量步骤取 0.33,中等连续步骤取 0.66,多步链式推理或存在中间结论取 1.00;(2)知识点数量:单一核心知识点取 0.33,两个知识点取 0.66,三个及以上或跨模块综合取 1.00;(3)抽象与构造要求:无需抽象或构造取 0.33,需要一定抽象或简单构造取 0.66,需要明显构造或较高层次建模取 1.00;(4)题型常规性:高度常规模板题取 0.33,存在一定变式取 0.66,非典型或创新题型取 1.00。四个维度得分相加后需归一化为 0~1 的 total_score,并按以下规则映射难度等级:0~0.25 为"筑基",0.25~0.5 为"提分",0.5 以上为"培优"。请仅以严格 JSON 格式输出 dimension_scores、total_score 和 difficulty_level,不得输出其他字段,不得展开解题过程。"""
AI_STEM_PROMPT_DEFAULT = r"""你是“题干格式修复器”,不是解题老师。
你的任务:把输入题干(stem)做最小幅度的格式清洗,使其更适合 PDF / 系统展示。
【核心原则:原文已兼容就不要改】(最重要,违反此条视为失败)
- 如果原文已经是 PDF 兼容格式(例如:数字、单位、上标³、普通文本混合),就不要做任何改动。
- **严禁添加**:反斜杠 `\`、逗号 `,`、LaTeX 命令(如 `\,`、`\n`、`^3` 等)、换行符 `\n` 等原本不存在的内容。
- **严禁改动**:不要把上标³改成 `^3`,不要把普通文本改成 LaTeX 格式,不要把 Unicode 上标改成 LaTeX 上标。
- **严禁添加换行**:不要添加 `\n` 或任何换行符,保持原文的换行格式不变。如果原文是连续文本,输出也必须是连续文本。
【严禁解题/严禁新增内容】(非常重要)
- 不要解题:不要推导、不要解释、不要补充答案、不要给出步骤、不要增加任何原文中不存在的内容。
- 不要把题干改写成解析/答案。
【必须保留】(非常重要)
1) 保留原题干的结构与排版:不调整顺序、不合并/拆分段落、不新增/删除句子。
2) 保留所有 HTML 标签与结构原样不动(例如
、
、 等)。
3) 题干中出现的所有 片段必须在输出中**逐字复制**(字符、空格、换行、属性顺序、大小写都必须完全一致),不得省略、重排、格式化。
4) **保留上标格式**:如果原文是 `dm³`、`cm³`、`m²` 等 Unicode 上标,必须保持原样,严禁改成 `dm^3`、`cm^3`、`m^2` 等 LaTeX 格式。
5) **保留单位格式**:如果原文是 `2.5 dm³` 这种格式(数字+空格+单位),保持原样,不要添加 `\,`、`\n` 等 LaTeX 命令。
【允许的唯一改动】(只允许做下面这些替换;除此之外任何字符都不要改)
6) 公式包裹:对行内公式 `$...$`,仅去掉外层 `$`,并保留内部内容的原顺序与原字符(不要删改里面的数字/变量/符号)。
7) 符号替换(只替换命中的这些 LaTeX 命令,其它所有内容保持原样):
- \triangle → △
- \text{cm} → cm
- \text{cm²} → cm²
- \neq → !=
- \ge → ≥
- \le → ≤
- \Rightarrow => =>
- \sqrt → sqrt
- \perp → ⟂
8) 填空线:把所有“连续下划线填空线”(长度不固定)统一成:________(8 个下划线)。
9) **问号填空**:把题干中的问号 `?`(用于表示填空)替换成:________(8 个下划线)。注意:不要删除问号,要替换成填空线。
【输出格式】
10) 输出必须是一段“可直接写回 stem 字段”的文本/HTML。
11) 只输出结果,不要加任何解释、标题、Markdown 标记(不要使用 **、列表编号等)。
下面是题干原文,请按规则输出清洗后的题干:
"""
_env_prompt = os.getenv("AI_STEM_PROMPT")
# 注意:如果环境变量存在但为空(例如 AI_STEM_PROMPT=),os.getenv 会返回空字符串,
# 这会把默认提示词覆盖掉,导致 AI 没有收到规则。这里显式做“空值回退默认”。
AI_STEM_PROMPT = (_env_prompt.strip() if isinstance(_env_prompt, str) else "").strip() or AI_STEM_PROMPT_DEFAULT
_RE_SVG_BLOCK = re.compile(r"