commit 4e62afc40e8214a99334fbc8b8a2f51a24638bb9 Author: yuetsh <517252939@qq.com> Date: Tue Oct 21 21:26:56 2025 +0800 first commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5f98ee2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,75 @@ +# Python 缓存和编译文件 +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# 虚拟环境 +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# 数据库文件(保留主数据库文件,排除临时文件) +database.db-shm +database.db-wal + +# IDE 和编辑器文件 +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# 操作系统文件 +.DS_Store +Thumbs.db +.directory + +# 日志文件 +*.log +logs/ + +# uv 缓存 +.uv/ + +# Git 相关 +.git/ +.gitignore + +# 测试文件 +test_*.py +*_test.py +tests/ + +# 文档文件 +README.md +*.md +docs/ + +# Docker 相关 +Dockerfile +.dockerignore + +# 临时文件 +*.tmp +*.temp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..02deb88 --- /dev/null +++ b/.gitignore @@ -0,0 +1,54 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Database +database.db +*.db + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +logs/ + +# uv +.uv/ + +*.db +*.db-* \ No newline at end of file diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e47687f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,41 @@ +# 使用 Python 3.12 官方镜像作为基础镜像 +FROM python:3.12-slim + +# 设置工作目录 +WORKDIR /app + +# 设置环境变量 +ENV PYTHONUNBUFFERED=1 +ENV PYTHONDONTWRITEBYTECODE=1 + +# 配置中科大镜像源 +RUN echo "deb https://mirrors.ustc.edu.cn/debian/ bookworm main" > /etc/apt/sources.list && \ + echo "deb https://mirrors.ustc.edu.cn/debian/ bookworm-updates main" >> /etc/apt/sources.list && \ + echo "deb https://mirrors.ustc.edu.cn/debian-security bookworm-security main" >> /etc/apt/sources.list + +# 安装系统依赖 +RUN apt-get update && apt-get install -y \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# 复制项目文件 +COPY pyproject.toml ./ +COPY uv.lock ./ + +# 安装 uv 包管理器 +RUN pip install uv + +# 配置中科大 PyPI 镜像源 +RUN uv config set global.index-url https://pypi.mirrors.ustc.edu.cn/simple/ + +# 使用 uv 安装 Python 依赖 +RUN uv sync --frozen + +# 复制应用代码 +COPY . . + +# 暴露端口 +EXPOSE 8080 + +# 设置启动命令 +CMD ["uv", "run", "python", "main.py"] diff --git a/database.py b/database.py new file mode 100644 index 0000000..e794895 --- /dev/null +++ b/database.py @@ -0,0 +1,47 @@ +from tortoise.contrib.fastapi import register_tortoise +from models import PresetCode +from schemas import PresetCodeCreate, PresetCodeResponse +from typing import List, Optional + +class DatabaseService: + """数据库操作服务类""" + + @staticmethod + def init_database(app, database_url: str): + """初始化数据库连接""" + register_tortoise( + app, + db_url=database_url, + modules={"models": ["models"]}, + generate_schemas=True, + ) + + @staticmethod + async def get_all_codes() -> List[PresetCodeResponse]: + """获取所有预设代码""" + codes = await PresetCode.all().order_by('-id') + return [PresetCodeResponse.from_orm(code).dict() for code in codes] + + @staticmethod + async def get_code_by_query(query: str) -> Optional[PresetCodeResponse]: + """根据查询字符串获取特定代码""" + code = await PresetCode.get_or_none(query=query) + if not code: + return None + return PresetCodeResponse.from_orm(code).dict() + + @staticmethod + async def create_code(code_data: PresetCodeCreate) -> PresetCodeResponse: + """创建新的预设代码""" + code = await PresetCode.create(**code_data.dict()) + return PresetCodeResponse.from_orm(code).dict() + + @staticmethod + async def delete_code(code_id: int) -> bool: + """删除指定 ID 的代码""" + code = await PresetCode.get_or_none(id=code_id) + if not code: + return False + + await code.delete() + return True diff --git a/main.py b/main.py new file mode 100644 index 0000000..124a864 --- /dev/null +++ b/main.py @@ -0,0 +1,145 @@ +from io import StringIO +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import StreamingResponse +import os +import json +from openai import OpenAI +from schemas import PresetCodeCreate, AIAnalysisRequest, DebugRequest +from database import DatabaseService +from pg_logger import exec_script_str_local +from dotenv import load_dotenv + + +# 加载环境变量 +load_dotenv() + +app = FastAPI(title="Code API", version="1.0.0") + +# CORS 配置 +app.add_middleware( + CORSMiddleware, + allow_origins=[ + "https://code.xuyue.cc", + "http://10.13.114.114", + "http://localhost:3000", + ], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 数据库配置 +DATABASE_URL = "sqlite://database.db" + +# 初始化数据库 +DatabaseService.init_database(app, DATABASE_URL) + + +@app.get("/") +async def get_all_codes() -> dict: + """获取所有预设代码""" + codes = await DatabaseService.get_all_codes() + return {"data": codes} + + +@app.get("/query/{query}") +async def get_code_by_query(query: str) -> dict: + """根据查询字符串获取特定代码""" + code = await DatabaseService.get_code_by_query(query) + if not code: + raise HTTPException(status_code=404, detail="Record not found!") + return {"data": code} + + +@app.post("/") +async def create_code(code_data: PresetCodeCreate) -> dict: + """创建新的预设代码""" + try: + code = await DatabaseService.create_code(code_data) + return {"data": code} + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + + +@app.delete("/{code_id}") +async def delete_code(code_id: int) -> dict: + """删除指定 ID 的代码""" + success = await DatabaseService.delete_code(code_id) + if not success: + raise HTTPException(status_code=400, detail="Record not found!") + + return {"data": True} + + +@app.post("/ai") +async def ai_analysis(request: AIAnalysisRequest): + """AI 代码分析端点""" + code = request.code + error_info = request.error_info + language = request.language + + api_key = os.getenv("API_KEY") + if not api_key: + raise HTTPException(status_code=400, detail="API_KEY is not set") + + system_prompt = "你是编程老师,擅长分析代码和错误信息,一般出错在语法和格式,请指出错误在第几行,并给出中文的、简要的解决方法。用 markdown 格式返回。" + user_prompt = f"编程语言:{language}\n代码:\n```{language}\n{code}\n```\n错误信息:\n```\n{error_info}\n```" + + def generate_response(): + try: + # 初始化 OpenAI 客户端 + client = OpenAI(api_key=api_key, base_url="https://api.deepseek.com") + + # 创建流式响应 + stream = client.chat.completions.create( + model="deepseek-chat", + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt}, + ], + stream=True, + seed=0, + ) + + for chunk in stream: + if chunk.choices and len(chunk.choices) > 0: + delta = chunk.choices[0].delta + if hasattr(delta, "content") and delta.content: + yield f"data: {json.dumps({'event': 'chunk', 'data': delta.content})}\n\n" + + # 发送完成信号 + yield f"data: {json.dumps({'event': 'done', 'data': ''})}\n\n" + + except Exception as e: + yield f"data: {json.dumps({'event': 'error', 'data': str(e)})}\n\n" + + return StreamingResponse( + generate_response(), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + }, + ) + + +@app.post("/debug") +async def debug(request: DebugRequest): + """调试端点""" + code = request.code + inputs = request.inputs + + data = {} + + def dump(input_code, output_trace): + data.update(dict(code=input_code, trace=output_trace)) + + exec_script_str_local(code, inputs, False, False, dump) + return {"data": data} + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run(app, port=8080) diff --git a/models.py b/models.py new file mode 100644 index 0000000..7b1fafd --- /dev/null +++ b/models.py @@ -0,0 +1,11 @@ +from tortoise.models import Model +from tortoise import fields + +class PresetCode(Model): + """预设代码数据库模型""" + id = fields.IntField(pk=True) + query = fields.CharField(max_length=255, unique=True) + code = fields.TextField() + + class Meta: + table = "preset_codes" diff --git a/pg_encoder.py b/pg_encoder.py new file mode 100644 index 0000000..b5af706 --- /dev/null +++ b/pg_encoder.py @@ -0,0 +1,589 @@ +# Online Python Tutor +# https://github.com/pgbovine/OnlinePythonTutor/ +# +# Copyright (C) Philip J. Guo (philip@pgbovine.net) +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# Thanks to John DeNero for making the encoder work on both Python 2 and 3 +# (circa 2012-2013) + + +# Given an arbitrary piece of Python data, encode it in such a manner +# that it can be later encoded into JSON. +# http://json.org/ +# +# We use this function to encode run-time traces of data structures +# to send to the front-end. +# +# Format: +# Primitives: +# * None, int, long, float, str, bool - unchanged +# (json.dumps encodes these fine verbatim, except for inf, -inf, and nan) +# +# exceptions: float('inf') -> ['SPECIAL_FLOAT', 'Infinity'] +# float('-inf') -> ['SPECIAL_FLOAT', '-Infinity'] +# float('nan') -> ['SPECIAL_FLOAT', 'NaN'] +# x == int(x) -> ['SPECIAL_FLOAT', '%.1f' % x] +# (this way, 3.0 prints as '3.0' and not as 3, which looks like an int) +# +# If render_heap_primitives is True, then primitive values are rendered +# on the heap as ['HEAP_PRIMITIVE', , ] +# +# (for SPECIAL_FLOAT values, is a list like ['SPECIAL_FLOAT', 'Infinity']) +# +# added on 2018-06-13: +# ['IMPORTED_FAUX_PRIMITIVE',