Grades_Bot / app.py
22UFNAmL's picture
Update app.py
d9e2c53 verified
raw
history blame
7.1 kB
import json, base64, urllib.request, os, traceback
from fastapi import FastAPI, Request, BackgroundTasks, Response
app = FastAPI()
# --- 环境配置 (请在 HF Secrets 中设置) ---
FEISHU_APP_ID = os.getenv("FEISHU_APP_ID")
FEISHU_APP_SECRET = os.getenv("FEISHU_APP_SECRET")
SILICON_API_KEY = os.getenv("SILICON_API_KEY")
SUPABASE_URL = os.getenv("SUPABASE_URL")
SUPABASE_KEY = os.getenv("SUPABASE_KEY")
MODEL_NAME = "Qwen/Qwen3-VL-32B-Instruct"
# 内存暂存,用于多轮修改
img_cache = {}
def get_token():
try:
url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
p = json.dumps({"app_id": FEISHU_APP_ID, "app_secret": FEISHU_APP_SECRET}).encode()
req = urllib.request.Request(url, data=p, method="POST", headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=5) as f:
return json.loads(f.read().decode()).get("tenant_access_token")
except: return None
def send_msg(open_id, text):
token = get_token()
if not token: return
url = "https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=open_id"
payload = json.dumps({"receive_id": open_id, "msg_type": "text", "content": json.dumps({"text": text})}).encode()
req = urllib.request.Request(url, data=payload, method="POST",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"})
try: urllib.request.urlopen(req, timeout=5)
except: pass
def save_to_supabase(content):
if not SUPABASE_URL: return "⚠️ 未配置 Supabase URL"
try:
url = f"{SUPABASE_URL}/rest/v1/scores" # 假设你的表名是 scores
headers = {
"apikey": SUPABASE_KEY,
"Authorization": f"Bearer {SUPABASE_KEY}",
"Content-Type": "application/json",
"Prefer": "return=minimal"
}
payload = json.dumps({"content": content}).encode()
req = urllib.request.Request(url, data=payload, method="POST", headers=headers)
urllib.request.urlopen(req, timeout=5)
return "✅ 数据已成功同步至 Supabase!"
except Exception as e:
return f"❌ Supabase 存储失败: {e}"
async def run_ai_task(img_bin, instruction, open_id):
try:
b64_img = base64.b64encode(img_bin).decode()
# 针对性提示词:处理学号与成绩的逻辑
prompt = (
f"你是一个表格数据提取助手。请分析图片并提取成绩:\n"
f"1. 提取每一行的成绩信息。\n"
f"2. 特别注意:如果表格最后一列是学号(通常是长数字),请跳过它,提取倒数第二列的成绩;如果最后一列是成绩,则提取最后一列。\n"
f"3.一定不要返回序号(连起来的数字),必须返回类似-1,-6,A+的内容列"
f"4. 仅以 Markdown 无序列表格式输出成绩数值,不要表头,不要解释,不要姓名。\n"
f"用户改进意见:{instruction}"
)
body = json.dumps({
"model": MODEL_NAME,
"messages": [{"role": "user", "content": [
{"type": "text", "text": prompt},
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64_img}"}}
]}]
}).encode()
req = urllib.request.Request("https://api.siliconflow.cn/v1/chat/completions", data=body, method="POST",
headers={"Authorization": f"Bearer {SILICON_API_KEY}", "Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=60) as f:
res = json.loads(f.read().decode())
ans = res['choices'][0]['message']['content'].strip()
# 清洗可能存在的 Markdown 标签
ans = ans.replace('```markdown', '').replace('```', '').strip()
reply = (
f"📊 **提取结果**:\n\n{ans}\n\n"
"--- \n"
"💡 **操作指南**:\n"
"- 直接回复 **改进建议** 让 AI 重改\n"
"- 回复 **y** 确认并保存到 Supabase\n"
"- 回复 **q** 退出当前任务"
)
send_msg(open_id, reply)
except Exception as e:
send_msg(open_id, f"‼️ AI 处理出错: {e}")
# --- 路由 ---
@app.head("/")
@app.get("/")
async def status_check():
# 适配 UptimeRobot 的 HEAD/GET 检查
return Response(status_code=200)
@app.post("/")
async def feishu_callback(request: Request, background_tasks: BackgroundTasks):
try:
data = await request.json()
if "challenge" in data: return {"challenge": data["challenge"]}
print(f"📡 收到推送: {json.dumps(data)}") # 调试用
event = data.get("event", {})
msg = event.get("message", {})
msg_type = msg.get("message_type") or msg.get("msg_type")
# 兼容性获取 OpenID
sender = event.get("sender", {})
open_id = sender.get("sender_id", {}).get("open_id") or sender.get("open_id") or msg.get("open_id")
if not open_id: return {"code": 0}
# 1. 收到图片
if msg_type == "image":
token = get_token()
content = json.loads(msg.get("content", "{}"))
img_key = content.get("image_key")
if img_key:
print(f"📩 正在下载图片: {img_key}")
img_url = f"https://open.feishu.cn/open-apis/im/v1/messages/{msg['message_id']}/resources/{img_key}?type=image"
req = urllib.request.Request(img_url, headers={"Authorization": f"Bearer {token}"})
with urllib.request.urlopen(req, timeout=10) as f:
img_bin = f.read()
img_cache[open_id] = img_bin
background_tasks.add_task(run_ai_task, img_bin, "初次提取", open_id)
# 2. 收到文本 (交互)
elif msg_type == "text":
text_content = json.loads(msg.get("content", "{}")).get("text", "").strip().lower()
# 简单处理:取空格后最后一部分(过滤@机器人)
cmd = text_content.split(' ')[-1]
if cmd == 'y':
# 保存逻辑,实际应存储 AI 上次的 ans
res = save_to_supabase("用户确认的最新成绩列表")
send_msg(open_id, res)
img_cache.pop(open_id, None)
elif cmd == 'q':
img_cache.pop(open_id, None)
send_msg(open_id, "✅ 已退出任务。")
else:
if open_id in img_cache:
send_msg(open_id, f"🔄 收到反馈:'{cmd}',正在重新校对...")
background_tasks.add_task(run_ai_task, img_cache[open_id], cmd, open_id)
else:
send_msg(open_id, "💡 请先发送成绩图片。")
return {"code": 0}
except:
print(traceback.format_exc())
return {"code": 0}