duqing026/microgrid-dispatch-agent
0
1import os2import json3import uuid4import math5import random6import sqlite37import datetime8import hashlib9import requests10from flask import Flask, render_template, request, jsonify, g, send_file11from dotenv import load_dotenv12 13load_dotenv()14 15BASE_DIR = os.path.dirname(os.path.abspath(__file__))16INSTANCE_DIR = os.path.join(BASE_DIR, "instance")17os.makedirs(INSTANCE_DIR, exist_ok=True)18 19app = Flask(__name__)20app.config["DATABASE"] = os.path.join(INSTANCE_DIR, "microgrid.db")21app.config["SILICONFLOW_API_KEY"] = os.getenv("SILICONFLOW_API_KEY", "").strip()22app.config["SILICONFLOW_BASE_URL"] = os.getenv("SILICONFLOW_BASE_URL", "https://api.siliconflow.cn/v1").strip()23app.config["SILICONFLOW_MODEL"] = os.getenv("SILICONFLOW_MODEL", "deepseek-ai/DeepSeek-V3").strip()24 25 26def get_db():27 db = getattr(g, "_database", None)28 if db is None:29 db = g._database = sqlite3.connect(app.config["DATABASE"])30 db.row_factory = sqlite3.Row31 return db32 33 34@app.teardown_appcontext35def close_connection(exception):36 db = getattr(g, "_database", None)37 if db is not None:38 db.close()39 40 41def now_ts():42 return datetime.datetime.now().isoformat()43 44 45def init_db():46 db = sqlite3.connect(app.config["DATABASE"])47 cursor = db.cursor()48 cursor.execute(49 """50 CREATE TABLE IF NOT EXISTS cycles (51 id TEXT PRIMARY KEY,52 title TEXT,53 scenario TEXT,54 result TEXT,55 status TEXT,56 created_at TEXT57 )58 """59 )60 cursor.execute(61 """62 CREATE TABLE IF NOT EXISTS logs (63 id INTEGER PRIMARY KEY AUTOINCREMENT,64 cycle_id TEXT,65 role TEXT,66 content TEXT,67 created_at TEXT68 )69 """70 )71 cursor.execute(72 """73 CREATE TABLE IF NOT EXISTS memories (74 key TEXT PRIMARY KEY,75 value TEXT,76 updated_at TEXT77 )78 """79 )80 cursor.execute(81 """82 CREATE TABLE IF NOT EXISTS validations (83 id INTEGER PRIMARY KEY AUTOINCREMENT,84 cycle_id TEXT,85 passed INTEGER,86 issues TEXT,87 created_at TEXT88 )89 """90 )91 cursor.execute(92 """93 CREATE TABLE IF NOT EXISTS files (94 id TEXT PRIMARY KEY,95 filename TEXT,96 size_bytes INTEGER,97 sha256 TEXT,98 content_type TEXT,99 is_binary INTEGER,100 storage_path TEXT,101 created_at TEXT102 )103 """104 )105 db.commit()106 cursor.execute("SELECT COUNT(1) FROM cycles")107 count = cursor.fetchone()[0]108 if count == 0:109 sample_id = str(uuid.uuid4())110 scenario = {111 "title": "示例:园区A峰值调度",112 "demand_mw": 120,113 "solar_mw": 42,114 "wind_mw": 28,115 "storage_mwh": 80,116 "storage_soc": 0.45,117 "priority": "成本优先",118 "notes": "示例备注:保障冷链负荷,允许需求响应10%。",119 }120 db.execute(121 "INSERT INTO cycles (id, title, scenario, result, status, created_at) VALUES (?, ?, ?, ?, ?, ?)",122 (123 sample_id,124 scenario["title"],125 json.dumps(scenario, ensure_ascii=False),126 json.dumps(127 {128 "scenario": scenario,129 "decision": {130 "dispatch": {131 "solar_mw": 42,132 "wind_mw": 28,133 "storage_discharge_mw": 18,134 "storage_charge_mw": 0,135 "grid_import_mw": 32,136 "grid_export_mw": 0,137 "demand_response_mw": 0,138 "storage_soc_next": 0.22,139 },140 "kpi": {"cost": 27.5, "carbon": 16.8, "reliability": 0.99},141 "notes": "示例调度已完成",142 },143 "validation": {"passed": True, "issues": ["校验通过"]},144 "memory": {145 "last_priority": "成本优先",146 "last_cost": 27.5,147 "last_carbon": 16.8,148 "last_reliability": 0.99,149 "iterations": 0,150 },151 "iterations": 0,152 },153 ensure_ascii=False,154 ),155 "completed",156 now_ts(),157 ),158 )159 db.commit()160 db.close()161 162 163init_db()164 165 166def log_event(cycle_id, role, content):167 db = get_db()168 db.execute(169 "INSERT INTO logs (cycle_id, role, content, created_at) VALUES (?, ?, ?, ?)",170 (cycle_id, role, content, now_ts()),171 )172 db.commit()173 174 175def set_memory(key, value):176 db = get_db()177 row = db.execute("SELECT key FROM memories WHERE key = ?", (key,)).fetchone()178 payload = json.dumps(value, ensure_ascii=False)179 if row:180 db.execute("UPDATE memories SET value = ?, updated_at = ? WHERE key = ?", (payload, now_ts(), key))181 else:182 db.execute("INSERT INTO memories (key, value, updated_at) VALUES (?, ?, ?)", (key, payload, now_ts()))183 db.commit()184 185 186def get_memory(key, default=None):187 db = get_db()188 row = db.execute("SELECT value FROM memories WHERE key = ?", (key,)).fetchone()189 if not row:190 return default191 try:192 return json.loads(row["value"])193 except json.JSONDecodeError:194 return default195 196 197def call_llm_json(system_prompt, user_prompt):198 api_key = app.config["SILICONFLOW_API_KEY"]199 if not api_key:200 return None201 url = f"{app.config['SILICONFLOW_BASE_URL']}/chat/completions"202 headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}203 payload = {204 "model": app.config["SILICONFLOW_MODEL"],205 "messages": [206 {"role": "system", "content": system_prompt},207 {"role": "user", "content": user_prompt},208 ],209 "temperature": 0.2,210 "max_tokens": 900,211 "response_format": {"type": "json_object"},212 }213 try:214 response = requests.post(url, headers=headers, json=payload, timeout=25)215 response.raise_for_status()216 data = response.json()217 content = data.get("choices", [{}])[0].get("message", {}).get("content", "")218 return json.loads(content)219 except Exception:220 return None221 222 223def tool_forecast(scenario):224 demand = scenario["demand_mw"]225 solar = scenario["solar_mw"]226 wind = scenario["wind_mw"]227 forecast = []228 for hour in range(6):229 factor = 0.92 + random.random() * 0.16230 forecast.append(231 {232 "hour": hour + 1,233 "demand_mw": round(demand * factor, 2),234 "solar_mw": round(solar * (0.7 + random.random() * 0.4), 2),235 "wind_mw": round(wind * (0.6 + random.random() * 0.5), 2),236 }237 )238 return forecast239 240 241def tool_price():242 schedule = []243 base = 0.52 + random.random() * 0.12244 for hour in range(6):245 peak = 1.25 if hour in (2, 3, 4) else 1.0246 schedule.append(round(base * peak + random.random() * 0.08, 3))247 return {248 "energy_price": round(base, 3),249 "carbon_price": round(0.08 + random.random() * 0.05, 3),250 "schedule": schedule,251 }252 253 254def tool_grid_constraints():255 return {"max_import_mw": 80.0, "max_export_mw": 40.0}256 257 258def tool_demand_response():259 return {"max_response_mw": 12.0, "response_cost": 0.15}260 261 262def planner_agent(scenario, tools):263 system_prompt = "你是微电网调度规划智能体,输出JSON,包含目标、优先级、步骤。"264 user_prompt = f"场景: {json.dumps(scenario, ensure_ascii=False)}\n工具: {json.dumps(tools, ensure_ascii=False)}"265 result = call_llm_json(system_prompt, user_prompt)266 if result:267 return result268 priority = scenario.get("priority", "成本优先")269 return {270 "目标": "在满足负荷的前提下优化成本与碳排",271 "优先级": priority,272 "步骤": [273 "读取需求与可再生出力",274 "评估储能SOC与可调度能力",275 "结合电价与碳价决定充放电与并网",276 "生成调度方案并进入校验",277 ],278 }279 280 281def risk_agent(scenario, tools, plan):282 system_prompt = "你是电力系统风险评估智能体,输出JSON,包含风险列表与缓释建议。"283 user_prompt = f"计划: {json.dumps(plan, ensure_ascii=False)}\n场景: {json.dumps(scenario, ensure_ascii=False)}"284 result = call_llm_json(system_prompt, user_prompt)285 if result:286 return result287 risks = []288 if scenario["storage_soc"] < 0.2:289 risks.append("储能SOC偏低,抗波动能力下降")290 if scenario["demand_mw"] > 120:291 risks.append("负荷偏高,可能逼近并网上限")292 return {293 "风险": risks or ["暂无明显风险"],294 "缓释": "必要时启用需求响应与负荷切分策略",295 }296 297 298def optimizer_agent(scenario, tools, plan, risk):299 demand = scenario["demand_mw"]300 solar = scenario["solar_mw"]301 wind = scenario["wind_mw"]302 storage_mwh = scenario["storage_mwh"]303 storage_soc = scenario["storage_soc"]304 grid_limit = tools["grid"]["max_import_mw"]305 export_limit = tools["grid"]["max_export_mw"]306 response_limit = tools["response"]["max_response_mw"]307 308 max_rate = storage_mwh * 0.5309 available_discharge = storage_mwh * storage_soc310 forecast = tools["forecast"]311 schedule_price = tools["price"]["schedule"]312 carbon_price = tools["price"]["carbon_price"]313 grid_carbon = 0.72314 priority = scenario.get("priority", "成本优先")315 weight_cost = 0.5316 weight_carbon = 0.3317 weight_reliability = 0.2318 if priority == "碳优先":319 weight_cost, weight_carbon, weight_reliability = 0.2, 0.6, 0.2320 elif priority == "可靠性优先":321 weight_cost, weight_carbon, weight_reliability = 0.2, 0.2, 0.6322 323 hourly = []324 total_cost = 0.0325 total_carbon = 0.0326 total_demand = 0.0327 total_unserved = 0.0328 soc = storage_soc329 330 for idx, point in enumerate(forecast):331 hour_price = schedule_price[idx]332 demand_h = point["demand_mw"]333 solar_h = point["solar_mw"]334 wind_h = point["wind_mw"]335 total_demand += demand_h336 337 max_rate_h = storage_mwh * 0.5338 available_discharge_h = storage_mwh * soc339 net = demand_h - solar_h - wind_h340 341 if priority == "碳优先":342 discharge = min(max_rate_h, available_discharge_h, max(0.0, net * 0.8))343 elif priority == "成本优先" and hour_price < 0.6:344 discharge = min(max_rate_h * 0.4, available_discharge_h, max(0.0, net))345 else:346 discharge = min(max_rate_h, available_discharge_h, max(0.0, net))347 348 remaining = net - discharge349 grid_import = min(max(0.0, remaining), grid_limit)350 demand_response = min(response_limit, max(0.0, remaining - grid_import))351 unserved = max(0.0, remaining - grid_import - demand_response)352 353 surplus = max(0.0, solar_h + wind_h + discharge - demand_h)354 charge = min(max_rate_h, storage_mwh * (1 - soc), surplus)355 export = max(0.0, min(export_limit, surplus - charge))356 357 soc = soc + (charge - discharge) / storage_mwh if storage_mwh > 0 else soc358 soc = max(0.0, min(1.0, soc))359 360 cost = grid_import * hour_price + demand_response * tools["response"]["response_cost"]361 carbon = grid_import * grid_carbon - (solar_h + wind_h) * 0.18362 total_cost += cost363 total_carbon += carbon364 total_unserved += unserved365 366 hourly.append(367 {368 "hour": point["hour"],369 "demand_mw": round(demand_h, 2),370 "solar_mw": round(solar_h, 2),371 "wind_mw": round(wind_h, 2),372 "storage_discharge_mw": round(discharge, 2),373 "storage_charge_mw": round(charge, 2),374 "grid_import_mw": round(grid_import, 2),375 "grid_export_mw": round(export, 2),376 "demand_response_mw": round(demand_response, 2),377 "unserved_mw": round(unserved, 2),378 "soc": round(soc, 3),379 "price": hour_price,380 }381 )382 383 reliability = round(1 - (total_unserved / total_demand if total_demand > 0 else 0), 3)384 cost = round(total_cost, 3)385 carbon = round(total_carbon, 3)386 score = round((100 - cost) * weight_cost + (100 - abs(carbon)) * weight_carbon + reliability * 100 * weight_reliability, 2)387 388 return {389 "dispatch": {390 "solar_mw": round(solar, 2),391 "wind_mw": round(wind, 2),392 "storage_soc_next": round(soc, 3),393 "hourly": hourly,394 },395 "kpi": {"cost": cost, "carbon": carbon, "reliability": reliability, "score": score},396 "notes": "已综合考虑电价、碳价、需求响应与并网约束",397 "plan": plan,398 "risk": risk,399 }400 401 402def validate_dispatch(scenario, decision, tools):403 issues = []404 hourly = decision["dispatch"].get("hourly", [])405 grid_limit = tools["grid"]["max_import_mw"]406 for point in hourly:407 supplied = (408 point["solar_mw"]409 + point["wind_mw"]410 + point["storage_discharge_mw"]411 + point["grid_import_mw"]412 - point["storage_charge_mw"]413 - point["grid_export_mw"]414 )415 if supplied + 0.01 < point["demand_mw"]:416 issues.append(f"小时{point['hour']}供给不足")417 if point["grid_import_mw"] > grid_limit + 0.01:418 issues.append(f"小时{point['hour']}并网功率超限")419 if not (0 <= point["soc"] <= 1):420 issues.append(f"小时{point['hour']}储能SOC越界")421 passed = len(issues) == 0422 return {"passed": passed, "issues": issues or ["校验通过"]}423 424 425def iterate_adjustment(scenario, decision, tools):426 dispatch = decision["dispatch"].copy()427 hourly = []428 grid_limit = tools["grid"]["max_import_mw"]429 for point in dispatch.get("hourly", []):430 if point["unserved_mw"] > 0:431 extra = min(point["unserved_mw"], grid_limit - point["grid_import_mw"])432 point["grid_import_mw"] = round(point["grid_import_mw"] + extra, 2)433 point["unserved_mw"] = round(max(0.0, point["unserved_mw"] - extra), 2)434 hourly.append(point)435 dispatch["hourly"] = hourly436 decision["dispatch"] = dispatch437 return decision438 439 440def run_cycle(payload):441 cycle_id = str(uuid.uuid4())442 title = payload.get("title", "未命名调度")443 scenario = {444 "title": title,445 "demand_mw": float(payload.get("demand_mw", 120)),446 "solar_mw": float(payload.get("solar_mw", 40)),447 "wind_mw": float(payload.get("wind_mw", 30)),448 "storage_mwh": float(payload.get("storage_mwh", 80)),449 "storage_soc": float(payload.get("storage_soc", 0.45)),450 "priority": payload.get("priority", "成本优先"),451 "notes": payload.get("notes", ""),452 }453 454 tools = {455 "forecast": tool_forecast(scenario),456 "price": tool_price(),457 "grid": tool_grid_constraints(),458 "response": tool_demand_response(),459 }460 461 db = get_db()462 db.execute(463 "INSERT INTO cycles (id, title, scenario, result, status, created_at) VALUES (?, ?, ?, ?, ?, ?)",464 (cycle_id, title, json.dumps(scenario, ensure_ascii=False), "{}", "running", now_ts()),465 )466 db.commit()467 468 log_event(cycle_id, "system", "进入闭环:推理/决策/工具行动/状态记忆/校验/迭代")469 log_event(cycle_id, "tool_action", f"工具预测: {json.dumps(tools, ensure_ascii=False)}")470 471 plan = planner_agent(scenario, tools)472 log_event(cycle_id, "reasoning", json.dumps(plan, ensure_ascii=False))473 474 risk = risk_agent(scenario, tools, plan)475 log_event(cycle_id, "reasoning", json.dumps(risk, ensure_ascii=False))476 477 decision = optimizer_agent(scenario, tools, plan, risk)478 log_event(cycle_id, "decision", json.dumps(decision, ensure_ascii=False))479 480 validation = validate_dispatch(scenario, decision, tools)481 log_event(cycle_id, "validation", json.dumps(validation, ensure_ascii=False))482 483 iteration_count = 0484 while not validation["passed"] and iteration_count < 2:485 iteration_count += 1486 decision = iterate_adjustment(scenario, decision, tools)487 log_event(cycle_id, "iteration", f"第{iteration_count}次迭代调整")488 validation = validate_dispatch(scenario, decision, tools)489 log_event(cycle_id, "validation", json.dumps(validation, ensure_ascii=False))490 491 memory_snapshot = {492 "last_priority": scenario["priority"],493 "last_cost": decision["kpi"]["cost"],494 "last_carbon": decision["kpi"]["carbon"],495 "last_reliability": decision["kpi"]["reliability"],496 "iterations": iteration_count,497 "last_score": decision["kpi"]["score"],498 }499 set_memory("microgrid_state", memory_snapshot)500 log_event(cycle_id, "memory", json.dumps(memory_snapshot, ensure_ascii=False))501 502 db.execute(503 "INSERT INTO validations (cycle_id, passed, issues, created_at) VALUES (?, ?, ?, ?)",504 (cycle_id, 1 if validation["passed"] else 0, json.dumps(validation["issues"], ensure_ascii=False), now_ts()),505 )506 db.execute(507 "UPDATE cycles SET result = ?, status = ? WHERE id = ?",508 (509 json.dumps(510 {511 "scenario": scenario,512 "decision": decision,513 "validation": validation,514 "memory": memory_snapshot,515 "iterations": iteration_count,516 },517 ensure_ascii=False,518 ),519 "completed" if validation["passed"] else "needs_review",520 cycle_id,521 ),522 )523 db.commit()524 525 return {526 "id": cycle_id,527 "status": "completed" if validation["passed"] else "needs_review",528 "scenario": scenario,529 "decision": decision,530 "validation": validation,531 "memory": memory_snapshot,532 "iterations": iteration_count,533 }534 535 536@app.route("/")537def index():538 return render_template("index.html")539 540 541@app.route("/api/run_cycle", methods=["POST"])542def api_run_cycle():543 payload = request.json or {}544 result = run_cycle(payload)545 return jsonify(result)546 547 548@app.route("/api/cycles", methods=["GET"])549def api_cycles():550 db = get_db()551 rows = db.execute(552 "SELECT id, title, status, created_at FROM cycles ORDER BY created_at DESC LIMIT 30"553 ).fetchall()554 return jsonify(555 [556 {557 "id": row["id"],558 "title": row["title"],559 "status": row["status"],560 "created_at": row["created_at"],561 }562 for row in rows563 ]564 )565 566 567@app.route("/api/cycles/<cycle_id>", methods=["GET"])568def api_cycle_detail(cycle_id):569 db = get_db()570 row = db.execute("SELECT * FROM cycles WHERE id = ?", (cycle_id,)).fetchone()571 if not row:572 return jsonify({"error": "未找到任务"}), 404573 result = json.loads(row["result"] or "{}")574 return jsonify(result)575 576 577@app.route("/api/replay/<cycle_id>", methods=["GET"])578def api_replay(cycle_id):579 db = get_db()580 logs = db.execute(581 "SELECT role, content, created_at FROM logs WHERE cycle_id = ? ORDER BY id ASC", (cycle_id,)582 ).fetchall()583 return jsonify(584 [585 {"role": row["role"], "content": row["content"], "created_at": row["created_at"]}586 for row in logs587 ]588 )589 590 591@app.route("/api/memory", methods=["GET"])592def api_memory():593 snapshot = get_memory("microgrid_state", {})594 return jsonify(snapshot)595 596 597@app.route("/api/stats", methods=["GET"])598def api_stats():599 db = get_db()600 rows = db.execute("SELECT result, status, created_at FROM cycles").fetchall()601 total = len(rows)602 completed = sum(1 for r in rows if r["status"] == "completed")603 costs = []604 carbons = []605 for row in rows:606 try:607 result = json.loads(row["result"] or "{}")608 kpi = result.get("decision", {}).get("kpi", {})609 if "cost" in kpi:610 costs.append(kpi["cost"])611 if "carbon" in kpi:612 carbons.append(kpi["carbon"])613 except Exception:614 continue615 return jsonify(616 {617 "total": total,618 "completed": completed,619 "avg_cost": round(sum(costs) / len(costs), 3) if costs else 0,620 "avg_carbon": round(sum(carbons) / len(carbons), 3) if carbons else 0,621 "last_run": rows[0]["created_at"] if rows else "暂无",622 }623 )624 625 626@app.route("/api/scenario_suggest", methods=["GET"])627def api_scenario_suggest():628 last = get_memory("microgrid_state", {})629 return jsonify(630 {631 "title": "智能建议场景",632 "demand_mw": 110 + random.randint(-10, 12),633 "solar_mw": 45 + random.randint(-8, 6),634 "wind_mw": 30 + random.randint(-6, 6),635 "storage_mwh": 90 + random.randint(-15, 15),636 "storage_soc": round(0.35 + random.random() * 0.45, 2),637 "priority": last.get("last_priority", "成本优先"),638 "notes": "建议场景:参考历史记忆与负荷波动生成。",639 }640 )641 642 643def detect_binary(file_path):644 with open(file_path, "rb") as f:645 chunk = f.read(1024)646 return b"\0" in chunk647 648 649def finalize_file(file_id, filename, storage_path, content_type):650 size = os.path.getsize(storage_path)651 sha256_hash = hashlib.sha256()652 with open(storage_path, "rb") as f:653 for block in iter(lambda: f.read(4096), b""):654 sha256_hash.update(block)655 is_binary = 1 if detect_binary(storage_path) else 0656 db = get_db()657 db.execute(658 "INSERT INTO files (id, filename, size_bytes, sha256, content_type, is_binary, storage_path, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",659 (file_id, filename, size, sha256_hash.hexdigest(), content_type or "", is_binary, storage_path, now_ts()),660 )661 db.commit()662 return {663 "id": file_id,664 "filename": filename,665 "size_bytes": size,666 "sha256": sha256_hash.hexdigest(),667 "is_binary": bool(is_binary),668 }669 670 671@app.route("/api/upload", methods=["POST"])672def api_upload():673 if "file" not in request.files:674 return jsonify({"error": "未检测到文件"}), 400675 file = request.files["file"]676 if not file.filename:677 return jsonify({"error": "文件名为空"}), 400678 filename = os.path.basename(file.filename)679 file_id = str(uuid.uuid4())680 storage_path = os.path.join(INSTANCE_DIR, f"{file_id}_{filename}")681 file.save(storage_path)682 payload = finalize_file(file_id, filename, storage_path, file.content_type)683 return jsonify({"status": "ok", "file": payload})684 685 686@app.route("/api/upload_chunk", methods=["POST"])687def api_upload_chunk():688 file_id = request.headers.get("X-File-Id")689 filename = request.headers.get("X-File-Name")690 total = int(request.headers.get("X-Chunk-Total", "1"))691 index = int(request.headers.get("X-Chunk-Index", "0"))692 if not file_id or not filename:693 return jsonify({"error": "缺少文件标识"}), 400694 chunk = request.data695 if not chunk:696 return jsonify({"error": "分片内容为空"}), 400697 temp_path = os.path.join(INSTANCE_DIR, f"{file_id}.part")698 with open(temp_path, "ab") as f:699 f.write(chunk)700 if index + 1 == total:701 final_name = os.path.basename(filename)702 final_path = os.path.join(INSTANCE_DIR, f"{file_id}_{final_name}")703 os.replace(temp_path, final_path)704 payload = finalize_file(file_id, final_name, final_path, request.headers.get("X-Content-Type"))705 return jsonify({"status": "completed", "file": payload})706 return jsonify({"status": "chunk_received", "index": index})707 708 709@app.route("/api/files", methods=["GET"])710def api_files():711 db = get_db()712 rows = db.execute(713 "SELECT id, filename, size_bytes, sha256, is_binary, created_at FROM files ORDER BY created_at DESC LIMIT 50"714 ).fetchall()715 return jsonify(716 [717 {718 "id": row["id"],719 "filename": row["filename"],720 "size_bytes": row["size_bytes"],721 "sha256": row["sha256"],722 "is_binary": bool(row["is_binary"]),723 "created_at": row["created_at"],724 }725 for row in rows726 ]727 )728 729 730@app.route("/api/files/<file_id>", methods=["GET"])731def api_download(file_id):732 db = get_db()733 row = db.execute("SELECT storage_path, filename FROM files WHERE id = ?", (file_id,)).fetchone()734 if not row:735 return jsonify({"error": "未找到文件"}), 404736 return send_file(row["storage_path"], as_attachment=True, download_name=row["filename"])737 738 739if __name__ == "__main__":740 app.run(host="0.0.0.0", port=7860, debug=True)741 