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}