307 lines
9.7 KiB
Python
307 lines
9.7 KiB
Python
# -*- 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("/<etype>/<int:eid>")
|
|
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/<path:path>")
|
|
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/<int:tid>")
|
|
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/<int:pid>")
|
|
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/<int:gid>")
|
|
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", "")
|
|
limit = int(request.args.get("limit", 20))
|
|
if q:
|
|
r = tools.search_news(q, limit=limit)
|
|
return jsonify(r.get("results", []))
|
|
from db import query
|
|
rows = query("""SELECT id,title,author,source,publish_time,tags,kind,substr(content,1,160) AS summary
|
|
FROM news WHERE (?='' OR kind=?) ORDER BY publish_time DESC, id DESC LIMIT ?""",
|
|
(kind, kind, limit))
|
|
return jsonify(rows)
|
|
|
|
|
|
@app.route("/api/news/<int:nid>")
|
|
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", "")
|
|
limit = int(request.args.get("limit", 50))
|
|
r = tools.search_persons(q, limit=limit)
|
|
results = r.get("results", [])
|
|
if role:
|
|
results = [p for p in results if p["role"] == role]
|
|
return jsonify(results)
|
|
|
|
|
|
@app.route("/api/persons/<int:pid>")
|
|
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/<table>")
|
|
@admin_mod.require_admin
|
|
def admin_list(table):
|
|
return admin_mod.list_rows(table)
|
|
|
|
|
|
@app.route("/api/admin/<table>", methods=["POST"])
|
|
@admin_mod.require_admin
|
|
def admin_create(table):
|
|
return admin_mod.create_row(table)
|
|
|
|
|
|
@app.route("/api/admin/<table>/<int:rid>")
|
|
@admin_mod.require_admin
|
|
def admin_get(table, rid):
|
|
return admin_mod.get_row(table, rid)
|
|
|
|
|
|
@app.route("/api/admin/<table>/<int:rid>", methods=["PUT"])
|
|
@admin_mod.require_admin
|
|
def admin_update(table, rid):
|
|
return admin_mod.update_row(table, rid)
|
|
|
|
|
|
@app.route("/api/admin/<table>/<int:rid>", 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)
|