Spaces:
Running
Running
File size: 7,097 Bytes
7d87714 cc47159 579ec86 7d87714 579ec86 50a57ad 7d87714 395223b 539a202 7d87714 539a202 7d87714 6fe5839 7d87714 6fe5839 7d87714 faf3e25 7d87714 d9e2c53 7d87714 6fe5839 7d87714 6fe5839 7d87714 6fe5839 7d87714 6fe5839 7d87714 579ec86 7d87714 539a202 7d87714 539a202 7d87714 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | 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} |