# -*- coding: utf-8 -*- """NBA球迷大全 - Flask 服务入口(API + 前端静态页 + 管理后台 + 详情页)""" import logging import os from flask import Flask, jsonify, request, send_from_directory from config import STATIC_DIR, SERVICE_NAME, SERVICE_PORT, SERVICE_HOST from db import init_db, table_count, query_one import tools import chat import vector_store import entity_linker import playoffs as playoffs_mod import admin as admin_mod logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") log = logging.getLogger("app") app = Flask(__name__, static_folder=None) app.config["JSON_AS_ASCII"] = False # ------------------------------------------------------------------ 页面 @app.route("/") def index(): return send_from_directory(STATIC_DIR, "index.html") @app.route("/admin") def admin_page(): return send_from_directory(STATIC_DIR, "admin.html") # 独立详情页(前端 detail.js 按路径加载数据渲染) @app.route("//") def detail_page(etype, eid): if etype in ("team", "player", "game", "news", "person"): return send_from_directory(STATIC_DIR, "detail.html") return send_from_directory(STATIC_DIR, "index.html") @app.route("/static/") def static_files(path): return send_from_directory(STATIC_DIR, path) # ------------------------------------------------------------------ 健康/统计 @app.route("/api/health") def health(): return jsonify({"status": "ok", "service": SERVICE_NAME, "db": {t: table_count(t) for t in ("teams", "players", "games", "news", "persons")}, "vector_docs": vector_store.collection_count()}) @app.route("/api/suggestions") def suggestions(): return jsonify(chat.suggest_questions()) @app.route("/api/boot") def boot(): """对话界面启动信息:开场白 + 快捷问题(管理后台可配置)""" return jsonify(chat.boot_info()) @app.route("/api/suggest", methods=["POST"]) def api_suggest(): """基于对话历史预测底部快捷问题(个数后台可配,默认3)""" body = request.get_json(force=True, silent=True) or {} history = body.get("history") or [] try: n = int(admin_mod.get_config().get("suggestion_count", "3") or "3") except Exception: n = 3 try: return jsonify({"suggestions": chat.predict_suggestions(history, n)}) except Exception as e: log.exception("suggest error") return jsonify({"suggestions": chat.suggest_questions()[:n]}), 200 # ------------------------------------------------------------------ 对话 @app.route("/api/chat", methods=["POST"]) def api_chat(): body = request.get_json(force=True, silent=True) or {} message = (body.get("message") or "").strip() history = body.get("history") or [] if not message: return jsonify({"error": "消息不能为空"}), 400 try: reply, sources, used_tools, news_refs = chat.chat_once(message, history) # 实体识别程序:扫描回答,标记球队/球员/人物/比赛 + 快速查看卡片 game_refs = [it for s in sources if s.get("tool") in ("search_games", "get_game_detail") for it in s.get("items", [])] mark_mode = admin_mod.get_config().get("entity_mark_mode", "first") spans, cards = entity_linker.link_entities(reply, game_refs=game_refs, mark_mode=mark_mode) return jsonify({"reply": reply, "sources": sources, "used_tools": used_tools, "news_refs": news_refs, "entities": spans, "cards": cards}) except Exception as e: log.exception("chat error") return jsonify({"error": f"服务异常: {e}"}), 500 # ------------------------------------------------------------------ 球队 @app.route("/api/teams") def api_teams(): q = request.args.get("q", "") r = tools.search_teams(q, limit=int(request.args.get("limit", 50))) return jsonify(r.get("results", [])) @app.route("/api/teams/") def api_team(tid): t = tools.get_team(tid) if not t: return jsonify({"error": "not found"}), 404 roster = tools.search_players(t["name"], limit=20)["results"] games = tools.search_games(t["name"], limit=10)["results"] # 各赛季排名 from db import query as dbq seasons = dbq("""SELECT season, conference, rank, wins, losses FROM standings WHERE team_id=? ORDER BY season DESC""", (tid,)) return jsonify({"team": t, "roster": roster, "recent_games": games, "standings": seasons}) # ------------------------------------------------------------------ 球员 @app.route("/api/players") def api_players(): q = request.args.get("q", "") position = request.args.get("position", "") page = max(1, int(request.args.get("page", 1))) size = min(60, max(1, int(request.args.get("size", 24)))) return jsonify(tools.list_players(q, position=position, page=page, size=size)) @app.route("/api/players/") def api_player(pid): p = tools.get_player(pid) if not p: return jsonify({"error": "not found"}), 404 p["game_logs"] = tools.player_game_logs(pid) return jsonify(p) # ------------------------------------------------------------------ 比赛 @app.route("/api/games") def api_games(): q = request.args.get("q", "") status = request.args.get("status", "") page = max(1, int(request.args.get("page", 1))) size = min(40, max(1, int(request.args.get("size", 12)))) return jsonify(tools.list_games(q, status=status, page=page, size=size)) @app.route("/api/games/") def api_game(gid): g = tools.get_game_detail(gid) if not g: return jsonify({"error": "not found"}), 404 return jsonify(g) # ------------------------------------------------------------------ 排名 @app.route("/api/standings") def api_standings(): conf = request.args.get("conf", "") season = request.args.get("season", "") or None r = tools.search_standings(conf, season=season, limit=30) return jsonify(r.get("results", [])) # ------------------------------------------------------------------ 赛季列表 / 季后赛对阵图 @app.route("/api/seasons") def api_seasons(): return jsonify(playoffs_mod.seasons()) @app.route("/api/playoffs") def api_playoffs(): season = request.args.get("season", "") or playoffs_mod.seasons()[0] try: return jsonify(playoffs_mod.build_bracket(season)) except Exception as e: return jsonify({"error": str(e)}), 500 # ------------------------------------------------------------------ 新闻 @app.route("/api/news") def api_news(): q = request.args.get("q", "") kind = request.args.get("kind", "") tag = request.args.get("tag", "") page = max(1, int(request.args.get("page", 1))) size = min(30, max(1, int(request.args.get("size", 10)))) return jsonify(tools.list_news(q, kind=kind, tag=tag, page=page, size=size)) @app.route("/api/news/") def api_news_detail(nid): from db import query_one as q1 n = q1("SELECT * FROM news WHERE id=?", (nid,)) if not n: return jsonify({"error": "not found"}), 404 return jsonify(n) # ------------------------------------------------------------------ 人物 @app.route("/api/persons") def api_persons(): q = request.args.get("q", "") role = request.args.get("role", "") page = max(1, int(request.args.get("page", 1))) size = min(30, max(1, int(request.args.get("size", 12)))) return jsonify(tools.list_persons(q, role=role, page=page, size=size)) @app.route("/api/persons/") def api_person(pid): from db import query_one as q1 p = q1("SELECT * FROM persons WHERE id=?", (pid,)) if not p: return jsonify({"error": "not found"}), 404 return jsonify(p) # ================================================================== 管理后台 @app.route("/api/admin/login", methods=["POST"]) def admin_login(): return admin_mod.login() @app.route("/api/admin/logout", methods=["POST"]) @admin_mod.require_admin def admin_logout(): return admin_mod.logout() @app.route("/api/admin/stats") @admin_mod.require_admin def admin_stats(): return admin_mod.stats() @app.route("/api/admin/config") @admin_mod.require_admin def admin_config_get(): return admin_mod.get_config_api() @app.route("/api/admin/config", methods=["PUT"]) @admin_mod.require_admin def admin_config_put(): return admin_mod.update_config() @app.route("/api/admin/suggestions/reset", methods=["POST"]) @admin_mod.require_admin def admin_suggestions_reset(): return admin_mod.reset_suggestions() @app.route("/api/admin/") @admin_mod.require_admin def admin_list(table): return admin_mod.list_rows(table) @app.route("/api/admin/
", methods=["POST"]) @admin_mod.require_admin def admin_create(table): return admin_mod.create_row(table) @app.route("/api/admin/
/") @admin_mod.require_admin def admin_get(table, rid): return admin_mod.get_row(table, rid) @app.route("/api/admin/
/", methods=["PUT"]) @admin_mod.require_admin def admin_update(table, rid): return admin_mod.update_row(table, rid) @app.route("/api/admin/
/", methods=["DELETE"]) @admin_mod.require_admin def admin_delete(table, rid): return admin_mod.delete_row(table, rid) if __name__ == "__main__": init_db() admin_mod.init_defaults() log.info("%s 启动于 http://%s:%s", SERVICE_NAME, SERVICE_HOST, SERVICE_PORT) app.run(host=SERVICE_HOST, port=SERVICE_PORT, threaded=True)