From dc7efb8ff808e5dd72833a395ac3775e8f6b8ddc Mon Sep 17 00:00:00 2001 From: wangbeihong Date: Sat, 14 Mar 2026 00:32:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E5=8A=9F=E8=83=BD=EF=BC=8C=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E6=97=B6=E9=94=99=E8=AF=AF=E5=92=8CAI?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E5=A4=B1=E8=B4=A5=EF=BC=8C=E6=8F=90=E4=BE=9B?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E6=9F=A5=E7=9C=8B=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + README.md | 10 +++ app.py | 139 +++++++++++++++++++++++++++++++++++ data/ai_settings.json | 2 +- static/css/style.css | 5 ++ templates/_account_menu.html | 1 + templates/ai_settings.html | 1 + templates/error.html | 3 + templates/logs.html | 43 +++++++++++ templates/types.html | 5 +- 10 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 templates/logs.html diff --git a/.gitignore b/.gitignore index 8fe066f..a0fec98 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ data/*.sqlite data/*.sqlite3 data/ai_settings.json data/box_types.json +data/*.log *.db *.sqlite *.sqlite3 diff --git a/README.md b/README.md index 00055ce..95a331c 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,16 @@ $env:SILICONFLOW_MODEL="Qwen/Qwen2.5-7B-Instruct" - 页面:`/ai/settings` - 保存文件:`data/ai_settings.json` +### 2.6 系统日志 + +为便于定位 AI 请求失败、配置错误、未处理异常,系统提供日志查看页面: + +- 页面入口:右上角 `账号` -> `系统日志` +- 快速入口:`仓库概览` 页面 AI 补货建议卡片中的 `日志` +- 日志文件:`data/app.log` + +当 AI 补货建议出现“请求失败,请稍后重试”时,优先打开系统日志查看最近的 `ERROR` 或 `WARNING` 记录。 + ## 3. 页面说明 ### 3.1 首页 `/` diff --git a/app.py b/app.py index bee8d08..740c62e 100644 --- a/app.py +++ b/app.py @@ -11,11 +11,14 @@ import re import csv import json import hmac +import logging import difflib import base64 import random +import socket import string import hashlib +import traceback import time import urllib.error import urllib.parse @@ -23,9 +26,11 @@ import urllib.request from copy import deepcopy from io import StringIO from datetime import datetime, timedelta +from logging.handlers import RotatingFileHandler from flask import Flask, Response, redirect, render_template, request, session, url_for from flask_sqlalchemy import SQLAlchemy +from werkzeug.exceptions import HTTPException from werkzeug.security import check_password_hash, generate_password_hash @@ -33,6 +38,7 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DB_DIR = os.path.join(BASE_DIR, "data") os.makedirs(DB_DIR, exist_ok=True) DB_PATH = os.path.join(DB_DIR, "inventory.db") +APP_LOG_PATH = os.path.join(DB_DIR, "app.log") # Flask 和 SQLAlchemy 基础初始化。 app = Flask(__name__) @@ -44,6 +50,58 @@ app.config["SECRET_KEY"] = os.environ.get( ) db = SQLAlchemy(app) + +def _setup_app_logger() -> None: + """初始化应用日志文件。 + + 中文说明:把运行期错误、AI调用失败等信息写入 data/app.log, + 方便在系统内直接查看,不再依赖控制台输出。 + """ + if getattr(app, "_inventory_log_ready", False): + return + + handler = RotatingFileHandler( + APP_LOG_PATH, + maxBytes=512 * 1024, + backupCount=3, + encoding="utf-8", + ) + handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")) + + for existing_handler in app.logger.handlers: + if getattr(existing_handler, "baseFilename", "") == handler.baseFilename: + app._inventory_log_ready = True + return + + app.logger.addHandler(handler) + app.logger.setLevel(logging.INFO) + app._inventory_log_ready = True + + +def _log_event(level: int, event: str, **context) -> None: + parts = [] + for key, value in context.items(): + if value is None: + continue + text = str(value).replace("\n", " ").strip() + if not text: + continue + if len(text) > 280: + text = text[:277] + "..." + parts.append(f"{key}={text}") + message = event if not parts else f"{event} | {' | '.join(parts)}" + app.logger.log(level, message) + + +def _read_log_lines(limit: int = 200) -> list[str]: + if not os.path.exists(APP_LOG_PATH): + return [] + with open(APP_LOG_PATH, "r", encoding="utf-8", errors="ignore") as file_obj: + return [line.rstrip("\n") for line in file_obj.readlines()[-limit:]] + + +_setup_app_logger() + # 这里集中放全局常量,避免后面函数里散落硬编码。 LOW_STOCK_THRESHOLD = 5 BOX_TYPES_OVERRIDE_PATH = os.path.join(DB_DIR, "box_types.json") @@ -2807,6 +2865,8 @@ def _call_siliconflow_chat( raise RuntimeError(f"AI 服务返回 HTTP {exc.code}: {detail[:200]}") from exc except urllib.error.URLError as exc: raise RuntimeError(f"AI 服务连接失败: {exc.reason}") from exc + except (TimeoutError, socket.timeout) as exc: + raise RuntimeError(f"AI 服务读取超时(>{timeout}秒),请稍后重试或在 AI 参数中调大超时") from exc try: data = json.loads(raw) @@ -2856,6 +2916,28 @@ def logout_page(): return redirect(url_for("login_page")) +@app.route("/system/logs") +def system_logs_page(): + """查看系统日志。 + + 中文说明:用于定位 AI 调用失败、未处理异常、配置错误等问题, + 默认展示最近 200 行,可通过 query 参数 lines 调整。 + """ + raw_lines = (request.args.get("lines", "200") or "200").strip() + try: + line_limit = max(50, min(int(raw_lines), 1000)) + except ValueError: + line_limit = 200 + + log_lines = _read_log_lines(line_limit) + return render_template( + "logs.html", + log_lines=log_lines, + line_limit=line_limit, + log_path=APP_LOG_PATH, + ) + + @app.route("/account/password", methods=["GET", "POST"]) def change_password_page(): """登录后修改当前账号密码。 @@ -4244,6 +4326,14 @@ def ai_restock_plan(): timeout=ai_settings["timeout"], ) except RuntimeError as exc: + _log_event( + logging.WARNING, + "ai_restock_plan_runtime_error", + error=str(exc), + model=ai_settings.get("model", ""), + api_url=ai_settings.get("api_url", ""), + low_stock_count=len(data.get("low_stock_items", [])), + ) fallback_plan = _build_rule_based_plan() return { "ok": False, @@ -4251,6 +4341,22 @@ def ai_restock_plan(): "plan": fallback_plan, "data": data, }, 400 + except Exception as exc: + _log_event( + logging.ERROR, + "ai_restock_plan_unexpected_error", + error=str(exc), + traceback=traceback.format_exc(), + model=ai_settings.get("model", ""), + api_url=ai_settings.get("api_url", ""), + ) + fallback_plan = _build_rule_based_plan() + return { + "ok": False, + "message": "服务器内部错误,请到系统日志查看详情", + "plan": fallback_plan, + "data": data, + }, 500 parse_warning = "" try: @@ -4799,6 +4905,39 @@ def clear_stats_logs(): return redirect(url_for("stats_page", days=days, box_type=box_type_filter, notice=notice)) +@app.errorhandler(Exception) +def handle_app_exception(exc: Exception): + if isinstance(exc, HTTPException): + status_code = exc.code or 500 + title = exc.name or "请求失败" + message = exc.description or "请求处理失败" + else: + status_code = 500 + title = "服务器异常" + message = "服务器内部错误,请到系统日志查看详情。" + + _log_event( + logging.ERROR if status_code >= 500 else logging.WARNING, + "unhandled_exception", + status_code=status_code, + path=request.path, + method=request.method, + error=str(exc), + traceback=traceback.format_exc() if status_code >= 500 else "", + ) + + return ( + render_template( + "error.html", + status_code=status_code, + title=title, + message=message, + back_url=request.referrer or url_for("types_page"), + ), + status_code, + ) + + def bootstrap() -> None: """应用启动时初始化数据库。 diff --git a/data/ai_settings.json b/data/ai_settings.json index 6cefb52..995b9a3 100644 --- a/data/ai_settings.json +++ b/data/ai_settings.json @@ -2,7 +2,7 @@ "api_url": "https://api.siliconflow.cn/v1/chat/completions", "model": "Pro/zai-org/GLM-5", "api_key": "sk-pekgnbdvwgydxzteabnykswjadkitoopwcekmksydfoslmlo", - "timeout": 30, + "timeout": 120, "restock_threshold": 2, "restock_limit": 24, "lcsc_base_url": "https://open-api.jlc.com", diff --git a/static/css/style.css b/static/css/style.css index f1eb52d..b147816 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -613,6 +613,11 @@ body { background: color-mix(in srgb, var(--card-alt) 76%, var(--accent) 24%); } +.log-viewer { + max-height: 70vh; + overflow: auto; +} + .box-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); diff --git a/templates/_account_menu.html b/templates/_account_menu.html index 5986e53..f5a2e88 100644 --- a/templates/_account_menu.html +++ b/templates/_account_menu.html @@ -5,6 +5,7 @@

在线时长:{{ auth_online_for }}

最后活动:{{ auth_last_active_at }}

空闲时长:{{ auth_idle_for }}

+ 系统日志 修改密码 退出登录 diff --git a/templates/ai_settings.html b/templates/ai_settings.html index e6cbd6c..e37a783 100644 --- a/templates/ai_settings.html +++ b/templates/ai_settings.html @@ -46,6 +46,7 @@ 超时(秒) +

若使用较慢模型(如 GLM-5、较大推理模型)生成补货建议超时,可先将这里调到 60-90 秒再重试。