You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

192 lines
7.2 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

from fastapi import FastAPI, Query, Body
from fastapi.responses import StreamingResponse, PlainTextResponse
from fastapi.middleware.cors import CORSMiddleware
import socket
from openai import OpenAI
import asyncio
import json
app = FastAPI()
# 添加 CORS 中间件
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 阿里云中用来调用 deepseek v3 的密钥
MODEL_API_KEY = "sk-01d13a39e09844038322108ecdbd1bbc"
MODEL_NAME = "deepseek-v3"
# 初始化 OpenAI 客户端
client = OpenAI(
api_key=MODEL_API_KEY,
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)
# 获取本机所有 IPv4 地址
def get_local_ips():
ips = []
hostname = socket.gethostname()
try:
# 获取所有 IP 地址
addrs = socket.getaddrinfo(hostname, None, family=socket.AF_INET) # 只获取 IPv4 地址
for addr in addrs:
ip = addr[4][0]
if ip not in ips:
ips.append(ip)
except Exception as e:
print(f"获取 IP 地址失败: {e}")
return ips
# 解析 Markdown 内容并生成 JSON 数据
def parse_markdown_to_json(content: str):
sections = []
current_section = None
current_subsection = None
for line in content.split("\n"):
# 忽略无用的标记
if line.strip() in ["```markdown", "```"]:
continue
if line.startswith("# "): # 一级标题(忽略,作为整体标题)
continue
elif line.startswith("## "): # 二级标题
if current_section:
sections.append(current_section)
current_section = {"title": line[3:].strip(), "items": []}
current_subsection = None
elif line.startswith("### "): # 三级标题
if current_section:
current_subsection = {"title": line[4:].strip(), "text": ""}
current_section["items"].append(current_subsection)
elif line.strip() and (line.startswith("- ") or (line[0].isdigit() and line[1] == '.')): # 列表项或数字加点
if not current_subsection: # 如果 current_subsection 为 None则创建一个默认子章节
current_subsection = {"title": "", "text": ""}
if current_section:
current_section["items"].append(current_subsection)
current_subsection["text"] += line.strip() + " "
elif line.strip(): # 普通文本
if current_subsection:
current_subsection["text"] += line.strip() + " "
if current_section:
sections.append(current_section)
return sections
# 生成流式 JSON 数据(中文输出,逐行返回)
# 根路由,返回提示信息
@app.get("/")
def root():
return PlainTextResponse("Hello ApiStream")
# 流式返回数据(支持 GET 和 POST 方法)
@app.api_route("/api/tools/aippt_outline", methods=["GET", "POST"])
async def aippt_outline(
course_name: str = Query(None, description="课程名称GET 方法使用)"), # 从查询参数中获取 course_name
course_name_body: str = Body(None, embed=True, description="课程名称POST 方法使用)") # 从请求体中获取 course_name
):
# 检查 course_name 是否为空
if not course_name and not course_name_body:
return PlainTextResponse("请提供课程名称,例course_name=三角形面积")
# 优先使用 POST 请求体中的 course_name
course_name = course_name_body if course_name_body else course_name
# 返回流式响应
return StreamingResponse(
generate_stream(course_name),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Access-Control-Allow-Origin": "*",
"X-Accel-Buffering": "no"
}
)
# 流式生成数据的函数
async def generate_stream(course_name: str):
# 调用阿里云 API启用流式响应
stream = client.chat.completions.create(
model=MODEL_NAME,
messages=[
{'role': 'system', 'content': '你是一个教学经验丰富的基础教育教师'},
{'role': 'user', 'content': '帮我设计一下' + course_name + '的课件提纲用markdown格式返回。'}
],
stream=True, # 启用流式响应
timeout=6000,
)
# 逐字返回数据
for chunk in stream:
if chunk.choices[0].delta.content:
for char in chunk.choices[0].delta.content:
yield char.encode("utf-8")
await asyncio.sleep(0.05) # 控制逐字输出的速度
# 生成流式 JSON 数据(中文输出,逐行返回)
async def generate_json_stream(content: str): # 新增 content 参数
# 解析 Markdown 内容
sections = parse_markdown_to_json(content)
# 封面(使用第一个章节的标题作为封面标题)
first_section_title = sections[0]["title"] if sections else "默认标题"
yield json.dumps({"type": "cover", "data": {"title": first_section_title, "text": "课程简介"}}, ensure_ascii=False) + "\n"
await asyncio.sleep(0.5)
# 目录(使用所有章节的标题)
yield json.dumps({"type": "contents", "data": {"items": [section["title"] for section in sections]}}, ensure_ascii=False) + "\n"
await asyncio.sleep(0.5)
# 逐章节生成内容
for section in sections:
# 过渡(使用章节标题动态生成过渡文本)
yield json.dumps({"type": "transition", "data": {"title": section["title"], "text": section["title"]}}, ensure_ascii=False) + "\n"
await asyncio.sleep(0.5)
# 具体内容(直接使用解析后的章节内容)
yield json.dumps({"type": "content", "data": {"title": section["title"], "items": section["items"]}}, ensure_ascii=False) + "\n"
await asyncio.sleep(0.5)
# 结束
yield json.dumps({"type": "end"}, ensure_ascii=False) + "\n"
# 从文件中读取 Markdown 并生成流式 JSON中文输出逐行返回
@app.get("/api/tools/aippt")
async def aippt(content: str = Query(..., description="Markdown 内容")): # 新增 content 参数
return StreamingResponse(
generate_json_stream(content), # 传入 content
media_type="text/plain", # 使用 text/plain 格式
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Access-Control-Allow-Origin": "*",
"X-Accel-Buffering": "no",
"Content-Type": "text/plain; charset=utf-8" # 明确设置 Content-Type
}
)
# 运行应用
if __name__ == "__main__":
import uvicorn
# 获取本机所有 IPv4 地址
ips = get_local_ips()
if not ips:
print("无法获取本机 IP 地址,使用默认地址 127.0.0.1")
ips = ["127.0.0.1"]
# 打印所有 IP 地址
print("服务将在以下 IP 地址上运行:")
for ip in ips:
print(f"http://{ip}:5173")
# 启动 FastAPI 应用,绑定到所有 IP 地址
uvicorn.run(app, host="0.0.0.0", port=5173)