CoolFace
Apppublic

duqing026/microgrid-dispatch-agent

sourceHugging Faceupdated 7mo agoView on Hugging Face
0likes
app.py741 linesDownload Raw Back to root
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