From e11677809e3711c6c54d4a74368284b1dad6f99c Mon Sep 17 00:00:00 2001 From: hz4th_coder Date: Sun, 16 Aug 2026 23:59:14 +0800 Subject: [PATCH] =?UTF-8?q?NBA=E7=90=83=E8=BF=B7=E5=A4=A7=E5=85=A8=20v1.0.?= =?UTF-8?q?0=EF=BC=9A=E5=AF=B9=E8=AF=9D=E9=97=AE=E7=AD=94+=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E6=B5=8F=E8=A7=88=E7=B3=BB=E7=BB=9F=EF=BC=88DeepSeek+?= =?UTF-8?q?RAG+SQLite=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 + README.md | 113 +++++++++++++ api.py | 174 ++++++++++++++++++++ chat.py | 225 ++++++++++++++++++++++++++ config.py | 43 +++++ db.py | 208 ++++++++++++++++++++++++ llm.py | 84 ++++++++++ requirements.txt | 2 + seed.py | 164 +++++++++++++++++++ seed_games.py | 141 ++++++++++++++++ seed_news.py | 158 ++++++++++++++++++ seed_persons.py | 128 +++++++++++++++ seed_players_a.py | 175 ++++++++++++++++++++ seed_players_b.py | 179 +++++++++++++++++++++ seed_teams.py | 58 +++++++ start.sh | 45 ++++++ static/app.js | 276 ++++++++++++++++++++++++++++++++ static/index.html | 105 ++++++++++++ static/style.css | 110 +++++++++++++ tools.py | 397 ++++++++++++++++++++++++++++++++++++++++++++++ vector_store.py | 138 ++++++++++++++++ 21 files changed, 2927 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 api.py create mode 100644 chat.py create mode 100644 config.py create mode 100644 db.py create mode 100644 llm.py create mode 100644 requirements.txt create mode 100644 seed.py create mode 100644 seed_games.py create mode 100644 seed_news.py create mode 100644 seed_persons.py create mode 100644 seed_players_a.py create mode 100644 seed_players_b.py create mode 100644 seed_teams.py create mode 100755 start.sh create mode 100644 static/app.js create mode 100644 static/index.html create mode 100644 static/style.css create mode 100644 tools.py create mode 100644 vector_store.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2e1be8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +data/*.db* +logs/* +__pycache__/ +*.pyc diff --git a/README.md b/README.md new file mode 100644 index 0000000..6bc5dce --- /dev/null +++ b/README.md @@ -0,0 +1,113 @@ +# 🏀 NBA球迷大全 + +面向球迷的 NBA 信息问答系统:对话即可查询比赛、球员、球队、新闻、人物、百科等准确信息。 +基于 **大模型(DeepSeek)+ 结构化查询(SQLite)+ RAG 向量检索(Chroma + bge-large-zh)** 混合架构, +数据全部来自内置数据库,回答准确可溯源。 + +## ✨ 功能 + +- 💬 **智能对话**:自然语言提问,自动路由到数据库/向量库,返回带来源的准确回答 + - 例:「2026年总决赛谁赢了」「库里本赛季场均数据」「约基奇和字母哥谁得分多」「雷霆为什么能夺冠」 +- 🏟️ **数据浏览**:球队 / 球员 / 比赛(含技术统计)/ 新闻 / 人物 / 排名 六大浏览页面 +- 🧠 **RAG 检索**:新闻与百科向量化存储,语义检索 + Rerank 精排 +- 🔧 **Function Calling**:8 个查询工具(球队/球员/比赛/单场详情/排名/新闻/人物/百科),多轮工具调用 + 实体覆盖补全 +- 🧵 **多轮对话**:支持上下文指代(「他拿过几个MVP?」→ 上一话题对象) + +## 🏗️ 系统架构 + +``` +用户提问 + │ + ▼ +┌─────────────┐ 工具调用(8个) ┌──────────────────┐ +│ DeepSeek │ ───────────────► │ tools.py 查询层 │ +│ deepseek- │ ◄─────────────── │ · SQLite 结构化 │ +│ v4-flash │ 工具结果回填 │ · Chroma 向量检索 │ +└─────────────┘ └──────────────────┘ + │ 最终答案(数据准确 + 来源标注) + ▼ + 前端(对话 + 数据浏览) +``` + +| 模块 | 技术 | 说明 | +|------|------|------| +| 对话大脑 | DeepSeek `deepseek-v4-flash`(函数调用) | 意图理解、工具选择、答案生成 | +| 结构化数据 | SQLite(本地) | 球队/球员/比赛/统计/排名/新闻/人物 | +| 向量检索 | Chroma(16010,REST) | 新闻/百科语义检索(cosine) | +| Embedding | bge-large-zh-v1.5(16011,1024维) | 中文语义向量 | +| Rerank | bge-reranker-v2-m3(16011) | 检索精排(可选) | +| 前端 | 原生 HTML/JS(无构建) | 深色球赛风,对话 + 六大浏览页 | + +## 📁 目录结构 + +``` +nba-fan-hub/ +├── api.py # Flask 服务(REST API + 静态页) +├── chat.py # 对话管线(工具路由 + 兜底 + 覆盖补全) +├── tools.py # 8 个查询工具(SQL + 向量),含绰号别名/模糊匹配 +├── llm.py # DeepSeek 调用(思考模式 reasoning_content 回传) +├── vector_store.py # Embedding + Chroma + Rerank(纯 REST,零额外依赖) +├── db.py # SQLite 连接与建表 +├── seed.py # 种子数据入口(幂等,可 --rebuild-vector) +├── seed_*.py # 模拟数据(30队/154球员/54比赛/46新闻百科/38人物) +├── static/ # 前端页面 +├── data/ # nba_fan.db(自动生成) +└── start.sh # 启停脚本(start/stop/restart/status/seed) +``` + +## 🚀 快速开始 + +```bash +# 1. 灌数据(首次或数据更新后) +./start.sh seed # 或 python3 seed.py --rebuild-vector + +# 2. 启动 +./start.sh start # http://:16090 +./start.sh status # 查看状态 +``` + +依赖:`flask`、`requests`(openclaw conda 环境已具备)。外部依赖服务: +- 大模型:DeepSeek API(key 配置于 `config.py`) +- 向量库:Chroma @ `121.40.164.32:16010` +- Embedding/Rerank:@ `121.40.164.32:16011` + +## 🔌 REST API + +| 接口 | 说明 | +|------|------| +| `POST /api/chat` | 对话 `{message, history}` → `{reply, sources, used_tools}` | +| `GET /api/teams[?q=]` `GET /api/teams/` | 球队列表/详情(含阵容+近期比赛) | +| `GET /api/players[?q=]` `GET /api/players/` | 球员列表/详情 | +| `GET /api/games[?q=&status=]` `GET /api/games/` | 比赛列表/详情(含技术统计) | +| `GET /api/standings?conf=西部/东部` | 排名 | +| `GET /api/news[?q=&kind=]` `GET /api/news/` | 新闻/百科 | +| `GET /api/persons[?q=&role=]` | 人物(coach/commentator/host/agent/gm/legend) | +| `GET /api/health` | 健康检查 + 数据统计 | + +## 🧩 扩展设计(重点) + +数据库采用「运动-联赛-球队-球员」分层,新增球类/联赛/人物角色无需改代码: + +| 扩展需求 | 怎么做 | +|----------|--------| +| **新增 CBA** | `sports` 已含 basketball;`leagues` 表加一行 `CBA`,灌入 CBA 球队/球员/比赛即可,全部工具自动生效 | +| **新增足球** | `sports` 表加 `football`;`leagues` 加英超/西甲等;球员/球队表字段通用(位置、数据字段可增列) | +| **新增人物角色**(教练/经纪人/评论员/主持人/总经理/球探…) | `persons.role` 是自由字符串,新增角色直接入库即可 | +| **新增查询工具** | `tools.py` 加一个函数 + 注册进 `TOOLS`/`TOOL_HANDLERS` 即自动接入对话 | +| **向量库隔离** | Chroma 按集合名隔离(`nba_fan_knowledge_v1` → `cba_...`),互不影响 | +| **数据更新** | 修改 `seed_*.py` 后 `./start.sh seed --rebuild-vector` 一键重建 | + +## 🧠 对话管线要点 + +1. **工具优先**:系统提示词强制「数据问题先查库」,8 个工具返回结构化 JSON +2. **思考模式兼容**:DeepSeek 思考模型要求回传 `reasoning_content`,已处理 +3. **文本工具调用兜底**:模型偶尔把工具调用写成正文,正则解析 + 未知工具名智能映射 +4. **实体覆盖补全**:多实体问题(「约基奇和字母哥…」)自动拆词补查,杜绝漏数据 +5. **多层兜底**:LLM 异常 → 关键词预检索注入;最终轮仍异常 → 工具结果摘要 +6. **诚实原则**:查不到如实说明,常识补充与数据库数据明确区分 + +## 📊 数据规模(模拟数据,2025-26 赛季) + +30 支球队 · 154 名球员(含绰号别名)· 54 场比赛(常规赛/季后赛/总决赛/夏季联赛)· 93 条技术统计 · 46 篇新闻/百科 · 38 位人物 + +> ⚠️ 数据为演示用模拟数据(2026 总决赛剧情:雷霆 4-2 凯尔特人,SGA FMVP),接入真实数据源时仅需替换 seed 数据。 diff --git a/api.py b/api.py new file mode 100644 index 0000000..dc8d023 --- /dev/null +++ b/api.py @@ -0,0 +1,174 @@ +# -*- 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 + +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("/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/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 = chat.chat_once(message, history) + return jsonify({"reply": reply, "sources": sources, "used_tools": used_tools}) + 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"] + return jsonify({"team": t, "roster": roster, "recent_games": games}) + + +# ------------------------------------------------------------------ 球员 +@app.route("/api/players") +def api_players(): + q = request.args.get("q", "") + r = tools.search_players(q, limit=int(request.args.get("limit", 60))) + return jsonify(r.get("results", [])) + + +@app.route("/api/players/") +def api_player(pid): + p = tools.get_player(pid) + if not p: + return jsonify({"error": "not found"}), 404 + return jsonify(p) + + +# ------------------------------------------------------------------ 比赛 +@app.route("/api/games") +def api_games(): + q = request.args.get("q", "") + status = request.args.get("status", "") + limit = int(request.args.get("limit", 30)) + r = tools.search_games(q, limit=limit) + results = r.get("results", []) + if status: + results = [g for g in results if g["status"] == status] + return jsonify(results) + + +@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", "") + r = tools.search_standings(conf, limit=30) + return jsonify(r.get("results", [])) + + +# ------------------------------------------------------------------ 新闻 +@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/") +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/") +def api_person(pid): + r = tools.search_persons("", limit=100) + p = next((x for x in r["results"] if x["id"] == pid), None) + if not p: + return jsonify({"error": "not found"}), 404 + return jsonify(p) + + +if __name__ == "__main__": + init_db() + log.info("%s 启动于 http://%s:%s", SERVICE_NAME, SERVICE_HOST, SERVICE_PORT) + app.run(host=SERVICE_HOST, port=SERVICE_PORT, threaded=True) diff --git a/chat.py b/chat.py new file mode 100644 index 0000000..8c0342d --- /dev/null +++ b/chat.py @@ -0,0 +1,225 @@ +# -*- coding: utf-8 -*- +""" +对话管线(RAG 混合检索 + 函数调用): +1. 系统提示词 + 用户问题 → DeepSeek(带 8 个工具) +2. 模型选择工具 → 执行(SQLite 结构化查询 + Chroma 向量检索) +3. 工具结果回填 → DeepSeek 生成最终答案(数据准确,标注来源) +4. 兜底:LLM 或工具异常时,用关键词预检索注入上下文后直接回答 +""" +import json +import logging +import re + +import llm +import tools as tools_mod +from db import query + +log = logging.getLogger("chat") + +SYSTEM_PROMPT = """你是「NBA球迷大全」智能助手,为球迷提供比赛、球员、球队、新闻、人物等准确信息。 + +工作准则: +1. 用户问到时事、数据、赛程类问题,必须先调用工具查询数据库,用工具返回的真实数据回答,严禁编造具体比分、数据、日期。 +2. 工具结果就是权威数据源。回答时引用关键数据(比分、时间、数据),并标注来源(如「数据库」「新闻」)。 +3. 多步问题可以连续调用多个工具(如先查球队,再查该队比赛)。比较多个球员/球队时,把每个名字拆开单独调用一次工具(例如“约基奇和字母哥谁强”应分别调用 search_players("约基奇") 与 search_players("字母哥"))。 +4. 查不到时如实说"数据库暂未收录",可以基于常识补充介绍,但要明确区分"数据库数据"与"常识补充"。 +5. 回答使用中文,简洁有条理,可适当使用小标题或列表;不要啰嗦。 +6. 严禁在回答正文中输出 tool_calls、XML 或函数调用代码——需要数据时直接调用工具函数,或直接如实回答;调用了工具就用工具返回的数据作答。 +7. 当前赛季为 2025-26 赛季,总决赛已于 2026年6月结束,雷霆 4-2 击败凯尔特人夺冠。""" + + +def _tool_result_to_text(name, result): + """把工具结果压缩成给模型的文本(控制 token 量)""" + if not result or result.get("_source") == "error": + return f"[工具 {name}] 查询失败:{result.get('error','未知错误') if result else '无结果'}" + results = result.get("results", []) + if not results: + return f"[工具 {name}] 未找到相关记录。" + lines = [f"[工具 {name}] 查询到 {len(results)} 条记录:"] + for r in results[:8]: + lines.append(json.dumps(r, ensure_ascii=False)[:600]) + return "\n".join(lines) + + +def _grounding_context(user_msg): + """兜底检索:用关键词在数据库里快速找相关记录,注入上下文""" + ctx = [] + for name, fn in (("teams", tools_mod.search_teams), ("players", tools_mod.search_players), + ("games", tools_mod.search_games), ("news", tools_mod.search_news), + ("persons", tools_mod.search_persons)): + try: + r = fn(user_msg, limit=3) + if r.get("results"): + ctx.append(_tool_result_to_text(name, r)) + except Exception: + continue + return "\n".join(ctx) + + +def chat_once(user_msg, history=None): + """单轮对话。返回 (reply, sources, used_tools) + sources: 供前端展示的信息来源卡片 + """ + history = history or [] + messages = [{"role": "system", "content": SYSTEM_PROMPT}] + for h in history[-10:]: + messages.append({"role": "user", "content": h.get("user", "")}) + if h.get("assistant"): + messages.append({"role": "assistant", "content": h["assistant"]}) + messages.append({"role": "user", "content": user_msg}) + + used_tools, sources = [], [] + + # ---- 第 1 轮:带工具 + try: + resp = llm.chat(messages, tools=tools_mod.TOOLS) + except Exception as e: + log.warning("LLM 首轮失败(%s),走兜底管线", e) + ctx = _grounding_context(user_msg) + msgs = messages + ([{"role": "system", "content": f"以下是数据库检索到的可能相关信息(未直接命中时勿强行引用):\n{ctx}"}] if ctx else []) + try: + resp2 = llm.chat(msgs) + return llm.parse_content(resp2), _mk_sources(ctx), [t for t in ("grounding",) if ctx] + except Exception as e2: + return (f"抱歉,大模型服务暂时不可用({e2})。你可以稍后再试,或直接浏览下方数据页面。", [], []) + + # ---- 工具执行(单轮)+ 实体覆盖补全 → 最终无工具作答 + all_executed = [] + calls = llm.extract_tool_calls(resp) + if not calls: + calls = _parse_text_tool_calls(llm.parse_content(resp), user_msg) + if not calls: + return llm.parse_content(resp) or "(模型未返回内容)", [], [] + + executed = [] + for c in calls: + if c["name"] in [e[0] for e in executed]: + continue + result = tools_mod.run_tool(c["name"], c["arguments"]) + log.info("[tools] %s(%s) -> %d条", c["name"], + json.dumps(c["arguments"], ensure_ascii=False)[:120], len(result.get("results", []))) + executed.append((c["name"], c["id"], c["arguments"], result)) + used_tools.append(c["name"]) + if result.get("results"): + sources.append({"tool": c["name"], "items": result["results"][:3]}) + all_executed += executed + + # 覆盖补全:用户问题里提到的其他实体(多球员/多球队对比)自动补查,避免模型漏调 + covered = set() + for _n, _cid, _args, result in executed: + for r in result.get("results", []): + covered.add(r.get("name") or r.get("team") or "") + for term in tools_mod._split_terms(user_msg)[:4]: + for fn, key in ((tools_mod.search_players, "name"), (tools_mod.search_teams, "name")): + try: + r = fn(term, limit=3) + except Exception: + continue + for row in r.get("results", []): + nm = row.get(key) or row.get("name") or "" + if nm and nm not in covered: + cid = f"cover_{key}_{len(all_executed)}" + executed.append(("search_players" if key == "name" else "search_teams", cid, {"query": term}, r)) + covered.add(nm) + used_tools.append("search_players" if key == "name" else "search_teams") + if r.get("results"): + sources.append({"tool": "search_players" if key == "name" else "search_teams", "items": r["results"][:3]}) + break + + messages.append({"role": "assistant", "content": None, + "reasoning_content": llm.extract_reasoning(resp), + "tool_calls": [ + {"id": cid, "type": "function", + "function": {"name": name, "arguments": json.dumps(args, ensure_ascii=False)}} + for name, cid, args, _ in executed]}) + for name, cid, _args, result in executed: + messages.append({"role": "tool", "tool_call_id": cid, "content": _tool_result_to_text(name, result)}) + + try: + resp = llm.chat(messages) # 最终轮:不带工具,让模型基于数据作答 + except Exception as e: + log.warning("LLM 最终轮失败(%s),返回工具结果摘要", e) + return _finalize(all_executed, user_msg, messages, sources, used_tools), sources, used_tools + + reply = llm.parse_content(resp) or "" + if not reply or "" in reply or "search_" in reply: + # 模型又输出工具调用文本 → 注入上下文再答一次 + return _finalize(all_executed, user_msg, messages, sources, used_tools), sources, used_tools + return reply, sources, used_tools + + +def _finalize(executed, user_msg, messages, sources, used_tools): + """工具循环结束后:把检索结果注入上下文,让模型再总结一次(无工具),失败则给原始摘要""" + if not executed: + return "抱歉,没有检索到相关信息。请换个问法,或直接浏览下方数据页面。" + ctx = "\n\n".join(_tool_result_to_text(name, result) for name, _cid, _args, result in executed) + msgs = [{"role": "system", "content": SYSTEM_PROMPT}, + {"role": "user", "content": user_msg}, + {"role": "assistant", "content": f"我已经查询了数据库,检索结果如下:\n{ctx}\n\n请基于以上数据回答用户的问题(不要提及'工具',直接给出答案;数据不足时如实说明)。"}] + try: + r = llm.chat(msgs) + return llm.parse_content(r) or _results_summary(executed) + except Exception: + return _results_summary(executed) + + +def _results_summary(executed): + """把已执行工具的结果整理成给用户的摘要文本""" + parts = ["以下是数据库查到的相关信息:"] + for name, _cid, _args, result in executed: + parts.append(_tool_result_to_text(name, result)) + return "\n".join(parts) + + +def _parse_text_tool_calls(content, user_msg=""): + """解析模型以正文形式输出的工具调用(兜底),返回与 extract_tool_calls 同构的列表 + 支持:真实工具名 / 模型臆造的工具名(get_game_stats、search_player_stats 等)→ 映射到最接近的真实工具""" + if not content: + return [] + names = re.findall(r"[a-z_]+_[a-z_]+", content) + real = [n for n in names if n in tools_mod.TOOL_HANDLERS] + game_id = None + m = re.search(r"game_id[\"':=>]+\s*(\d+)", content) + if m: + game_id = int(m.group(1)) + out = [] + if real: + for i, n in enumerate(dict.fromkeys(real)): + q = re.findall(r"[\"'“”]([^\"'“”]{1,80})[\"'“”]", content) + qv = q[i] if i < len(q) else user_msg + args = {"query": qv} if n != "get_game_detail" else {"game_id": game_id or 0} + out.append({"name": n, "arguments": args, "id": f"text_{i}"}) + return out + # 模型臆造的工具名 → 智能映射 + game_intent = bool(re.search(r"总决赛|比赛|技术统计|统计|G\d|第.场|比分|对位", user_msg or "")) + if game_id and any("game" in n or "stat" in n for n in names): + out.append({"name": "get_game_detail", "arguments": {"game_id": game_id}, "id": "text_g"}) + elif game_intent and any("stat" in n or "score" in n or "game" in n for n in names): + out.append({"name": "search_games", "arguments": {"query": user_msg}, "id": "text_g2"}) + elif any("player" in n or "stat" in n for n in names): + out.append({"name": "search_players", "arguments": {"query": user_msg}, "id": "text_p"}) + elif any("game" in n or "match" in n for n in names): + out.append({"name": "search_games", "arguments": {"query": user_msg}, "id": "text_g2"}) + elif any("news" in n for n in names): + out.append({"name": "search_news", "arguments": {"query": user_msg}, "id": "text_n"}) + return out + + +def _mk_sources(ctx): + return [{"tool": "grounding", "items": [{"note": "关键词预检索上下文"}]}] if ctx else [] + + +def suggest_questions(): + """快捷问题(前端展示用)""" + return [ + "最近一场比赛结果", + "湖人本赛季战绩怎么样", + "库里本赛季场均数据", + "2026年总决赛谁赢了", + "SGA拿了什么荣誉", + "NBA工资帽是什么", + "介绍一下波波维奇", + "今天有什么新闻", + "西部排名", + "雷霆和凯尔特人总决赛G6数据", + ] diff --git a/config.py b/config.py new file mode 100644 index 0000000..6d0b0a5 --- /dev/null +++ b/config.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +""" +NBA球迷大全 - 全局配置 +所有环境相关配置集中在此,便于迁移与扩展(新增 CBA / 足球等只需扩展数据库,不改代码) +""" +import os + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +DATA_DIR = os.path.join(BASE_DIR, "data") +LOG_DIR = os.path.join(BASE_DIR, "logs") +STATIC_DIR = os.path.join(BASE_DIR, "static") +DB_PATH = os.path.join(DATA_DIR, "nba_fan.db") + +os.makedirs(DATA_DIR, exist_ok=True) +os.makedirs(LOG_DIR, exist_ok=True) + +# ---------------- 大模型(DeepSeek) ---------------- +LLM_BASE_URL = "https://api.deepseek.com" +LLM_API_KEY = "sk-edb9df58ff574f8c98df1cd6a425e97c" +LLM_MODEL = "deepseek-v4-flash" +LLM_TIMEOUT = 90 # 单次请求超时(秒) +LLM_MAX_TOKENS = 2048 +LLM_TEMPERATURE = 0.3 # 事实问答,低温度更准确 + +# ---------------- Embedding 服务(本地 16011,OpenAI 兼容) ---------------- +EMBEDDING_API_URL = "http://121.40.164.32:16011/v1/embeddings" +EMBEDDING_MODEL = "bge-large-zh-v1.5" # 容器内白名单模型名(1024 维) +EMBEDDING_DIM = 1024 + +# ---------------- Rerank 服务(本地 16011,Cohere 兼容,可选) ---------------- +RERANK_API_URL = "http://121.40.164.32:16011/v1/rerank" +RERANK_MODEL = "bge-reranker-v2-m3" +USE_RERANK = True + +# ---------------- Chroma 向量库(本地 16010) ---------------- +CHROMA_HOST = "121.40.164.32" +CHROMA_PORT = 16010 +CHROMA_COLLECTION = "nba_fan_knowledge_v1" # 集合名(升级可换 v2,平滑迁移) + +# ---------------- 服务 ---------------- +SERVICE_PORT = 16090 +SERVICE_HOST = "0.0.0.0" +SERVICE_NAME = "NBA球迷大全" diff --git a/db.py b/db.py new file mode 100644 index 0000000..7a91b80 --- /dev/null +++ b/db.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- +""" +数据库层:SQLite 统一存储结构化数据。 +Schema 采用"运动-联赛-球队-球员-比赛"分层设计,天然支持扩展: + - 新增 CBA → sports 表加一行 basketball,leagues 表加 CBA(复用全部代码) + - 新增足球 → sports 表加 football,league 可加 英超/西甲,球员字段可扩展 + - 新增人物角色 → persons.role 自由扩展(coach/agent/commentator/host/gm...) +""" +import sqlite3 +import threading + +from config import DB_PATH + +_SCHEMA = """ +CREATE TABLE IF NOT EXISTS sports ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + code TEXT UNIQUE NOT NULL, -- 'basketball' / 'football' ... + name TEXT NOT NULL, -- '篮球' / '足球' + name_en TEXT, + created_at TEXT DEFAULT (datetime('now','localtime')) +); + +CREATE TABLE IF NOT EXISTS leagues ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + sport_id INTEGER NOT NULL REFERENCES sports(id), + code TEXT UNIQUE NOT NULL, -- 'NBA' / 'CBA' / 'EPL' + name TEXT NOT NULL, -- '美国职业篮球联赛' + name_en TEXT, + country TEXT, + season TEXT, -- '2025-26' + created_at TEXT DEFAULT (datetime('now','localtime')) +); + +CREATE TABLE IF NOT EXISTS teams ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + league_id INTEGER NOT NULL REFERENCES leagues(id), + code TEXT, -- 'LAL' + name TEXT NOT NULL, -- '洛杉矶湖人' + name_en TEXT, -- 'Los Angeles Lakers' + city TEXT, + arena TEXT, + founded INTEGER, + champion_count INTEGER, + head_coach TEXT, + logo_url TEXT, + intro TEXT, + created_at TEXT DEFAULT (datetime('now','localtime')) +); + +CREATE TABLE IF NOT EXISTS players ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + team_id INTEGER REFERENCES teams(id), + name TEXT NOT NULL, + name_en TEXT, + position TEXT, -- PG/SG/SF/PF/C + number INTEGER, + height_cm INTEGER, + weight_kg INTEGER, + birth_date TEXT, + country TEXT, + draft_year INTEGER, + draft_pick INTEGER, -- 0 = 落选 + salary_m REAL, -- 年薪(万美元) + season_pts REAL, season_reb REAL, season_ast REAL, + season_stl REAL, season_blk REAL, season_min REAL, -- 本赛季场均 + career_pts REAL, career_reb REAL, career_ast REAL, -- 生涯场均 + career_games INTEGER, + career_steals INTEGER, career_blocks INTEGER, -- 生涯总数 + awards TEXT, -- 主要荣誉 + bio TEXT, + created_at TEXT DEFAULT (datetime('now','localtime')) +); + +CREATE TABLE IF NOT EXISTS games ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + league_id INTEGER NOT NULL REFERENCES leagues(id), + season TEXT, + round_name TEXT, -- 常规赛/季后赛首轮/总决赛... + game_time TEXT, -- '2026-06-05 09:00' + status TEXT, -- scheduled / finished / live + home_team_id INTEGER REFERENCES teams(id), + away_team_id INTEGER REFERENCES teams(id), + home_score INTEGER, + away_score INTEGER, + venue TEXT, + broadcast TEXT, + created_at TEXT DEFAULT (datetime('now','localtime')) +); + +CREATE TABLE IF NOT EXISTS game_player_stats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + game_id INTEGER NOT NULL REFERENCES games(id), + player_id INTEGER NOT NULL REFERENCES players(id), + team_id INTEGER NOT NULL REFERENCES teams(id), + points INTEGER DEFAULT 0, rebounds INTEGER DEFAULT 0, + assists INTEGER DEFAULT 0, steals INTEGER DEFAULT 0, + blocks INTEGER DEFAULT 0, minutes INTEGER DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS standings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + league_id INTEGER NOT NULL REFERENCES leagues(id), + season TEXT, + team_id INTEGER NOT NULL REFERENCES teams(id), + wins INTEGER DEFAULT 0, losses INTEGER DEFAULT 0, + win_pct REAL DEFAULT 0, + conference TEXT, -- 西部/东部 + rank INTEGER +); + +CREATE TABLE IF NOT EXISTS news ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + sport_id INTEGER NOT NULL REFERENCES sports(id), + league_id INTEGER REFERENCES leagues(id), + team_id INTEGER REFERENCES teams(id), + title TEXT NOT NULL, + content TEXT NOT NULL, + author TEXT, + source TEXT, + publish_time TEXT, + tags TEXT, + kind TEXT DEFAULT 'news', -- news / wiki(百科词条) + created_at TEXT DEFAULT (datetime('now','localtime')) +); + +CREATE TABLE IF NOT EXISTS persons ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + sport_id INTEGER NOT NULL REFERENCES sports(id), + league_id INTEGER REFERENCES leagues(id), + team_id INTEGER REFERENCES teams(id), + name TEXT NOT NULL, + name_en TEXT, + role TEXT, -- coach/agent/commentator/host/gm/legend... + role_cn TEXT, -- 教练/经纪人/评论员/主持人/总经理... + title TEXT, + bio TEXT, + achievements TEXT, + created_at TEXT DEFAULT (datetime('now','localtime')) +); + +CREATE INDEX IF NOT EXISTS idx_players_team ON players(team_id); +CREATE INDEX IF NOT EXISTS idx_players_name ON players(name); +CREATE INDEX IF NOT EXISTS idx_games_time ON games(game_time); +CREATE INDEX IF NOT EXISTS idx_games_team ON games(home_team_id); +CREATE INDEX IF NOT EXISTS idx_news_time ON news(publish_time); +CREATE INDEX IF NOT EXISTS idx_persons_role ON persons(role); +""" + +_lock = threading.Lock() + + +def get_conn(): + conn = sqlite3.connect(DB_PATH, timeout=30) + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA journal_mode=WAL") + conn.execute("PRAGMA foreign_keys=ON") + return conn + + +def init_db(): + with _lock: + conn = get_conn() + try: + conn.executescript(_SCHEMA) + conn.commit() + finally: + conn.close() + + +def query(sql, args=()): + conn = get_conn() + try: + cur = conn.execute(sql, args) + return [dict(r) for r in cur.fetchall()] + finally: + conn.close() + + +def query_one(sql, args=()): + rows = query(sql, args) + return rows[0] if rows else None + + +def execute(sql, args=()): + conn = get_conn() + try: + cur = conn.execute(sql, args) + conn.commit() + return cur.lastrowid + finally: + conn.close() + + +def executemany(sql, rows): + conn = get_conn() + try: + conn.executemany(sql, rows) + conn.commit() + finally: + conn.close() + + +def table_count(table): + return query_one(f"SELECT COUNT(*) AS c FROM {table}")["c"] + + +def fuzzy(expr): # SQL LIKE 模糊匹配(转义 % _) + return expr.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") diff --git a/llm.py b/llm.py new file mode 100644 index 0000000..88fb0bb --- /dev/null +++ b/llm.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +"""DeepSeek 大模型调用层(OpenAI 兼容 /chat/completions,支持 function calling)""" +import json +import logging +import time + +import requests + +from config import LLM_BASE_URL, LLM_API_KEY, LLM_MODEL, LLM_TIMEOUT, LLM_MAX_TOKENS, LLM_TEMPERATURE + +log = logging.getLogger("llm") + +URL = f"{LLM_BASE_URL}/chat/completions" +HEADERS = {"Authorization": f"Bearer {LLM_API_KEY}", "Content-Type": "application/json"} + + +def _post(payload, timeout=LLM_TIMEOUT): + resp = requests.post(URL, headers=HEADERS, json=payload, timeout=timeout) + resp.raise_for_status() + return resp.json() + + +def chat(messages, tools=None, temperature=LLM_TEMPERATURE, max_tokens=LLM_MAX_TOKENS): + """基础对话。返回完整 OpenAI 响应 dict。 + tools 为 None 时不带工具;带工具时模型可能返回 tool_calls。""" + payload = { + "model": LLM_MODEL, + "messages": messages, + "temperature": temperature, + "max_tokens": max_tokens, + "stream": False, + } + if tools: + payload["tools"] = tools + payload["tool_choice"] = "auto" + for attempt in range(2): + try: + return _post(payload) + except requests.exceptions.HTTPError as e: + if e.response is not None and e.response.status_code in (429, 500, 502, 503) and attempt == 0: + time.sleep(2) + continue + raise + raise RuntimeError("LLM 请求失败") + + +def parse_content(resp): + """提取 assistant 文本内容""" + try: + return resp["choices"][0]["message"].get("content") or "" + except Exception: + return "" + + +def extract_tool_calls(resp): + """提取 [{name, arguments(dict), id}]""" + calls = [] + try: + msg = resp["choices"][0]["message"] + for tc in msg.get("tool_calls") or []: + try: + args = json.loads(tc["function"].get("arguments") or "{}") + except json.JSONDecodeError: + args = {} + calls.append({"name": tc["function"]["name"], "arguments": args, "id": tc["id"]}) + except Exception: + pass + return calls + + +def extract_reasoning(resp): + """提取思考模式下的 reasoning_content(回传时必须带上)""" + try: + return resp["choices"][0]["message"].get("reasoning_content") or "" + except Exception: + return "" + + +def count_tokens(messages): + """粗略估算 token 数(中英混合,1字≈1token)""" + total = 0 + for m in messages: + total += len(m.get("content") or "") // 2 + 8 + return total diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a0d407c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +flask>=3.0 +requests>=2.31 diff --git a/seed.py b/seed.py new file mode 100644 index 0000000..5d6afce --- /dev/null +++ b/seed.py @@ -0,0 +1,164 @@ +# -*- coding: utf-8 -*- +""" +种子数据执行入口:初始化 SQLite 表结构 → 灌入模拟数据 → 构建 Chroma 向量索引。 +幂等设计:重复执行会自动清空重灌,方便数据更新后一键重建。 +用法:python3 seed.py [--rebuild-vector] (--rebuild-vector 强制重建向量集合) +""" +import argparse +import logging +import sys +import time + +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") +log = logging.getLogger("seed") + +from db import init_db, execute, executemany, query_one, table_count # noqa: E402 +from seed_teams import SPORTS, LEAGUES, TEAMS, STANDINGS # noqa: E402 +from seed_players_a import PLAYERS_A # noqa: E402 +from seed_players_b import PLAYERS_B # noqa: E402 +from seed_games import GAMES, BOX # noqa: E402 +from seed_news import NEWS, WIKI # noqa: E402 +from seed_persons import PERSONS # noqa: E402 +import vector_store # noqa: E402 + + +def wipe(): + for t in ("game_player_stats", "games", "standings", "players", "teams", + "leagues", "sports", "news", "persons"): + execute(f"DELETE FROM {t}") + execute(f"DELETE FROM sqlite_sequence WHERE name='{t}'") + log.info("已清空旧数据") + + +def seed_core(): + """sports / leagues / teams / players / standings / games / persons""" + for code, name, name_en in SPORTS: + execute("INSERT INTO sports(code,name,name_en) VALUES(?,?,?)", (code, name, name_en)) + sport_id = query_one("SELECT id FROM sports WHERE code='basketball'")["id"] + for code, name, name_en, country, season in LEAGUES: + execute("INSERT INTO leagues(sport_id,code,name,name_en,country,season) VALUES(?,?,?,?,?,?)", + (sport_id, code, name, name_en, country, season)) + league_id = query_one("SELECT id FROM leagues WHERE code='NBA'")["id"] + + # 球队 + for code, name, en, city, arena, founded, champs, coach, intro in TEAMS: + execute("""INSERT INTO teams(league_id,code,name,name_en,city,arena,founded,champion_count,head_coach,intro) + VALUES(?,?,?,?,?,?,?,?,?,?)""", + (league_id, code, name, en, city, arena, founded, champs, coach, intro)) + team_id = {r["code"]: r["id"] for r in __import__("db").query("SELECT id,code FROM teams")} + + # 球员 + p_rows = [] + for p in PLAYERS_A + PLAYERS_B: + (t, name, en, pos, num, h, w, country, dy, dp, sal, + pts, reb, ast, stl, blk, mn, cpts, creb, cast, cg, awards, bio) = p + p_rows.append((team_id[t], name, en, pos, num, h, w, country, dy, dp, sal, + pts, reb, ast, stl, blk, mn, cpts, creb, cast, cg, awards, bio)) + executemany("""INSERT INTO players(team_id,name,name_en,position,number,height_cm,weight_kg,country, + draft_year,draft_pick,salary_m,season_pts,season_reb,season_ast,season_stl,season_blk,season_min, + career_pts,career_reb,career_ast,career_games,awards,bio) + VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", p_rows) + log.info("球员 %d 名", len(p_rows)) + + # 排名 + s_rows = [(league_id, "2025-26", team_id[c], w, l, round(w / (w + l), 4), conf, rk) + for c, conf, rk, w, l in STANDINGS] + executemany("""INSERT INTO standings(league_id,season,team_id,wins,losses,win_pct,conference,rank) + VALUES(?,?,?,?,?,?,?,?)""", s_rows) + + # 比赛 + game_id = {} + g_rows = [] + for rnd, gt, status, home, away, hs, as_, venue, bcast in GAMES: + g_rows.append((league_id, "2025-26", rnd, gt, status, team_id[home], team_id[away], + hs, as_, venue, bcast)) + executemany("""INSERT INTO games(league_id,season,round_name,game_time,status,home_team_id,away_team_id, + home_score,away_score,venue,broadcast) VALUES(?,?,?,?,?,?,?,?,?,?,?)""", g_rows) + for i, (rnd, gt, *_rest) in enumerate(GAMES): + game_id[(rnd, gt)] = query_one( + "SELECT id FROM games WHERE round_name=? AND game_time=?", (rnd, gt))["id"] + + # 球员技术统计(box score) + box_rows = [] + for (rnd, gt), lines in BOX.items(): + gid = game_id.get((rnd, gt)) + if not gid: + continue + for pname, pts, reb, ast, stl, blk, mn in lines: + player = query_one("SELECT id,team_id FROM players WHERE name=?", (pname,)) + if player: + box_rows.append((gid, player["id"], player["team_id"], pts, reb, ast, stl, blk, mn)) + executemany("""INSERT INTO game_player_stats(game_id,player_id,team_id,points,rebounds,assists,steals,blocks,minutes) + VALUES(?,?,?,?,?,?,?,?,?)""", box_rows) + log.info("比赛 %d 场,技术统计 %d 条", len(GAMES), len(box_rows)) + + # 人物 + per_rows = [] + for t, name, en, role, role_cn, title, bio, ach in PERSONS: + per_rows.append((sport_id, league_id, team_id.get(t), name, en, role, role_cn, title, bio, ach)) + executemany("""INSERT INTO persons(sport_id,league_id,team_id,name,name_en,role,role_cn,title,bio,achievements) + VALUES(?,?,?,?,?,?,?,?,?,?)""", per_rows) + log.info("人物 %d 名", len(per_rows)) + + # 新闻 / 百科 + n_rows = [] + for t, title, content, author, source, pt, tags, kind in NEWS + WIKI: + n_rows.append((sport_id, league_id, team_id.get(t), title, content, author, source, pt, tags, kind)) + executemany("""INSERT INTO news(sport_id,league_id,team_id,title,content,author,source,publish_time,tags,kind) + VALUES(?,?,?,?,?,?,?,?,?,?)""", n_rows) + log.info("新闻/百科 %d 篇", len(n_rows)) + + +def seed_vector(rebuild=False): + """把新闻/百科分块后写入 Chroma(title + 正文 按 400 字切块,重叠 60 字)""" + from db import query as dbq + rows = dbq("SELECT id,title,content,kind,team_id,publish_time,tags FROM news ORDER BY id") + ids, docs, metas = [], [], [] + for r in rows: + text = f"{r['title']}\n{r['content']}" + chunk_size, overlap = 400, 60 + start = 0 + while start < len(text): + chunk = text[start:start + chunk_size] + ids.append(f"news_{r['id']}_{start}") + docs.append(chunk) + metas.append({"news_id": r["id"], "title": r["title"], "kind": r["kind"], + "team_id": r["team_id"] or 0, "publish_time": r["publish_time"], + "tags": r["tags"] or ""}) + start += chunk_size - overlap + log.info("向量文档 %d 块(%d 篇)", len(ids), len(rows)) + if rebuild: + vector_store.reset_collection() + # 若已有数据则跳过(增量) + if vector_store.collection_count() >= len(ids): + log.info("向量库已有 %d 条,跳过写入", vector_store.collection_count()) + return + if vector_store.collection_count() > 0: + vector_store.reset_collection() + for i in range(0, len(ids), 50): # 分批写入,避免单次请求过大 + vector_store.add_documents(ids[i:i + 50], docs[i:i + 50], metas[i:i + 50]) + log.info("向量索引完成,共 %d 条", vector_store.collection_count()) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--rebuild-vector", action="store_true", help="强制重建向量集合") + ap.add_argument("--no-vector", action="store_true", help="跳过向量索引") + args = ap.parse_args() + + t0 = time.time() + init_db() + wipe() + seed_core() + if not args.no_vector: + seed_vector(rebuild=args.rebuild_vector) + else: + log.info("跳过向量索引") + log.info("数据灌入完成,耗时 %.1fs", time.time() - t0) + for t in ("sports", "leagues", "teams", "players", "games", "standings", + "game_player_stats", "news", "persons"): + log.info(" %-16s %d", t, table_count(t)) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/seed_games.py b/seed_games.py new file mode 100644 index 0000000..add1710 --- /dev/null +++ b/seed_games.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- +"""比赛种子数据(2025-26 赛季,模拟)+ 重点场次球员技术统计 +GAMES: (轮次, 开赛时间, 状态, 主队, 客队, 主队得分, 客队得分, 球馆, 转播) +BOX : {(轮次, 开赛时间): [(球员中文名, 得分, 篮板, 助攻, 抢断, 盖帽, 分钟), ...]} +""" + +GAMES = [ + # ---------------- 常规赛(2025-11 ~ 2026-04) + ("常规赛", "2025-11-04 09:30", "finished", "LAL", "GSW", 117, 108, "加密网球馆", "腾讯体育"), + ("常规赛", "2025-11-09 08:00", "finished", "BOS", "NYK", 121, 114, "TD花园球馆", "腾讯体育"), + ("常规赛", "2025-11-18 09:00", "finished", "OKC", "LAC", 128, 115, "佩科姆中心", "腾讯体育"), + ("常规赛", "2025-11-25 09:30", "finished", "GSW", "PHX", 119, 112, "大通中心", "央视体育"), + ("常规赛", "2025-12-02 08:30", "finished", "BOS", "CLE", 115, 110, "TD花园球馆", "腾讯体育"), + ("常规赛", "2025-12-08 09:00", "finished", "DEN", "LAL", 124, 118, "波尔球馆", "腾讯体育"), + ("常规赛", "2025-12-15 09:30", "finished", "DAL", "LAC", 121, 116, "美航中心", "腾讯体育"), + ("常规赛", "2025-12-22 08:00", "finished", "MIL", "CHI", 118, 105, "费哲论坛球馆", "腾讯体育"), + ("常规赛", "2025-12-28 09:00", "finished", "HOU", "MEM", 114, 108, "丰田中心", "腾讯体育"), + ("常规赛", "2026-01-03 09:30", "finished", "GSW", "LAL", 116, 113, "大通中心", "央视体育"), + ("常规赛", "2026-01-10 08:00", "finished", "CLE", "IND", 122, 117, "火箭按揭球馆", "腾讯体育"), + ("常规赛", "2026-01-18 09:00", "finished", "OKC", "SAS", 132, 108, "佩科姆中心", "腾讯体育"), + ("常规赛", "2026-01-25 09:30", "finished", "LAL", "GSW", 112, 108, "加密网球馆", "央视体育"), + ("常规赛", "2026-01-31 08:30", "finished", "NYK", "MIA", 118, 109, "麦迪逊广场花园", "腾讯体育"), + ("常规赛", "2026-02-05 09:00", "finished", "DEN", "MIN", 121, 112, "波尔球馆", "腾讯体育"), + ("常规赛", "2026-02-12 08:30", "finished", "BOS", "NYK", 125, 118, "TD花园球馆", "腾讯体育"), + ("常规赛", "2026-02-18 09:00", "finished", "PHX", "DAL", 117, 113, "足迹中心", "腾讯体育"), + ("常规赛", "2026-02-25 08:00", "finished", "MIL", "DET", 124, 110, "费哲论坛球馆", "腾讯体育"), + ("常规赛", "2026-03-04 09:30", "finished", "LAC", "SAC", 115, 108, "直觉巨蛋", "腾讯体育"), + ("常规赛", "2026-03-08 09:00", "finished", "OKC", "DEN", 131, 120, "佩科姆中心", "央视体育"), + ("常规赛", "2026-03-14 08:30", "finished", "BOS", "PHI", 119, 108, "TD花园球馆", "腾讯体育"), + ("常规赛", "2026-03-20 09:00", "finished", "HOU", "SAS", 126, 112, "丰田中心", "腾讯体育"), + ("常规赛", "2026-03-26 08:00", "finished", "CLE", "MIA", 118, 104, "火箭按揭球馆", "腾讯体育"), + ("常规赛", "2026-04-02 09:30", "finished", "GSW", "MEM", 121, 115, "大通中心", "腾讯体育"), + ("常规赛", "2026-04-05 09:30", "finished", "DAL", "GSW", 118, 115, "美航中心", "央视体育"), + ("常规赛", "2026-04-10 08:00", "finished", "NYK", "BKN", 112, 101, "麦迪逊广场花园", "腾讯体育"), + ("常规赛", "2026-04-12 09:00", "finished", "OKC", "LAL", 126, 114, "佩科姆中心", "腾讯体育"), + ("常规赛", "2026-04-14 08:00", "finished", "BOS", "TOR", 128, 96, "TD花园球馆", "腾讯体育"), + # ---------------- 季后赛 + ("季后赛首轮", "2026-04-19 09:00", "finished", "OKC", "MIN", 118, 101, "佩科姆中心", "腾讯体育"), + ("季后赛首轮", "2026-04-22 08:30", "finished", "BOS", "PHI", 121, 112, "TD花园球馆", "腾讯体育"), + ("季后赛首轮", "2026-04-25 09:30", "finished", "GSW", "DAL", 114, 108, "大通中心", "腾讯体育"), + ("季后赛首轮", "2026-04-28 09:00", "finished", "LAL", "MEM", 119, 110, "加密网球馆", "腾讯体育"), + ("季后赛首轮", "2026-05-03 09:30", "finished", "GSW", "DAL", 119, 114, "大通中心", "央视体育"), + ("季后赛次轮", "2026-05-06 08:30", "finished", "BOS", "MIL", 117, 112, "TD花园球馆", "腾讯体育"), + ("季后赛次轮", "2026-05-07 09:00", "finished", "OKC", "GSW", 121, 115, "佩科姆中心", "腾讯体育"), + ("季后赛次轮", "2026-05-16 09:30", "finished", "DEN", "LAL", 121, 116, "波尔球馆", "央视体育"), + ("季后赛次轮", "2026-05-17 08:30", "finished", "CLE", "NYK", 112, 104, "火箭按揭球馆", "腾讯体育"), + ("季后赛次轮", "2026-05-18 09:00", "finished", "OKC", "GSW", 119, 110, "佩科姆中心", "腾讯体育"), + ("东部决赛", "2026-05-21 08:30", "finished", "BOS", "CLE", 118, 112, "TD花园球馆", "腾讯体育"), + ("东部决赛", "2026-05-30 08:30", "finished", "BOS", "CLE", 108, 101, "TD花园球馆", "央视体育"), + ("西部决赛", "2026-05-22 09:00", "finished", "OKC", "DEN", 124, 118, "佩科姆中心", "腾讯体育"), + ("西部决赛", "2026-05-31 09:00", "finished", "OKC", "DEN", 117, 110, "佩科姆中心", "央视体育"), + # ---------------- 总决赛(雷霆 4-2 凯尔特人) + ("总决赛", "2026-06-05 09:00", "finished", "OKC", "BOS", 118, 112, "佩科姆中心", "央视体育"), + ("总决赛", "2026-06-07 08:30", "finished", "OKC", "BOS", 124, 119, "佩科姆中心", "腾讯体育"), + ("总决赛", "2026-06-10 08:30", "finished", "BOS", "OKC", 115, 108, "TD花园球馆", "腾讯体育"), + ("总决赛", "2026-06-12 09:00", "finished", "BOS", "OKC", 110, 104, "TD花园球馆", "央视体育"), + ("总决赛", "2026-06-15 09:00", "finished", "OKC", "BOS", 121, 105, "佩科姆中心", "腾讯体育"), + ("总决赛", "2026-06-17 09:00", "finished", "OKC", "BOS", 116, 109, "佩科姆中心", "央视体育"), + # ---------------- 夏季联赛 / 季前赛 / 新赛季揭幕 + ("夏季联赛", "2026-07-08 10:00", "finished", "BKN", "SAS", 108, 102, "托马斯马克中心", "NBA TV"), + ("夏季联赛", "2026-07-14 10:00", "finished", "GSW", "LAL", 104, 98, "托马斯马克中心", "NBA TV"), + ("夏季联赛", "2026-07-18 09:00", "scheduled", "BKN", "SAS", None, None, "托马斯马克中心", "NBA TV"), + ("季前赛", "2026-10-08 19:30", "scheduled", "LAL", "GSW", None, None, "北京五棵松体育馆", "央视体育"), + ("季前赛", "2026-10-11 19:30", "scheduled", "GSW", "LAL", None, None, "上海东方体育中心", "央视体育"), + ("常规赛", "2026-10-20 09:30", "scheduled", "OKC", "BOS", None, None, "佩科姆中心", "腾讯体育"), +] + +# 重点场次球员技术统计(球员名, 得分, 篮板, 助攻, 抢断, 盖帽, 分钟) +BOX = { + ("常规赛", "2026-01-25 09:30"): [ # 湖人 112-108 勇士 + ("勒布朗·詹姆斯", 28, 9, 12, 1, 1, 37), ("安东尼·戴维斯", 32, 14, 3, 1, 4, 36), + ("奥斯汀·里夫斯", 18, 4, 5, 2, 0, 33), ("八村塁", 12, 6, 1, 0, 1, 27), + ("斯蒂芬·库里", 35, 6, 7, 1, 0, 36), ("吉米·巴特勒", 20, 6, 5, 2, 1, 35), + ("乔纳森·库明加", 16, 5, 2, 1, 1, 28), ("德雷蒙德·格林", 10, 8, 7, 2, 1, 32), + ], + ("常规赛", "2026-02-12 08:30"): [ # 凯尔特人 125-118 尼克斯 + ("杰森·塔图姆", 36, 8, 6, 1, 1, 38), ("杰伦·布朗", 27, 6, 3, 2, 0, 35), + ("德里克·怀特", 19, 3, 6, 1, 1, 33), ("克里斯塔普斯·波尔津吉斯", 15, 7, 2, 0, 2, 26), + ("杰伦·布伦森", 32, 3, 9, 1, 0, 37), ("卡尔-安东尼·唐斯", 26, 12, 3, 0, 1, 35), + ("OG·阿努诺比", 18, 5, 2, 2, 1, 34), ("约什·哈特", 12, 9, 5, 1, 0, 36), + ], + ("常规赛", "2026-03-08 09:00"): [ # 雷霆 131-120 掘金 + ("谢伊·吉尔杰斯-亚历山大", 45, 4, 6, 2, 0, 37), ("杰伦·威廉姆斯", 25, 5, 4, 2, 1, 34), + ("切特·霍姆格伦", 19, 11, 2, 0, 3, 32), ("吕冈茨·多尔特", 14, 4, 2, 1, 0, 29), + ("尼古拉·约基奇", 30, 15, 10, 1, 1, 38), ("贾马尔·穆雷", 26, 4, 7, 1, 0, 35), + ("阿隆·戈登", 17, 7, 3, 1, 1, 33), ("小迈克尔·波特", 15, 6, 1, 0, 1, 30), + ], + ("常规赛", "2026-04-05 09:30"): [ # 独行侠 118-115 勇士 + ("卢卡·东契奇", 40, 11, 10, 2, 1, 40), ("凯里·欧文", 24, 4, 6, 1, 0, 36), + ("克莱·汤普森", 15, 3, 2, 1, 0, 28), ("德雷克·莱夫利二世", 12, 10, 3, 0, 2, 30), + ("斯蒂芬·库里", 33, 5, 8, 2, 0, 38), ("吉米·巴特勒", 22, 7, 6, 1, 1, 37), + ("乔纳森·库明加", 18, 6, 3, 1, 1, 30), ("巴迪·希尔德", 14, 3, 2, 0, 0, 27), + ], + ("季后赛次轮", "2026-05-16 09:30"): [ # 掘金 121-116 湖人 G7 + ("尼古拉·约基奇", 34, 16, 9, 1, 2, 41), ("贾马尔·穆雷", 28, 4, 8, 1, 0, 39), + ("阿隆·戈登", 19, 8, 4, 2, 1, 36), ("小迈克尔·波特", 16, 7, 2, 0, 1, 34), + ("勒布朗·詹姆斯", 31, 10, 9, 1, 1, 42), ("安东尼·戴维斯", 27, 14, 3, 1, 4, 40), + ("奥斯汀·里夫斯", 16, 5, 6, 1, 0, 36), ("八村塁", 12, 6, 2, 0, 0, 30), + ], + ("总决赛", "2026-06-05 09:00"): [ # G1 雷霆 118-112 凯尔特人 + ("谢伊·吉尔杰斯-亚历山大", 38, 6, 9, 2, 0, 39), ("杰伦·威廉姆斯", 24, 5, 4, 2, 1, 36), + ("切特·霍姆格伦", 22, 12, 2, 0, 3, 34), ("以赛亚·哈滕施泰因", 8, 10, 3, 1, 1, 26), + ("吕冈茨·多尔特", 12, 4, 2, 1, 0, 30), + ("杰森·塔图姆", 31, 8, 5, 1, 1, 40), ("杰伦·布朗", 27, 6, 3, 2, 0, 38), + ("德里克·怀特", 16, 3, 5, 1, 1, 35), ("朱·霍勒迪", 12, 4, 4, 2, 0, 33), + ("克里斯塔普斯·波尔津吉斯", 14, 8, 1, 0, 2, 28), + ], + ("总决赛", "2026-06-07 08:30"): [ # G2 雷霆 124-119 凯尔特人(加时) + ("谢伊·吉尔杰斯-亚历山大", 42, 7, 11, 3, 0, 45), ("杰伦·威廉姆斯", 26, 6, 5, 1, 1, 41), + ("切特·霍姆格伦", 18, 10, 3, 1, 3, 39), ("亚历克斯·卡鲁索", 9, 3, 4, 3, 0, 28), + ("杰森·塔图姆", 35, 9, 6, 1, 1, 44), ("杰伦·布朗", 28, 7, 4, 2, 0, 42), + ("德里克·怀特", 18, 4, 6, 2, 1, 40), ("克里斯塔普斯·波尔津吉斯", 16, 9, 2, 0, 2, 33), + ("艾尔·霍福德", 10, 8, 3, 1, 1, 31), + ], + ("总决赛", "2026-06-10 08:30"): [ # G3 凯尔特人 115-108 雷霆 + ("杰森·塔图姆", 38, 10, 4, 1, 1, 41), ("杰伦·布朗", 24, 6, 5, 2, 1, 38), + ("德里克·怀特", 20, 4, 5, 1, 0, 36), ("朱·霍勒迪", 14, 5, 3, 2, 0, 34), + ("谢伊·吉尔杰斯-亚历山大", 33, 5, 8, 2, 0, 40), ("杰伦·威廉姆斯", 22, 5, 4, 1, 1, 37), + ("切特·霍姆格伦", 15, 11, 2, 0, 2, 35), ("吕冈茨·多尔特", 12, 4, 1, 1, 0, 31), + ], + ("总决赛", "2026-06-12 09:00"): [ # G4 凯尔特人 110-104 雷霆 + ("杰森·塔图姆", 30, 8, 7, 2, 1, 40), ("杰伦·布朗", 26, 6, 4, 1, 0, 38), + ("克里斯塔普斯·波尔津吉斯", 18, 10, 2, 0, 2, 32), ("德里克·怀特", 12, 3, 6, 1, 1, 35), + ("谢伊·吉尔杰斯-亚历山大", 36, 6, 6, 2, 0, 41), ("杰伦·威廉姆斯", 19, 6, 5, 1, 0, 37), + ("吕冈茨·多尔特", 14, 4, 2, 1, 0, 32), ("切特·霍姆格伦", 12, 9, 3, 0, 3, 34), + ], + ("总决赛", "2026-06-15 09:00"): [ # G5 雷霆 121-105 凯尔特人 + ("谢伊·吉尔杰斯-亚历山大", 39, 8, 10, 2, 0, 39), ("杰伦·威廉姆斯", 28, 5, 6, 2, 1, 37), + ("切特·霍姆格伦", 20, 12, 2, 0, 4, 35), ("以赛亚·哈滕施泰因", 12, 12, 3, 0, 1, 28), + ("杰森·塔图姆", 28, 7, 4, 1, 0, 38), ("杰伦·布朗", 22, 6, 3, 1, 0, 36), + ("德里克·怀特", 13, 3, 5, 1, 0, 33), ("朱·霍勒迪", 9, 4, 3, 1, 0, 30), + ], + ("总决赛", "2026-06-17 09:00"): [ # G6 雷霆 116-109 凯尔特人(SGA 41分 FMVP) + ("谢伊·吉尔杰斯-亚历山大", 41, 7, 8, 2, 1, 42), ("杰伦·威廉姆斯", 23, 5, 5, 1, 0, 38), + ("切特·霍姆格伦", 17, 10, 2, 1, 3, 36), ("吕冈茨·多尔特", 15, 5, 2, 2, 0, 33), + ("以赛亚·哈滕施泰因", 9, 11, 4, 1, 1, 29), + ("杰森·塔图姆", 34, 9, 5, 1, 1, 42), ("杰伦·布朗", 25, 7, 4, 2, 0, 39), + ("德里克·怀特", 15, 3, 6, 1, 0, 36), ("克里斯塔普斯·波尔津吉斯", 12, 8, 2, 0, 2, 30), + ("朱·霍勒迪", 8, 4, 4, 2, 0, 32), + ], +} diff --git a/seed_news.py b/seed_news.py new file mode 100644 index 0000000..6fa8bc9 --- /dev/null +++ b/seed_news.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +"""新闻 / 百科词条 种子数据(模拟 2026 年休赛期背景) +(队code或None, 标题, 正文, 作者, 来源, 发布时间, 标签, kind: news/wiki) +""" + +NEWS = [ + # ---------------- 总决赛与冠军 + ("OKC", "雷霆4-2击败凯尔特人 时隔多年再夺总冠军", + "北京时间6月17日,2025-26赛季NBA总决赛第六场在俄克拉荷马城打响。雷霆主场116-109力克凯尔特人,以大比分4-2夺得队史第二座总冠军奖杯。谢伊·吉尔杰斯-亚历山大全场砍下41分7篮板8助攻,荣膺总决赛MVP。这是雷霆继2012年后再次登上联盟之巅,亚历山大在夺冠后表示:'这是我们团队努力的最好回报,俄克拉荷马值得这一切。'", + "NBA中文官网", "NBA官网", "2026-06-17 11:30", "雷霆,总决赛,冠军", "news"), + ("OKC", "SGA总决赛场均35.2分 比肩历史传奇", + "总决赛六场比赛,亚历山大场均贡献35.2分6.4篮板8.1助攻,投篮命中率52.3%,成为近20年来首位在总决赛场均砍下35+的球员。他在第六场的关键时刻连续命中中距离,彻底浇灭了凯尔特人反扑的希望。美媒评价:'SGA正在用最古典的方式,统治最现代的联盟。'", + "体坛周报", "体坛周报", "2026-06-18 09:00", "SGA,总决赛,数据", "news"), + ("BOS", "塔图姆赛后发声:下赛季我们会卷土重来", + "总决赛失利后,凯尔特人当家球星杰森·塔图姆在更衣室接受采访时表示:'连续两年打进总决赛(注:模拟剧情),我们证明了自己是联盟最好的球队之一。输掉总决赛很难受,但我们会带着教训回来。'塔图姆本次总决赛场均30.4分8.7篮板5.2助攻,表现无可指摘。", + "腾讯体育", "腾讯体育", "2026-06-18 15:00", "凯尔特人,塔图姆,总决赛", "news"), + ("BOS", "凯尔特人休赛期计划曝光:优先续约怀特 补强内线替补", + "据ESPN报道,凯尔特人管理层休赛期的首要任务是续约自由球员德里克·怀特,同时为波尔津吉斯寻找一名可靠的替补中锋。总经理表示球队不会拆散核心阵容,'我们距离总冠军只差一点点细节'。", + "ESPN", "ESPN中文", "2026-06-22 10:00", "凯尔特人,续约,休赛期", "news"), + # ---------------- 选秀 + ("BKN", "2026年NBA选秀大会:篮网状元签选中卡梅隆·布泽尔", + "6月24日,2026年NBA选秀大会在布鲁克林巴克莱中心举行。篮网用状元签选中了杜克大学前锋卡梅隆·布泽尔,这位星二代在夏季联赛首秀中砍下28分12篮板,展现出不俗的即战力。布泽尔表示:'能在家乡球队开启职业生涯,是梦想成真。'", + "NBA中文官网", "NBA官网", "2026-06-24 12:00", "选秀,篮网,布泽尔", "news"), + ("BKN", "夏季联赛首秀:布泽尔28+12 状元即战力拉满", + "北京时间7月8日,NBA夏季联赛拉开大幕,篮网108-102击败马刺。状元秀卡梅隆·布泽尔首秀出战30分钟,砍下28分12篮板3助攻,正负值全场最高。他的中距离和策应能力让教练组赞不绝口,'他比我们预期的还要成熟。'", + "腾讯体育", "腾讯体育", "2026-07-08 12:30", "夏季联赛,布泽尔,新秀", "news"), + # ---------------- 自由市场 / 交易 + ("LAL", "勒布朗·詹姆斯与湖人续约2年1.2亿美元", + "7月1日自由市场开启首日,41岁的勒布朗·詹姆斯与湖人达成2年1.2亿美元的续约合同,最后一年为球员选项。这是詹姆斯生涯第23个赛季,他在上赛季依旧场均25.8分7.8篮板8.4助攻。湖人总经理佩林卡表示:'勒布朗就是湖人的图腾,我们很荣幸他能终老这里。'", + "The Athletic", "The Athletic", "2026-07-01 09:00", "詹姆斯,湖人,续约", "news"), + ("GSW", "勇士与爵士达成交易:库明加+首轮签换来马尔卡宁", + "据Shams报道,勇士送出乔纳森·库明加和2028年首轮签,从爵士得到全明星前锋劳里·马尔卡宁。勇士管理层希望为库里和巴特勒再配一名高炮台内线,冲击生涯第五冠。马尔卡宁上赛季场均22.8分8.4篮板,三分命中率39.8%。", + "The Athletic", "Shams报道", "2026-07-05 10:30", "勇士,交易,马尔卡宁", "news"), + ("SAS", "波波维奇宣布退休 执教马刺29年成就传奇", + "7月12日,76岁的格雷格·波波维奇召开新闻发布会,正式宣布退休,结束29年的马刺执教生涯。他执教期间带队5夺总冠军,常规赛胜场数历史第一。马刺随后宣布由助教米奇·约翰逊接任主帅。波波维奇在发布会上说:'篮球给了我一切,是时候把时间留给家人了。'", + "央视体育", "央视体育", "2026-07-12 10:00", "马刺,波波维奇,退役", "news"), + ("PHX", "杜兰特与太阳达成2年1.1亿续约 38岁老兵再战两年", + "太阳官方宣布与凯文·杜兰特达成2年1.1亿美元的续约合同。38岁的杜兰特上赛季场均仍能贡献27.4分6.8篮板5.2助攻,投篮命中率51.8%。'我还能打,我还想赢。'杜兰特在签约发布会上说。", + "ESPN", "ESPN中文", "2026-07-03 09:00", "杜兰特,太阳,续约", "news"), + ("DAL", "东契奇晒训练照 新赛季目标:减重+带队夺冠", + "独行侠球星卢卡·东契奇休赛期在斯洛文尼亚进行特训,社交媒体晒出训练照并配文:'第9个赛季,目标只有一个。'球队体能团队透露东契奇休赛期已减重8公斤,上赛季他场均30.4分8.6篮板8.8助攻,连续6年入选全明星。", + "腾讯体育", "腾讯体育", "2026-07-20 16:00", "东契奇,独行侠,训练", "news"), + ("CHN", "崔永熙结束NBA之旅 正式回归CBA联赛", + "中国球员崔永熙在结束NBA两年征程后,正式与CBA广东宏远队签约。他在NBA期间代表篮网出场27次,场均4.2分1.8篮板。崔永熙表示:'感谢NBA的经历,回到CBA我会继续努力,争取为国家队出战。'", + "新华社", "新华社", "2026-07-15 11:00", "崔永熙,CBA,中国球员", "news"), + # ---------------- 赛季奖项 + ("DEN", "约基奇当选2025-26赛季常规赛MVP 生涯第四座", + "6月3日,NBA官方宣布掘金中锋尼古拉·约基奇当选2025-26赛季常规赛MVP,这是他职业生涯第4次获此殊荣,追平勒布朗·詹姆斯并列历史第四。约基奇本赛季场均28.8分12.6篮板9.8助攻,率队取得57胜25负。", + "NBA中文官网", "NBA官网", "2026-06-03 09:00", "约基奇,MVP,掘金", "news"), + ("OKC", "最佳阵容出炉:SGA全票一阵 塔图姆约基奇字母哥东契奇在列", + "6月4日,NBA公布2025-26赛季最佳阵容。一阵为:亚历山大、东契奇、塔图姆、字母哥、约基奇。亚历山大全票当选,连续三年入选一阵。二阵包括爱德华兹、布伦森、杜兰特、戴维斯、文班亚马;三阵为库里、米切尔、巴特勒、萨博尼斯、霍姆格伦。", + "腾讯体育", "腾讯体育", "2026-06-04 10:00", "最佳阵容,赛季奖项", "news"), + ("SAS", "文班亚马蝉联最佳防守球员 场均3.8盖帽冠绝联盟", + "文班亚马以场均3.8次盖帽连续第二年当选最佳防守球员,同时入选最佳防守一阵。他在防守端的覆盖面积被媒体称为'外星人级别的存在',马刺本赛季防守效率联盟第三。", + "NBA中文官网", "NBA官网", "2026-06-05 09:30", "文班亚马,DPOY,马刺", "news"), + ("OKC", "杰伦·威廉姆斯荣膺进步最快球员", + "雷霆前锋杰伦·威廉姆斯当选2025-26赛季进步最快球员,他本赛季场均22.4分5.6篮板5.4助攻,较上赛季提升明显,并首次入选全明星。他在季后赛中的表现同样稳定,是雷霆夺冠的重要拼图。", + "体坛周报", "体坛周报", "2026-06-02 09:00", "杰伦·威廉姆斯,进步最快,雷霆", "news"), + ("BOS", "普理查德当选最佳第六人 凯尔特人板凳火力冠绝联盟", + "凯尔特人后卫佩顿·普理查德当选2025-26赛季最佳第六人,他场均贡献16.8分3.9助攻,三分命中率41.2%。凯尔特人替补场均得分联盟第一,普理查德是当之无愧的板凳领袖。", + "NBA中文官网", "NBA官网", "2026-06-01 09:00", "最佳第六人,凯尔特人", "news"), + # ---------------- 全明星与常规赛 + ("GSW", "2026年全明星周末:SGA荣膺全明星MVP", + "2026年全明星周末在旧金山大通中心举行。亚历山大全场砍下42分7篮板8助攻,率队击败对手荣膺全明星MVP。扣篮大赛上,火箭新秀阿门·汤普森的'罚球线起跳360度'技惊四座,夺得扣篮大赛冠军。", + "腾讯体育", "腾讯体育", "2026-02-16 12:00", "全明星,旧金山,SGA", "news"), + ("OKC", "雷霆68胜创队史纪录 提前锁定西部第一", + "4月12日,雷霆主场126-114击败湖人,取得赛季第68胜,打破队史纪录(此前为2012-13赛季的60胜)。亚历山大本赛季场均32.8分领跑联盟得分榜,雷霆也成为联盟历史上第11支单赛季68胜的球队。", + "ESPN", "ESPN中文", "2026-04-12 11:00", "雷霆,68胜,纪录", "news"), + ("LAL", "41岁詹姆斯场均25+8+8 历史最老全明星首发", + "勒布朗·詹姆斯在生涯第23个赛季依旧场均25.8分7.8篮板8.4助攻,成为NBA历史上年龄最大的全明星首发球员。湖人以55胜27负排名西部第三,'不老传说'仍在继续。", + "央视体育", "央视体育", "2026-04-13 09:30", "詹姆斯,湖人,纪录", "news"), + # ---------------- 评论与专栏 + ("OKC", "杨毅专栏:雷霆的崛起不是偶然,是十年布局的胜利", + "从2020年送走乔治和威少开启重建,到2026年登顶,雷霆用六年时间完成了联盟最教科书式的重建。普雷斯蒂囤积了海量选秀权,选中了亚历山大、杰威、切特,再通过交易得到哈滕和卡鲁索补强,每一步都精准无比。这枚总冠军,是对'耐心'二字最好的奖赏。", + "杨毅", "杨毅侃球", "2026-06-19 09:00", "评论,雷霆,重建", "news"), + ("BOS", "苏群:凯尔特人的遗憾与底气", + "凯尔特人连续两年闯入总决赛,阵容深度联盟顶级,但面对雷霆的无限换防,双探花的攻坚效率成了胜负手。好消息是,怀特、霍勒迪的核心框架都还在,下赛季他们依然是东部最大热门。绿军的底气,从来都是他们的体系。", + "苏群", "苏群说球", "2026-06-20 09:00", "评论,凯尔特人,总决赛", "news"), + ("BKN", "徐静雨锐评:布泽尔能成为下一个塔图姆吗?", + "状元秀布泽尔夏季联赛首秀28+12,技术成熟度远超同龄人。他的模板不是传统内线,而是塔图姆这种能持球、能策应、能投三分的现代前锋。篮网的重建,从选中他的那一刻起就正式启动了。", + "徐静雨", "静雨体育", "2026-07-09 09:00", "评论,布泽尔,新秀", "news"), + # ---------------- 中国元素 + ("CHN", "NBA中国赛官宣:10月北京上海两站 湖人vs勇士", + "NBA官方宣布,2026年NBA中国赛将于10月8日和11日分别在北京五棵松体育馆、上海东方体育中心举行,对阵双方为洛杉矶湖人与金州勇士。这是NBA中国赛时隔7年重返中国,詹库对决一票难求,门票开售10分钟即告罄。", + "NBA中文官网", "NBA官网", "2026-07-25 10:00", "中国赛,湖人,勇士", "news"), + ("CHN", "姚明出任NBA中国区大使 推动中美篮球交流", + "7月30日,NBA官方宣布中国篮球传奇姚明出任NBA中国区大使。姚明表示:'希望更多中国孩子爱上篮球,也希望NBA和中国篮球的交流越来越深入。'此前姚明曾长期担任中国篮协主席,推动CBA职业化改革。", + "新华社", "新华社", "2026-07-30 11:00", "姚明,NBA中国,大使", "news"), + # ---------------- 伤病与球队动态 + ("LAC", "快船官方:伦纳德接受膝盖手术 预计缺席新赛季前半程", + "快船宣布科怀·伦纳德将接受右膝手术,预计缺席2026-27赛季前半程。这是伦纳德近三个赛季第三次接受膝盖手术。快船总经理表示不会催促他复出,'他的健康永远是第一位的'。", + "ESPN", "ESPN中文", "2026-07-18 09:00", "快船,伦纳德,伤病", "news"), + ("PHI", "恩比德恢复训练 76人新赛季展望:健康就是一切", + "乔尔·恩比德休赛期恢复训练状态良好,76人主帅纳斯表示:'乔尔的身体状况是球队最大的变量,如果他健康,我们就是争冠球队。'过去三个赛季恩比德场均出战不足55场,76人的命运系于他的膝盖。", + "The Athletic", "The Athletic", "2026-07-22 09:00", "76人,恩比德,伤病", "news"), + ("HOU", "火箭青年军新赛季展望:申京的进化方向", + "火箭上赛季49胜闯入季后赛首轮,年轻核心阵容保持完整。申京上赛季场均19.4分10.2篮板5.0助攻,被球迷称为'小约基奇'。新赛季他将开发更多三分出手,乌度卡希望他成为现代中锋的完全体。", + "腾讯体育", "腾讯体育", "2026-07-28 09:00", "火箭,申京,新赛季", "news"), + ("SAS", "马刺新时代:米奇·约翰逊接棒 福克斯文班双核启航", + "波波维奇退休后,马刺正式进入米奇·约翰逊时代。球队围绕文班亚马和福克斯建队,休赛期又签下两名3D锋线。文班亚马在训练营中展示了新的背身技术,'我要让对手在内线也不得不包夹我。'", + "体坛周报", "体坛周报", "2026-07-26 09:00", "马刺,文班亚马,新主帅", "news"), + ("MIL", "字母哥与雄鹿续约3年 一人一城故事继续", + "字母哥扬尼斯·阿德托昆博与雄鹿达成3年1.8亿美元续约,合同到2030年。字母哥表示:'密尔沃基就是我的家,我们还有未完成的事业。'雄鹿上赛季52胜,季后赛次轮不敌凯尔特人。", + "ESPN", "ESPN中文", "2026-07-08 09:00", "字母哥,雄鹿,续约", "news"), + ("NYK", "尼克斯3年1.5亿续约布伦森 大苹果城的争冠窗口", + "尼克斯与杰伦·布伦森达成3年1.5亿美元提前续约。布伦森上赛季场均27.8分7.2助攻,率队54胜排名东部第三。加上唐斯和阿努诺比,尼克斯的争冠窗口已经开启。", + "The Athletic", "The Athletic", "2026-07-10 09:00", "尼克斯,布伦森,续约", "news"), + ("DEN", "掘金休赛期操作:留住穆雷 补强侧翼深度", + "掘金与贾马尔·穆雷完成4年1.6亿续约,并签下两名底薪侧翼老将。约基奇即将年满31岁,掘金管理层明确表态:'我们会围绕尼古拉全力争冠,直到他退役。'", + "腾讯体育", "腾讯体育", "2026-07-16 09:00", "掘金,穆雷,续约", "news"), + ("MIA", "热火文化延续:希罗签下5年2.2亿顶薪", + "热火与泰勒·希罗达成5年2.2亿美元顶薪续约,这是队史最大合同。希罗上赛季场均23.6分5.2篮板5.6助攻,荣膺进步最快球员。斯波尔斯特拉表示:'泰勒已经是全明星级别的球员,他配得上这份合同。'", + "ESPN", "ESPN中文", "2026-07-06 09:00", "热火,希罗,顶薪", "news"), + ("CLE", "骑士双塔留队:莫布利阿伦均获续约", + "骑士与埃文·莫布利达成5年2.1亿续约,贾莱特·阿伦则签下4年1.1亿合同。骑士上赛季58胜东部第二,双塔+米切尔加兰的后场组合保持完整,被视为凯尔特人东部最大的对手。", + "The Athletic", "The Athletic", "2026-07-12 09:00", "骑士,莫布利,续约", "news"), + # ---------------- 夏季联赛 / 其他 + ("LAL", "夏季联赛:湖人新秀克内克特三分9中6砍27分", + "夏季联赛湖人98-104不敌勇士,但二年级新秀道尔顿·克内克特表现出色,三分球9投6中砍下27分。湖人教练组表示他新赛季将进入主要轮换。", + "腾讯体育", "腾讯体育", "2026-07-14 12:00", "夏季联赛,湖人,克内克特", "news"), + ("BKN", "篮网官方:布泽尔球衣销量高居新秀第一", + "NBA官方商店数据显示,状元秀卡梅隆·布泽尔的球衣销量在2026年新秀中排名第一,进入联盟总销量前十。布鲁克林的球市正在因为这位状元复苏。", + "NBA中文官网", "NBA官网", "2026-07-19 09:00", "篮网,布泽尔,球衣", "news"), + ("CHN", "曾凡博与步行者签下双向合同 中国球员再闯NBA", + "据记者报道,中国球员曾凡博与步行者签下一份双向合同,将代表步行者出战新赛季。他在夏季联赛中场均12.4分5.2篮板,三分命中率38.9%。若最终留队,他将成为第9位正式登陆NBA的中国球员。", + "新华社", "新华社", "2026-07-27 10:00", "曾凡博,步行者,中国球员", "news"), + ("CHN", "总决赛中国收视创十年新高 篮球热潮持续升温", + "据第三方数据机构统计,2026年总决赛第六场在中国地区的观看人数创下近十年新高,全平台直播观看量突破1.2亿。NBA官方表示将加大在中国的推广投入,10月的中国赛就是第一步。", + "NBA中文官网", "NBA官网", "2026-06-19 09:00", "收视率,中国市场,总决赛", "news"), +] + +WIKI = [ + # (team_code或None, 标题, 正文, 作者, 来源, 时间, 标签, kind) + (None, "NBA联赛简介与历史", + "NBA(National Basketball Association,美国职业篮球联赛)成立于1946年,是北美四大职业体育联盟之一,也是全球水平最高的篮球联赛。联盟现有30支球队,分为东西部各15支,每个赛季包括常规赛(82场)和季后赛。历史上,波士顿凯尔特人和洛杉矶湖人各夺得17次总冠军并列历史第一(注:本系统模拟2026年数据,凯尔特人为18冠)。迈克尔·乔丹、科比·布莱恩特、勒布朗·詹姆斯等巨星先后定义了一个个时代。", + "系统百科", "百科词条", "2026-01-01 00:00", "NBA,历史,百科", "wiki"), + (None, "NBA选秀制度", + "NBA选秀(NBA Draft)是联盟补充新球员的主要方式,每年6月举行。选秀大会共两轮,每轮30个顺位,战绩最差的球队获得最高概率抽中状元签。乐透抽签(Draft Lottery)决定前四顺位归属。被选中的球员需与球队签订新秀合同(首轮秀为保障合同),次轮秀和落选秀则可通过双向合同等方式进入联盟。2026年状元秀为篮网选中的卡梅隆·布泽尔。", + "系统百科", "百科词条", "2026-01-01 00:00", "选秀,规则,百科", "wiki"), + (None, "工资帽与奢侈税", + "工资帽(Salary Cap)是联盟规定的球队薪资总额上限,2025-26赛季约为1.54亿美元。超过工资帽的球队可使用各种特例续约球员,但若薪资超过奢侈税线(约1.88亿美元),需按比例缴纳奢侈税。超级顶薪合同起薪可达工资帽的35%,用于奖励满足条件的自家培养球星。", + "系统百科", "百科词条", "2026-01-01 00:00", "工资帽,奢侈税,规则,百科", "wiki"), + (None, "全明星周末", + "NBA全明星周末(All-Star Weekend)通常在2月举行,包括新秀挑战赛、技巧挑战赛、三分大赛和扣篮大赛,压轴大戏是全明星正赛。全明星首发由球迷、媒体和球员投票选出。2026年全明星周末在旧金山大通中心举行,SGA荣膺全明星MVP。", + "系统百科", "百科词条", "2026-01-01 00:00", "全明星,百科", "wiki"), + (None, "季后赛与总决赛", + "NBA季后赛(Playoffs)由东西部前八名(现为附加赛决出)球队参加,采用7场4胜制,首轮对阵为1-8、2-7、3-6、4-5。东西部冠军会师总决赛,争夺拉里·奥布莱恩奖杯。总决赛MVP(FMVP)颁发给系列赛表现最出色的球员,2026年由雷霆的亚历山大获得。", + "系统百科", "百科词条", "2026-01-01 00:00", "季后赛,总决赛,规则,百科", "wiki"), + (None, "三分球革命", + "三分线于1979-80赛季引入NBA,最初被视为'杂技'。2010年代,斯蒂芬·库里用惊人的三分投射改变了整个联盟的进攻理念,勇士王朝将'空间与速度'推向极致。如今三分出手占比已超过40%,'魔球理论'成为主流。库里以超过4000记三分球高居历史三分榜第一。", + "系统百科", "百科词条", "2026-01-01 00:00", "三分球,库里,百科", "wiki"), + (None, "篮球数据统计入门", + "NBA常用数据包括:得分(PTS)、篮板(REB)、助攻(AST)、抢断(STL)、盖帽(BLK)、失误(TO)、正负值(+/-)等。进阶数据有PER(效率值)、真实命中率(TS%)、使用率(USG%)、胜利贡献值(WS)等。'三双'指单场得分、篮板、助攻(或抢断盖帽)三项达到两位数,约基奇是现役三双王。", + "系统百科", "百科词条", "2026-01-01 00:00", "数据,统计,百科", "wiki"), + (None, "中国球员在NBA", + "自2001年王治郅成为首位登陆NBA的中国球员以来,先后有巴特尔、姚明、易建联、孙悦、周琦、崔永熙等中国球员征战NBA。姚明2002年以状元秀身份加盟火箭,8次入选全明星,2016年入选奈史密斯篮球名人堂,是中美篮球交流的桥梁。2026年,曾凡博与步行者签下双向合同,中国球员的NBA之旅仍在继续。", + "系统百科", "百科词条", "2026-01-01 00:00", "中国球员,姚明,百科", "wiki"), +] diff --git a/seed_persons.py b/seed_persons.py new file mode 100644 index 0000000..857710c --- /dev/null +++ b/seed_persons.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +"""人物种子数据:教练 / 评论员 / 主持人 / 经纪人 / 总经理 / 传奇人物 +(队code或None, 姓名, 英文名, 角色role, 角色中文, 头衔, 简介, 成就) +扩展新角色(如裁判、球探)只需增加 role 枚举值,无需改表结构。 +""" + +PERSONS = [ + # ---------------- 教练 + ("GSW", "史蒂夫·科尔", "Steve Kerr", "coach", "主教练", "金州勇士主教练", + "球员时期5夺总冠军(公牛3冠+马刺2冠),2014年起执教勇士,打造小球时代王朝。以传切体系闻名,是三分革命的践行者。", + "4届总冠军教练(2015/2017/2018/2025),2016年最佳教练,2015/2017全明星主教练"), + ("SAS", "格雷格·波波维奇", "Gregg Popovich", "coach", "主教练", "马刺传奇主帅(已退休)", + "执教马刺29年,NBA历史上执教胜场数第一的教练。以团队篮球和国际化建队著称,培养了邓肯、吉诺比利、帕克等巨星。2026年7月正式退休。", + "5届总冠军教练(1999/2003/2005/2007/2014),3届最佳教练,2016年里约奥运美国男篮主帅"), + ("MIA", "埃里克·斯波尔斯特拉", "Erik Spoelstra", "coach", "主教练", "迈阿密热火主教练", + "帕特·莱利门徒,热火铁血文化的传承者。2008年上任以来从未缺席季后赛窗口,2023年率热火完成黑八奇迹闯入总决赛。", + "2届总冠军教练(2012/2013),2023年东部决赛抢七晋级"), + ("LAL", "JJ·雷迪克", "JJ Redick", "coach", "主教练", "洛杉矶湖人主教练", + "前NBA神射手,退役后转型教练,2024年起执教湖人。以现代篮球理念著称,擅长打造空间型进攻体系,执教首个赛季即率湖人重返西部前三。", + "15年NBA球员生涯(三分命中率41.5%),2026年最佳教练提名"), + ("LAC", "泰伦·卢", "Tyronn Lue", "coach", "主教练", "洛杉矶快船主教练", + "球员时期是湖人三连冠成员,2020年起执教快船。以临场调整和巨星使用闻名,2021年率快船队史首次闯入西部决赛。", + "2016年总冠军教练(骑士),2021年西部决赛,3届总冠军球员(湖人)"), + ("HOU", "伊梅·乌度卡", "Ime Udoka", "coach", "主教练", "休斯顿火箭主教练", + "以防守著称的教练,2021年执教凯尔特人首年即闯入总决赛,2023年接手火箭开启青年军重建,2025年率队重返季后赛。", + "2022年总决赛(凯尔特人),2025年最佳教练提名"), + ("DAL", "杰森·基德", "Jason Kidd", "coach", "主教练", "达拉斯独行侠主教练", + "名人堂控卫,2021年起执教独行侠,2024年率队闯入总决赛。擅长后卫调教与防守体系搭建。", + "2011年总冠军球员(独行侠),2024年总决赛(独行侠),10次全明星"), + ("DEN", "迈克尔·马龙", "Michael Malone", "coach", "主教练", "丹佛掘金主教练", + "2015年起执教掘金,围绕约基奇打造出联盟最流畅的进攻体系,2023年率队夺得队史首冠。", + "2023年总冠军教练,2023年最佳教练提名"), + ("OKC", "马克·戴格诺特", "Mark Daigneault", "coach", "主教练", "俄克拉荷马雷霆主教练", + "2020年接手重建中的雷霆,以培养年轻球员著称,将SGA、杰威、切特打造成冠军班底,2026年率队68胜夺冠。", + "2026年总冠军教练,2024年最佳教练,2026年全明星主教练"), + ("BOS", "乔·马祖拉", "Joe Mazzulla", "coach", "主教练", "波士顿凯尔特人主教练", + "2022年接替乌度卡执教凯尔特人,2024年率队夺得队史第18冠,以数据驱动和临场果断著称。", + "2024年总冠军教练,2025年最佳教练提名"), + ("NYK", "汤姆·锡伯杜", "Tom Thibodeau", "coach", "主教练", "纽约尼克斯主教练", + "防守大师,执教风格以高强度著称。2020年起执教尼克斯,2025年率队54胜重返东部前列。", + "2011年最佳教练(公牛),2021年最佳教练(尼克斯)"), + ("PHX", "迈克·布登霍尔泽", "Mike Budenholzer", "coach", "主教练", "菲尼克斯太阳主教练", + "马刺系教练,2021年率雄鹿夺得总冠军,2025年起执教太阳。", + "2021年总冠军教练(雄鹿),2015/2019年最佳教练"), + ("IND", "里克·卡莱尔", "Rick Carlisle", "coach", "主教练", "印第安纳步行者主教练", + "2011年率独行侠夺冠,2021年起执教步行者,2025年率队闯入东部决赛。", + "2011年总冠军教练(独行侠),2024年最佳教练"), + ("MIN", "克里斯·芬奇", "Chris Finch", "coach", "主教练", "明尼苏达森林狼主教练", + "2021年起执教森林狼,2024年率队闯入西部决赛创造队史最佳战绩。", + "2024年西部决赛,2024年最佳教练提名"), + ("CLE", "肯尼·阿特金森", "Kenny Atkinson", "coach", "主教练", "克利夫兰骑士主教练", + "2024年起执教骑士,将双塔体系与快速攻防融合,率队取得58胜的联盟第二战绩。", + "2025年最佳教练,2026年最佳教练提名"), + # ---------------- 评论员 / 解说 + (None, "杨毅", "Yang Yi", "commentator", "评论员", "著名篮球评论员", + "中国最知名的篮球评论员之一,创办'杨毅侃球',以犀利的观点和深厚的历史知识著称,长期解说NBA与CBA赛事。", + "20余年篮球解说经验,多档篮球节目嘉宾"), + (None, "苏群", "Su Qun", "commentator", "评论员", "资深篮球评论员", + "《篮球先锋报》总编辑,中国最早的NBA驻美记者之一,解说风格专业沉稳,著有《苏群说球》专栏。", + "30年篮球媒体人,首位赴美报道NBA的中国记者之一"), + (None, "王猛", "Wang Meng", "commentator", "评论员", "篮球评论员", + "腾讯体育王牌解说,以数据流和段子手风格著称,主持《王猛说球》等节目。", + "20年NBA解说经验,腾讯NBA头牌解说"), + (None, "张卫平", "Zhang Weiping", "commentator", "评论员", "篮球评论员/前国手", + "前中国男篮国手,退役后成为最经典的NBA解说员之一,'张指导'口头禅深入人心。", + "1978年亚运会冠军,中国篮球名人堂成员"), + (None, "徐静雨", "Xu Jingyu", "commentator", "评论员", "篮球评论员/自媒体人", + "以夸张的直播风格和'锐评'出圈的篮球自媒体人,观点鲜明,争议与流量并存。", + "全网粉丝超千万的篮球解说"), + (None, "柯凡", "Ke Fan", "commentator", "解说员", "篮球解说员", + "腾讯体育NBA解说,声音辨识度高,擅长数据解读。", + "多年NBA赛事解说经验"), + (None, "段冉", "Duan Ran", "commentator", "解说员", "篮球解说员", + "NBA前方记者出身,解说风格专业严谨,多次现场报道总决赛。", + "多次现场报道NBA总决赛"), + # ---------------- 主持人 + (None, "于嘉", "Yu Jia", "host", "主持人", "央视体育主持人", + "央视体育频道主持人,长期主持NBA赛事转播与篮球节目,曾现场解说多届奥运会篮球比赛。", + "央视体育NBA转播主持人"), + (None, "美娜", "Meina", "host", "主持人", "腾讯NBA主持人", + "腾讯体育NBA女主播,主持《NBA最前线》等节目,深受球迷喜爱。", + "腾讯NBA人气女主播"), + (None, "张曼源", "Zhang Manyuan", "host", "主持人", "篮球主持人", + "腾讯体育篮球主持人,曾担任NBA中国赛现场主持人。", + "NBA中国赛现场主持人"), + # ---------------- 经纪人 + (None, "里奇·保罗", "Rich Paul", "agent", "经纪人", "Klutch Sports创始人", + "NBA最具影响力的经纪人之一,旗下客户包括勒布朗·詹姆斯、安东尼·戴维斯、达龙·福克斯等球星,以'球员赋权'理念闻名。", + "代理客户合同总额超20亿美元"), + (None, "比尔·达菲", "Bill Duffy", "agent", "经纪人", "BDA体育创始人", + "老牌NBA经纪人,客户包括卢卡·东契奇、姚明(NBA时期)等,以诚信和国际化运作著称。", + "姚明NBA时期经纪人,代理多届选秀状元"), + (None, "杰夫·施瓦茨", "Jeff Schwartz", "agent", "经纪人", "Excel Sports总裁", + "NBA顶级经纪人之一,客户包括多曼塔斯·萨博尼斯、德文·布克等全明星球员。", + "代理客户合同总额超30亿美元"), + # ---------------- 总经理 / 管理层 + ("LAL", "罗勃·佩林卡", "Rob Pelinka", "gm", "总经理", "洛杉矶湖人总经理", + "前NBA经纪人,2017年起担任湖人总经理,2019年组建詹姆斯+戴维斯组合并夺冠,2026年成功续约詹姆斯。", + "2020年总冠军总经理,2023年季中锦标赛冠军总经理"), + ("OKC", "萨姆·普雷斯蒂", "Sam Presti", "gm", "总经理", "俄克拉荷马雷霆总经理", + "2007年起执掌超音速/雷霆管理层,以顶级选秀眼光著称(选中杜兰特、威少、哈登、SGA),2026年打造出冠军阵容。", + "2026年总冠军总经理,多次最佳总经理提名"), + ("BOS", "布拉德·史蒂文斯", "Brad Stevens", "gm", "总经理", "凯尔特人篮球运营总裁", + "前凯尔特人主教练,2021年转任管理层,2024年组建冠军阵容夺冠,2026年续约怀特保留核心。", + "2024年总冠军总经理"), + # ---------------- 传奇人物 + (None, "姚明", "Yao Ming", "legend", "传奇人物", "中国篮球传奇/NBA中国区大使", + "2002年NBA状元秀,效力火箭8个赛季,场均19.0分9.2篮板,8次入选全明星。退役后曾任中国篮协主席推动CBA改革,2026年出任NBA中国区大使。", + "2016年奈史密斯篮球名人堂,2002年状元秀,8次全明星,5次最佳阵容"), + (None, "迈克尔·乔丹", "Michael Jordan", "legend", "传奇人物", "篮球之神", + "公认的历史最佳球员(GOAT),1984年探花秀进入公牛,六次率队夺冠,两次三连冠。以无解的得分能力和好胜心定义了一个时代。", + "6届总冠军+FMVP,5届MVP,14次全明星,10届得分王,2009年名人堂"), + (None, "科比·布莱恩特", "Kobe Bryant", "legend", "传奇人物", "黑曼巴", + "1996年第13顺位被黄蜂选中后交易至湖人,20年紫金生涯5夺总冠军,单场81分永载史册。'曼巴精神'激励了无数后来者。", + "5届总冠军+2届FMVP,2008年MVP,18次全明星,2020年名人堂"), + (None, "沙奎尔·奥尼尔", "Shaquille O'Neal", "legend", "传奇人物", "大鲨鱼", + "1992年状元秀,史上最具统治力的中锋之一,湖人三连冠核心,2000年包揽MVP+FMVP。", + "4届总冠军+3届FMVP,2000年MVP,15次全明星,2016年名人堂"), + (None, "魔术师约翰逊", "Magic Johnson", "legend", "传奇人物", "魔术师", + "1979年状元秀,湖人Showtime时代核心,新秀赛季即夺FMVP,史上最伟大的组织后卫之一。", + "5届总冠军+3届FMVP,3届MVP,12次全明星,2002年名人堂"), + (None, "蒂姆·邓肯", "Tim Duncan", "legend", "传奇人物", "石佛", + "1997年状元秀,马刺五冠王朝基石,19年生涯全部效力马刺,'未来是你的'经典语录主角。", + "5届总冠军+3届FMVP,2届MVP,15次全明星,2020年名人堂"), + (None, "德克·诺维茨基", "Dirk Nowitzki", "legend", "传奇人物", "德国战车", + "1998年进入NBA,21年独行侠生涯,2011年单核夺冠铸就传奇,金鸡独立后仰跳投成为绝学。", + "2011年总冠军+FMVP,2007年MVP,14次全明星,2023年名人堂"), +] diff --git a/seed_players_a.py b/seed_players_a.py new file mode 100644 index 0000000..b77a503 --- /dev/null +++ b/seed_players_a.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- +"""球员种子数据 Part A:东部球队(模拟 2025-26 赛季) +(队code, 中文名, 英文名, 位置, 号码, 身高cm, 体重kg, 国籍, 选秀年, 顺位, 年薪万, + 本季场均 得分/篮板/助攻/抢断/盖帽/分钟, 生涯场均 得分/篮板/助攻, 生涯场次, 荣誉, 简介) +""" + +PLAYERS_A = [ + # ---- 凯尔特人 BOS + ("BOS", "杰森·塔图姆", "Jayson Tatum", "SF", 0, 203, 95, "美国", 2017, 3, 5400, + 29.1, 8.4, 5.6, 1.1, 0.7, 35.8, 24.8, 7.3, 4.9, 850, "2024总冠军+FMVP,6次全明星,4次最佳一阵", "凯尔特人当家球星,攻防一体的顶级锋线,联盟前十巨星。"), + ("BOS", "杰伦·布朗", "Jaylen Brown", "SG", 7, 198, 101, "美国", 2016, 3, 5000, + 24.2, 6.1, 4.2, 1.2, 0.5, 34.0, 23.0, 5.8, 3.7, 700, "2024总冠军+FMVP,3次全明星", "2024年总决赛MVP,身体天赋炸裂的攻防一体得分手。"), + ("BOS", "德里克·怀特", "Derrick White", "PG", 9, 193, 86, "美国", 2017, 29, 2800, + 17.5, 4.2, 4.8, 1.0, 1.0, 32.5, 15.2, 4.0, 4.4, 560, "2024总冠军,最佳防守阵容", "攻防兼备的顶级拼图型后卫,凯尔特人冠军阵容关键一环。"), + ("BOS", "朱·霍勒迪", "Jrue Holiday", "PG", 4, 193, 93, "美国", 2009, 17, 3400, + 13.8, 4.0, 4.6, 1.2, 0.6, 30.0, 16.3, 4.2, 6.2, 1000, "2届总冠军(2021/2024),5次最佳防守阵容", "外线防守大师,冠军球队最需要的老将后卫。"), + ("BOS", "克里斯塔普斯·波尔津吉斯", "Kristaps Porzingis", "C", 8, 218, 109, "拉脱维亚", 2015, 4, 3200, + 19.4, 7.6, 2.1, 0.7, 1.8, 28.5, 19.0, 7.8, 1.8, 500, "2024总冠军,1次全明星", "独角兽中锋,能投三分能护框,凯尔特人内线屏障。"), + ("BOS", "艾尔·霍福德", "Al Horford", "C", 42, 206, 109, "多米尼加", 2007, 3, 1000, + 9.2, 6.8, 2.8, 0.6, 0.9, 26.0, 13.8, 8.2, 3.3, 1150, "2024总冠军,5次全明星", "征战联盟19年的老将内线,更衣室领袖。"), + # ---- 尼克斯 NYK + ("NYK", "杰伦·布伦森", "Jalen Brunson", "PG", 11, 188, 86, "美国", 2018, 33, 3800, + 27.8, 3.4, 7.2, 1.0, 0.2, 35.0, 22.0, 3.8, 6.1, 480, "2次全明星,2024最佳阵容二阵", "二轮秀逆袭典范,尼克斯进攻发动机。"), + ("NYK", "卡尔-安东尼·唐斯", "Karl-Anthony Towns", "C", 32, 213, 112, "美国", 2015, 1, 5000, + 24.8, 11.2, 3.5, 0.8, 1.3, 33.5, 22.9, 10.4, 3.2, 650, "4次全明星,2016最佳新秀", "投射型空间内线,2025年加盟尼克斯后焕发第二春。"), + ("NYK", "OG·阿努诺比", "OG Anunoby", "SF", 8, 201, 105, "英国", 2017, 23, 3200, + 16.8, 4.9, 2.1, 1.4, 0.7, 33.0, 13.5, 4.7, 1.8, 480, "2023总冠军(猛龙),最佳防守阵容", "顶级3D锋线,尼克斯防守体系的支柱。"), + ("NYK", "约什·哈特", "Josh Hart", "SG", 3, 193, 98, "美国", 2017, 30, 2200, + 14.5, 9.2, 5.4, 1.2, 0.4, 34.0, 10.8, 7.0, 3.0, 550, "2023总冠军(猛龙)", "能量无限的万金油球员,脏活累活全包。"), + ("NYK", "米卡尔·布里奇斯", "Mikal Bridges", "SG", 25, 198, 95, "美国", 2018, 10, 2600, + 17.8, 4.1, 3.2, 1.0, 0.5, 32.0, 15.8, 4.2, 3.0, 560, "铁人(连续出战纪录)", "铁人属性拉满的攻防均衡锋线。"), + # ---- 篮网 BKN + ("BKN", "卡梅隆·布泽尔", "Cameron Boozer", "PF", 1, 206, 112, "美国", 2026, 1, 1200, + 18.5, 9.6, 3.2, 0.8, 0.9, 30.0, 18.5, 9.6, 3.2, 8, "2026状元秀,夏季联赛MVP", "2026年NBA选秀状元,篮网复兴的希望之星。"), + ("BKN", "卡梅隆·约翰逊", "Cameron Johnson", "SF", 24, 203, 95, "美国", 2019, 11, 2400, + 19.2, 4.8, 3.1, 0.9, 0.3, 32.0, 14.2, 4.0, 2.2, 420, "2021总决赛(太阳)", "顶级射手型锋线,篮网重建期最稳的得分点。"), + ("BKN", "尼古拉斯·克拉克斯顿", "Nic Claxton", "C", 33, 211, 98, "美国", 2019, 31, 2200, + 11.5, 9.8, 2.2, 0.7, 1.9, 29.0, 10.5, 8.4, 1.9, 380, "2023最佳防守阵容二阵", "机动型护框中锋,换防能力出色。"), + ("BKN", "卡姆·托马斯", "Cam Thomas", "SG", 4, 193, 88, "美国", 2021, 27, 1800, + 22.4, 3.2, 3.6, 0.7, 0.2, 31.0, 20.6, 3.0, 3.1, 280, "2024单场得分爆发力纪录", "纯得分手,单打能力出众的年轻后卫。"), + ("BKN", "本·西蒙斯", "Ben Simmons", "PG", 10, 208, 109, "澳大利亚", 2016, 1, 2000, + 8.5, 6.2, 5.8, 1.1, 0.6, 24.0, 13.8, 7.8, 7.0, 330, "3次全明星,2021最佳防守一阵", "天赋异禀的组织前锋,伤病影响状态起伏。"), + # ---- 76人 PHI + ("PHI", "乔尔·恩比德", "Joel Embiid", "C", 21, 213, 127, "喀麦隆", 2014, 3, 5500, + 26.8, 10.4, 4.1, 0.8, 1.6, 32.0, 28.4, 11.0, 3.8, 480, "2023常规赛MVP,7次全明星,3次得分王", "现役最具统治力的内线之一,伤病是最大敌人。"), + ("PHI", "泰瑞斯·马克西", "Tyrese Maxey", "PG", 0, 188, 90, "美国", 2020, 21, 3900, + 25.6, 3.6, 6.2, 1.1, 0.3, 35.5, 20.2, 3.4, 5.0, 380, "1次全明星,2024进步最快球员", "速度极快的得分型后卫,76人未来核心。"), + ("PHI", "保罗·乔治", "Paul George", "SF", 13, 203, 100, "美国", 2010, 10, 4500, + 17.2, 5.6, 3.4, 1.3, 0.5, 31.0, 20.6, 6.3, 3.8, 900, "9次全明星,2021最佳阵容一阵", "攻防一体的锋线老将,2025年加盟76人组三巨头。"), + ("PHI", "凯利·乌布雷", "Kelly Oubre Jr", "SF", 9, 198, 92, "美国", 2015, 15, 1300, + 13.5, 5.1, 1.8, 1.0, 0.6, 28.0, 13.0, 4.6, 1.2, 560, "", "能量型锋线,转换进攻利器。"), + ("PHI", "安德烈·德拉蒙德", "Andre Drummond", "C", 1, 211, 127, "美国", 2012, 9, 500, + 8.2, 9.6, 1.2, 0.9, 0.8, 18.0, 12.9, 12.3, 1.3, 900, "2次全明星,4届篮板王", "传统蓝领中锋,篮板嗅觉顶级。"), + # ---- 猛龙 TOR + ("TOR", "斯科蒂·巴恩斯", "Scottie Barnes", "SF", 4, 201, 102, "美国", 2021, 4, 3500, + 20.4, 8.2, 6.1, 1.2, 1.1, 34.0, 17.2, 7.8, 5.2, 320, "1次全明星,2022最佳新秀", "全能锋线,猛龙重建核心。"), + ("TOR", "RJ·巴雷特", "RJ Barrett", "SG", 9, 198, 97, "加拿大", 2019, 3, 2800, + 22.6, 5.4, 4.8, 0.8, 0.3, 33.0, 19.2, 5.2, 3.5, 450, "", "加拿大本土球星,多伦多的锋线尖刀。"), + ("TOR", "雅各布·珀尔特尔", "Jakob Poeltl", "C", 19, 213, 118, "奥地利", 2016, 9, 2000, + 12.4, 9.8, 2.6, 0.7, 1.5, 29.0, 10.2, 8.6, 2.1, 600, "", "扎实的传统中锋,掩护与护框俱佳。"), + ("TOR", "伊曼纽尔·奎克利", "Immanuel Quickley", "PG", 5, 191, 86, "美国", 2020, 25, 2000, + 16.8, 3.8, 5.2, 0.9, 0.2, 30.0, 14.8, 3.9, 4.6, 320, "", "攻防均衡的年轻后卫。"), + ("TOR", "格雷迪·迪克", "Gradey Dick", "SG", 1, 201, 93, "美国", 2023, 13, 900, + 12.5, 3.2, 1.8, 0.8, 0.2, 26.0, 10.8, 2.9, 1.5, 180, "", "潜力射手,三分出手果断。"), + # ---- 雄鹿 MIL + ("MIL", "扬尼斯·阿德托昆博", "Giannis Antetokounmpo", "PF", 34, 211, 110, "希腊", 2013, 15, 6000, + 31.8, 11.5, 6.4, 1.2, 1.3, 34.5, 24.1, 9.8, 4.9, 900, "2021总冠军+FMVP,2届MVP,8次全明星,2020DPOY", "希腊怪兽,历史级冲击力,雄鹿绝对核心。"), + ("MIL", "达米安·利拉德", "Damian Lillard", "PG", 0, 188, 88, "美国", 2012, 6, 5500, + 24.8, 4.2, 7.0, 1.0, 0.2, 34.0, 25.1, 4.3, 6.7, 900, "8次全明星,1次最佳阵容一阵,75大球星", "冷血杀手,超远三分代言人。"), + ("MIL", "克里斯·米德尔顿", "Khris Middleton", "SF", 22, 201, 101, "美国", 2012, 39, 3000, + 14.2, 4.1, 4.6, 0.8, 0.3, 27.0, 17.2, 4.9, 4.1, 800, "2021总冠军,3次全明星", "关键球先生,雄鹿冠军元老。"), + ("MIL", "布鲁克·洛佩斯", "Brook Lopez", "C", 11, 216, 128, "美国", 2008, 10, 2200, + 11.8, 4.9, 1.6, 0.5, 1.9, 27.0, 16.0, 6.3, 1.4, 1050, "2021总冠军,1次全明星", "空间型护框中锋,三分时代的传统巨人。"), + ("MIL", "鲍比·波蒂斯", "Bobby Portis", "PF", 9, 208, 113, "美国", 2015, 22, 1200, + 12.6, 7.8, 1.8, 0.7, 0.5, 24.0, 13.2, 7.8, 1.5, 700, "2021总冠军", "激情四射的板凳匪徒。"), + # ---- 公牛 CHI + ("CHI", "扎克·拉文", "Zach LaVine", "SG", 8, 196, 91, "美国", 2014, 13, 4200, + 22.8, 4.6, 4.8, 1.0, 0.3, 33.5, 20.2, 4.3, 4.0, 700, "2次全明星,2届扣篮大赛冠军", "两届扣篮王,现役顶级得分后卫之一。"), + ("CHI", "尼古拉·武切维奇", "Nikola Vucevic", "C", 9, 211, 118, "黑山", 2011, 16, 2000, + 18.2, 10.6, 3.4, 0.8, 0.8, 31.0, 17.8, 10.5, 2.8, 950, "2次全明星", "技术流欧洲内线,两双机器。"), + ("CHI", "科比·怀特", "Coby White", "PG", 0, 193, 88, "美国", 2019, 7, 1600, + 18.5, 3.8, 5.2, 0.9, 0.2, 32.0, 15.2, 3.6, 4.2, 420, "", "投射型双能卫,公牛后场核心。"), + ("CHI", "约什·吉迪", "Josh Giddey", "PG", 3, 203, 98, "澳大利亚", 2021, 6, 2500, + 14.2, 7.6, 6.8, 1.0, 0.5, 30.0, 13.4, 7.2, 6.4, 320, "", "高个组织后卫,视野出众。"), + ("CHI", "帕特里克·威廉姆斯", "Patrick Williams", "PF", 44, 201, 98, "美国", 2020, 4, 1500, + 11.2, 5.4, 2.0, 0.8, 0.6, 28.0, 10.2, 4.8, 1.8, 350, "", "防守型锋线,可塑性强的拼图球员。"), + # ---- 骑士 CLE + ("CLE", "多诺万·米切尔", "Donovan Mitchell", "SG", 45, 191, 98, "美国", 2017, 13, 4500, + 27.2, 4.8, 5.4, 1.4, 0.4, 34.5, 24.8, 4.6, 4.8, 580, "6次全明星,2023全明星MVP", "得分爆炸力顶级,骑士进攻核心。"), + ("CLE", "达柳斯·加兰", "Darius Garland", "PG", 10, 185, 87, "美国", 2019, 5, 3400, + 19.8, 2.6, 6.8, 1.1, 0.2, 32.0, 18.5, 2.9, 6.4, 420, "1次全明星", "灵动的小个后卫,组织与投射俱佳。"), + ("CLE", "埃文·莫布利", "Evan Mobley", "PF", 4, 211, 98, "美国", 2021, 3, 3500, + 17.4, 9.2, 3.0, 0.9, 1.6, 32.0, 15.2, 8.6, 2.6, 330, "2023最佳防守一阵,2022最佳新秀一阵", "防守核心,协防护框能力联盟顶级。"), + ("CLE", "贾莱特·阿伦", "Jarrett Allen", "C", 31, 211, 110, "美国", 2017, 22, 2000, + 14.2, 10.4, 2.2, 0.7, 1.4, 30.0, 12.8, 9.8, 1.8, 520, "1次全明星", "吃饼型护框中锋,双塔之一。"), + ("CLE", "马克斯·斯特鲁斯", "Max Strus", "SG", 1, 196, 98, "美国", 2019, 0, 1400, + 12.5, 4.2, 3.0, 0.8, 0.2, 28.0, 11.8, 3.8, 2.4, 400, "2023总决赛(热火)", "落选秀逆袭的3D射手。"), + # ---- 步行者 IND + ("IND", "泰瑞斯·哈利伯顿", "Tyrese Haliburton", "PG", 0, 196, 84, "美国", 2020, 12, 4200, + 20.8, 3.8, 9.4, 1.2, 0.6, 34.0, 18.8, 3.9, 9.2, 400, "2次全明星,2024最佳阵容三阵", "组织大师,步行者快攻体系的灵魂。"), + ("IND", "帕斯卡尔·西亚卡姆", "Pascal Siakam", "PF", 43, 203, 104, "喀麦隆", 2016, 27, 4200, + 21.4, 7.2, 4.0, 0.9, 0.5, 33.0, 18.8, 6.9, 3.8, 620, "2次全明星,2019总冠军(猛龙)", "全能锋线,攻防两端都能打。"), + ("IND", "迈尔斯·特纳", "Myles Turner", "C", 33, 211, 113, "美国", 2015, 11, 2100, + 15.8, 7.2, 1.8, 0.6, 2.2, 30.0, 13.8, 7.0, 1.5, 650, "2届盖帽王", "护框+三分兼备的空间中锋。"), + ("IND", "安德鲁·内姆布哈德", "Andrew Nembhard", "PG", 2, 193, 87, "加拿大", 2022, 31, 1200, + 12.8, 3.2, 5.4, 1.0, 0.2, 28.0, 10.8, 2.8, 4.6, 240, "", "大心脏双能卫,季后赛表现亮眼。"), + ("IND", "本内迪克特·马图林", "Bennedict Mathurin", "SG", 0, 198, 95, "加拿大", 2022, 6, 1200, + 16.8, 4.8, 2.0, 0.7, 0.2, 28.0, 15.2, 4.4, 1.8, 260, "", "冲击力十足的得分后卫。"), + # ---- 活塞 DET + ("DET", "凯德·坎宁安", "Cade Cunningham", "PG", 2, 198, 100, "美国", 2021, 1, 3800, + 25.4, 5.8, 7.4, 1.1, 0.5, 34.5, 21.8, 5.0, 6.2, 300, "1次全明星,2022最佳新秀一阵", "2021状元秀,活塞重建基石。"), + ("DET", "杰登·艾维", "Jaden Ivey", "SG", 23, 193, 88, "美国", 2022, 5, 1400, + 17.8, 4.2, 5.0, 0.9, 0.4, 31.0, 16.2, 3.8, 4.4, 260, "", "速度型后卫,突破犀利。"), + ("DET", "杰伦·杜伦", "Jalen Duren", "C", 0, 208, 113, "美国", 2022, 13, 1300, + 11.8, 11.2, 2.4, 0.7, 1.4, 29.0, 10.8, 10.4, 2.1, 240, "", "年轻的两双机器中锋。"), + ("DET", "以赛亚·斯图尔特", "Isaiah Stewart", "PF", 28, 203, 113, "美国", 2020, 16, 1200, + 9.2, 7.4, 1.6, 0.6, 1.1, 26.0, 9.8, 7.2, 1.4, 350, "", "硬汉型内线,防守积极。"), + ("DET", "奥萨尔·汤普森", "Ausar Thompson", "SF", 9, 198, 93, "美国", 2023, 5, 1000, + 11.5, 6.8, 2.8, 1.3, 0.9, 27.0, 10.2, 6.4, 2.4, 160, "", "运动能力炸裂的防守锋线。"), + # ---- 热火 MIA + ("MIA", "巴姆·阿德巴约", "Bam Adebayo", "C", 13, 206, 116, "美国", 2017, 14, 4200, + 20.4, 10.2, 4.2, 1.2, 0.9, 34.0, 16.8, 9.2, 3.6, 550, "3次全明星,2024最佳防守一阵", "全能型中锋,热火防守支柱。"), + ("MIA", "泰勒·希罗", "Tyler Herro", "SG", 14, 196, 88, "美国", 2019, 13, 3300, + 23.6, 5.2, 5.6, 0.8, 0.2, 34.5, 18.8, 4.8, 4.2, 420, "1次全明星,2025进步最快球员", "投射见长的得分后卫,热火外线核心。"), + ("MIA", "凯尔·韦尔", "Kel'el Ware", "C", 7, 213, 104, "美国", 2024, 15, 700, + 14.2, 9.4, 1.6, 0.6, 1.5, 28.0, 12.4, 8.6, 1.4, 120, "", "新秀赛季表现出色,热火未来内线。"), + ("MIA", "特里·罗齐尔", "Terry Rozier", "PG", 2, 185, 86, "美国", 2015, 16, 2400, + 13.8, 3.6, 4.2, 0.9, 0.3, 27.0, 15.8, 4.0, 4.2, 650, "", "能突能投的得分后卫。"), + ("MIA", "邓肯·罗宾逊", "Duncan Robinson", "SG", 55, 201, 98, "美国", 2018, 0, 1800, + 11.2, 2.8, 2.4, 0.6, 0.2, 25.0, 11.8, 2.9, 2.0, 480, "2023总决赛(热火)", "落选秀逆袭的顶级射手。"), + # ---- 老鹰 ATL + ("ATL", "特雷·杨", "Trae Young", "PG", 11, 185, 74, "美国", 2018, 5, 4300, + 25.8, 3.4, 11.2, 1.0, 0.2, 35.5, 25.2, 3.8, 9.6, 540, "3次全明星,2021东部决赛", "进攻组织一肩挑的顶级持球核心。"), + ("ATL", "杰伦·约翰逊", "Jalen Johnson", "SF", 1, 203, 99, "美国", 2021, 20, 1600, + 18.6, 9.8, 5.2, 1.2, 0.9, 33.0, 15.2, 8.4, 4.2, 240, "", "全能锋线,2025年迎来爆发。"), + ("ATL", "扎卡里·里萨谢", "Zaccharie Risacher", "SF", 10, 203, 93, "法国", 2024, 1, 1600, + 13.8, 4.2, 1.8, 0.9, 0.5, 28.0, 12.8, 3.9, 1.6, 150, "2024状元秀,2025最佳新秀一阵", "法国天才锋线,投射潜力大。"), + ("ATL", "奥涅卡·奥孔古", "Onyeka Okongwu", "C", 17, 206, 107, "美国", 2020, 6, 1300, + 11.4, 8.6, 2.0, 0.6, 1.2, 27.0, 10.2, 7.4, 1.6, 320, "", "活力型内线,护框与篮板俱佳。"), + ("ATL", "戴森·丹尼尔斯", "Dyson Daniels", "PG", 5, 198, 92, "澳大利亚", 2022, 8, 1400, + 14.2, 4.8, 4.6, 2.4, 0.6, 31.0, 12.4, 4.4, 4.0, 220, "2026抢断王,最佳防守一阵", "2025-26赛季联盟抢断王,防守大闸。"), + # ---- 魔术 ORL + ("ORL", "保罗·班凯罗", "Paolo Banchero", "PF", 5, 208, 113, "美国", 2022, 1, 3800, + 24.8, 8.4, 5.2, 0.9, 0.6, 34.0, 22.6, 7.6, 4.8, 260, "2023最佳新秀,1次全明星", "2022状元秀,魔术重建核心。"), + ("ORL", "弗朗茨·瓦格纳", "Franz Wagner", "SF", 22, 208, 102, "德国", 2021, 8, 3500, + 22.4, 6.2, 5.0, 1.1, 0.5, 33.5, 19.2, 5.4, 4.2, 320, "1次全明星", "德国全能锋线,攻防一体。"), + ("ORL", "杰伦·萨格斯", "Jalen Suggs", "PG", 4, 196, 93, "美国", 2021, 5, 2200, + 15.2, 4.0, 4.2, 1.6, 0.5, 30.0, 13.2, 3.8, 3.6, 280, "2024最佳防守一阵", "防守凶悍的年轻后卫。"), + ("ORL", "小温德尔·卡特", "Wendell Carter Jr", "C", 34, 208, 122, "美国", 2018, 7, 1200, + 10.8, 8.4, 1.8, 0.6, 0.8, 26.0, 11.8, 8.6, 1.8, 420, "", "技术型内线,策应出色。"), + ("ORL", "安东尼·布莱克", "Anthony Black", "PG", 0, 201, 91, "美国", 2023, 6, 900, + 9.8, 3.6, 3.8, 1.0, 0.4, 25.0, 8.4, 3.2, 3.2, 180, "", "高个控卫,防守潜力大。"), + # ---- 奇才 WAS + ("WAS", "乔丹·普尔", "Jordan Poole", "SG", 13, 193, 88, "美国", 2019, 28, 3000, + 21.4, 3.2, 5.2, 1.2, 0.3, 31.0, 17.2, 2.9, 4.0, 420, "2022总冠军(勇士)", "勇士冠军成员,奇才得分核心。"), + ("WAS", "凯尔·库兹马", "Kyle Kuzma", "PF", 33, 206, 100, "美国", 2017, 27, 2400, + 17.8, 6.8, 3.6, 0.7, 0.5, 31.0, 16.2, 6.4, 2.8, 550, "2020总冠军(湖人)", "万金油锋线,湖人冠军成员。"), + ("WAS", "亚历克斯·萨尔", "Alex Sarr", "C", 20, 216, 98, "法国", 2024, 2, 1300, + 11.8, 8.4, 1.8, 0.7, 1.8, 29.0, 10.2, 7.8, 1.6, 150, "2024榜眼秀", "法国独角兽中锋,护框天赋出众。"), + ("WAS", "科里·基斯珀特", "Corey Kispert", "SG", 24, 198, 101, "美国", 2021, 15, 1200, + 12.4, 3.2, 2.0, 0.6, 0.2, 27.0, 11.8, 3.0, 1.8, 320, "", "稳定射手,拉开空间。"), + ("WAS", "比拉尔·库利巴利", "Bilal Coulibaly", "SF", 0, 203, 88, "法国", 2023, 7, 1000, + 12.8, 4.6, 3.0, 1.0, 0.7, 29.0, 10.8, 4.2, 2.6, 180, "", "法国新星,防守潜力巨大。"), + # ---- 黄蜂 CHA + ("CHA", "拉梅洛·鲍尔", "LaMelo Ball", "PG", 1, 201, 82, "美国", 2020, 3, 3600, + 25.2, 5.6, 8.4, 1.6, 0.3, 34.0, 21.8, 5.8, 7.6, 280, "1次全明星,2021最佳新秀", "打法华丽的年轻控卫,黄蜂门面。"), + ("CHA", "布兰登·米勒", "Brandon Miller", "SF", 24, 206, 91, "美国", 2023, 2, 1700, + 21.8, 5.2, 3.4, 1.0, 0.5, 33.0, 18.8, 4.6, 2.9, 170, "2024最佳新秀一阵", "2023榜眼秀,得分能力出众。"), + ("CHA", "迈尔斯·布里奇斯", "Miles Bridges", "PF", 0, 201, 102, "美国", 2018, 12, 2400, + 17.4, 7.2, 3.0, 0.9, 0.5, 31.0, 16.8, 6.8, 2.8, 420, "", "暴力锋线,能飞善扣。"), + ("CHA", "马克·威廉姆斯", "Mark Williams", "C", 5, 213, 110, "美国", 2022, 15, 900, + 13.2, 9.8, 1.8, 0.6, 1.4, 28.0, 12.4, 9.2, 1.6, 160, "", "吃饼型大个子,护框积极。"), + ("CHA", "特雷·曼", "Tre Mann", "PG", 23, 191, 86, "美国", 2021, 10, 900, + 10.8, 3.4, 3.8, 0.9, 0.2, 26.0, 10.2, 3.2, 3.4, 200, "", "灵动双能卫,替补火力点。"), +] diff --git a/seed_players_b.py b/seed_players_b.py new file mode 100644 index 0000000..6ea02f5 --- /dev/null +++ b/seed_players_b.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- +"""球员种子数据 Part B:西部球队(模拟 2025-26 赛季) +(队code, 中文名, 英文名, 位置, 号码, 身高cm, 体重kg, 国籍, 选秀年, 顺位, 年薪万, + 本季场均 得分/篮板/助攻/抢断/盖帽/分钟, 生涯场均 得分/篮板/助攻, 生涯场次, 荣誉, 简介) +""" + +PLAYERS_B = [ + # ---- 独行侠 DAL + ("DAL", "卢卡·东契奇", "Luka Doncic", "PG", 77, 201, 104, "斯洛文尼亚", 2018, 3, 6000, + 30.4, 8.6, 8.8, 1.4, 0.5, 36.0, 28.9, 8.7, 8.2, 500, "6次全明星,5次最佳一阵,2024得分王", "欧洲天才,持球大核打法,联盟前十巨星。"), + ("DAL", "凯里·欧文", "Kyrie Irving", "PG", 11, 188, 88, "美国", 2011, 1, 4200, + 24.8, 4.2, 5.6, 1.2, 0.4, 34.0, 23.6, 4.4, 5.8, 800, "2016总冠军,8次全明星,2014全明星MVP", "运球大师,关键时刻杀手。"), + ("DAL", "德雷克·莱夫利二世", "Dereck Lively II", "C", 2, 216, 104, "美国", 2023, 12, 1300, + 10.8, 8.6, 2.2, 0.6, 1.8, 27.0, 9.8, 7.8, 1.9, 180, "2024新秀一阵", "吃饼+护框的年轻内线,独行侠未来支柱。"), + ("DAL", "PJ·华盛顿", "P.J. Washington", "PF", 25, 201, 104, "美国", 2019, 12, 1700, + 13.4, 6.2, 2.4, 1.0, 0.9, 30.0, 13.2, 5.8, 2.2, 480, "", "强硬锋线,攻防兼备。"), + ("DAL", "克莱·汤普森", "Klay Thompson", "SG", 31, 198, 100, "美国", 2011, 11, 1600, + 15.8, 3.4, 2.0, 0.7, 0.4, 28.0, 18.2, 3.7, 2.4, 850, "4届总冠军(勇士),5次全明星,单场14三分纪录", "历史顶级射手,水花兄弟之一。"), + # ---- 火箭 HOU + ("HOU", "杰伦·格林", "Jalen Green", "SG", 4, 193, 82, "美国", 2021, 2, 3300, + 21.8, 4.6, 3.8, 1.0, 0.4, 32.0, 20.2, 4.4, 3.4, 380, "2025全明星", "得分爆发力极强的年轻后卫。"), + ("HOU", "阿尔佩伦·申京", "Alperen Sengun", "C", 28, 211, 110, "土耳其", 2021, 16, 3000, + 19.4, 10.2, 5.0, 1.2, 0.9, 32.0, 17.2, 9.4, 4.4, 320, "1次全明星", "土耳其小约基奇,策应内线。"), + ("HOU", "阿门·汤普森", "Amen Thompson", "PG", 1, 201, 91, "美国", 2023, 4, 1600, + 15.2, 8.4, 4.8, 1.6, 1.0, 31.0, 13.2, 7.8, 4.0, 180, "2025最佳防守一阵", "运动天赋怪,防守覆盖面巨大。"), + ("HOU", "小贾巴里·史密斯", "Jabari Smith Jr", "PF", 10, 211, 100, "美国", 2022, 3, 1400, + 13.4, 8.2, 1.6, 0.7, 1.0, 29.0, 12.8, 7.6, 1.4, 260, "", "空间型大前锋,防守积极。"), + ("HOU", "弗雷德·范弗利特", "Fred VanVleet", "PG", 5, 183, 89, "美国", 2016, 0, 4200, + 15.8, 3.6, 6.2, 1.4, 0.4, 33.0, 15.8, 3.6, 5.8, 520, "2019总冠军(猛龙),1次全明星", "落选秀逆袭典范,老练的指挥官。"), + # ---- 灰熊 MEM + ("MEM", "贾·莫兰特", "Ja Morant", "PG", 12, 188, 79, "美国", 2019, 2, 4000, + 25.4, 4.6, 8.2, 1.1, 0.3, 33.5, 23.4, 4.6, 7.6, 380, "1次全明星,2020最佳新秀", "暴力美学控卫,灰熊核心。"), + ("MEM", "小贾伦·杰克逊", "Jaren Jackson Jr", "PF", 13, 211, 110, "美国", 2018, 4, 3000, + 22.4, 5.8, 2.4, 1.0, 2.4, 32.0, 17.8, 5.6, 1.8, 460, "2023最佳防守球员,2届盖帽王,1次全明星", "联盟顶级护框者,攻防一体。"), + ("MEM", "德斯蒙德·贝恩", "Desmond Bane", "SG", 22, 196, 98, "美国", 2020, 30, 3000, + 19.8, 5.2, 5.0, 1.1, 0.4, 33.0, 18.2, 4.8, 4.2, 380, "", "稳如老狗的射手,灰熊外线核心。"), + ("MEM", "马库斯·斯马特", "Marcus Smart", "PG", 36, 191, 100, "美国", 2014, 6, 1900, + 12.4, 3.8, 5.2, 1.4, 0.4, 28.0, 11.8, 3.6, 4.8, 650, "2022最佳防守球员,2024总冠军(凯尔特人)", "球商极高的防守型后卫。"), + ("MEM", "扎克·埃迪", "Zach Edey", "C", 14, 224, 136, "加拿大", 2024, 9, 900, + 12.8, 9.6, 1.4, 0.5, 1.6, 24.0, 11.2, 8.6, 1.2, 140, "2025最佳新秀一阵", "巨人中锋,篮下终结强势。"), + # ---- 鹈鹕 NOP + ("NOP", "锡安·威廉森", "Zion Williamson", "PF", 1, 198, 129, "美国", 2019, 1, 4200, + 24.8, 7.2, 5.2, 1.0, 0.8, 32.0, 23.8, 6.8, 4.6, 280, "2次全明星,2020最佳新秀", "暴力美学大前锋,健康时无人可挡。"), + ("NOP", "布兰登·英格拉姆", "Brandon Ingram", "SF", 14, 203, 86, "美国", 2016, 2, 3600, + 22.6, 5.8, 5.4, 0.9, 0.6, 33.0, 20.8, 5.4, 4.8, 550, "1次全明星,2020进步最快球员", "中距离单打大师,鹈鹕双子星之一。"), + ("NOP", "CJ·麦科勒姆", "CJ McCollum", "SG", 3, 191, 86, "美国", 2013, 10, 3100, + 17.8, 3.8, 4.8, 0.9, 0.3, 31.0, 19.4, 3.8, 4.2, 800, "2019最佳进步球员", "经验丰富的得分后卫,更衣室领袖。"), + ("NOP", "特雷·墨菲三世", "Trey Murphy III", "SF", 25, 203, 93, "美国", 2021, 17, 1600, + 16.8, 4.8, 2.4, 0.9, 0.5, 30.0, 14.2, 4.2, 2.0, 280, "", "高炮台射手,三分产量大。"), + ("NOP", "德章泰·穆雷", "Dejounte Murray", "PG", 5, 193, 82, "美国", 2016, 29, 3100, + 16.8, 6.2, 6.8, 1.6, 0.5, 32.0, 15.4, 6.0, 5.8, 480, "2022全明星,2018抢断王", "攻防一体的全能后卫。"), + # ---- 马刺 SAS + ("SAS", "维克托·文班亚马", "Victor Wembanyama", "C", 1, 224, 107, "法国", 2023, 1, 1400, + 26.4, 11.8, 4.2, 1.4, 3.8, 33.0, 22.8, 10.4, 3.8, 200, "2025最佳防守球员,2024最佳新秀,3届盖帽王,2次全明星", "法国独角兽,跨时代天赋,联盟未来门面。"), + ("SAS", "达龙·福克斯", "De'Aaron Fox", "PG", 2, 191, 84, "美国", 2017, 5, 3800, + 25.2, 4.2, 6.4, 1.6, 0.4, 34.0, 22.4, 4.0, 6.2, 560, "1次全明星,2023全明星技巧赛冠军", "速度型控卫,2025年加盟马刺辅佐文班。"), + ("SAS", "德文·瓦塞尔", "Devin Vassell", "SG", 24, 196, 91, "美国", 2020, 11, 2700, + 16.8, 4.2, 3.4, 1.0, 0.6, 30.0, 15.2, 3.9, 2.9, 320, "", "攻守均衡的年轻得分后卫。"), + ("SAS", "杰里米·索汉", "Jeremy Sochan", "PF", 10, 206, 104, "波兰", 2022, 9, 1400, + 11.8, 7.2, 3.2, 1.0, 0.7, 28.0, 11.2, 6.8, 2.9, 240, "", "多面手锋线,防守凶悍。"), + ("SAS", "凯尔登·约翰逊", "Keldon Johnson", "SF", 3, 196, 100, "美国", 2019, 29, 1800, + 13.4, 4.8, 2.6, 0.8, 0.3, 27.0, 15.2, 5.2, 2.4, 450, "", "冲击力强的板凳尖刀。"), + # ---- 掘金 DEN + ("DEN", "尼古拉·约基奇", "Nikola Jokic", "C", 15, 211, 129, "塞尔维亚", 2014, 41, 5800, + 28.8, 12.6, 9.8, 1.4, 0.9, 35.0, 22.4, 11.4, 7.0, 800, "2023总冠军+FMVP,3届MVP,7次全明星", "历史级组织中锋,2025-26赛季MVP,掘金绝对核心。"), + ("DEN", "贾马尔·穆雷", "Jamal Murray", "PG", 27, 193, 98, "加拿大", 2016, 7, 3800, + 21.4, 4.0, 5.8, 1.0, 0.5, 33.0, 18.8, 4.0, 5.2, 520, "2023总冠军,2023全明星", "季后赛大心脏,掘金冠军后卫。"), + ("DEN", "阿隆·戈登", "Aaron Gordon", "PF", 50, 203, 107, "美国", 2014, 4, 2400, + 14.8, 6.8, 3.2, 0.8, 0.7, 31.0, 14.2, 6.4, 2.8, 700, "2023总冠军", "暴力扣将转型防守悍将。"), + ("DEN", "小迈克尔·波特", "Michael Porter Jr", "SF", 1, 208, 99, "美国", 2018, 14, 3800, + 17.8, 6.8, 1.8, 0.6, 0.6, 31.0, 16.2, 6.2, 1.6, 400, "2023总冠军", "高炮台射手,三分无死角。"), + ("DEN", "克里斯蒂安·布劳恩", "Christian Braun", "SG", 0, 198, 100, "美国", 2022, 21, 700, + 13.2, 4.6, 2.6, 0.9, 0.4, 27.0, 11.2, 4.0, 2.0, 260, "2023总冠军", "活力型后卫,攻防积极。"), + # ---- 森林狼 MIN + ("MIN", "安东尼·爱德华兹", "Anthony Edwards", "SG", 5, 193, 102, "美国", 2020, 1, 4200, + 27.8, 5.8, 4.8, 1.4, 0.6, 35.0, 24.8, 5.4, 4.4, 400, "4次全明星", "华子,森林狼门面,未来门面级得分后卫。"), + ("MIN", "鲁迪·戈贝尔", "Rudy Gobert", "C", 27, 216, 117, "法国", 2013, 27, 4400, + 12.8, 11.8, 1.8, 0.7, 2.1, 31.0, 12.6, 11.8, 1.4, 800, "4届最佳防守球员,4次全明星,2024总冠军", "法国铁塔,历史级护框中锋。"), + ("MIN", "朱利叶斯·兰德尔", "Julius Randle", "PF", 30, 203, 113, "美国", 2014, 7, 3000, + 19.8, 8.2, 4.8, 0.8, 0.4, 32.0, 18.8, 8.4, 4.2, 700, "3次全明星,2021进步最快球员", "全能大前锋,内线硬凿高手。"), + ("MIN", "迈克·康利", "Mike Conley", "PG", 10, 185, 79, "美国", 2007, 4, 2000, + 9.8, 2.8, 5.4, 1.0, 0.2, 26.0, 14.8, 3.0, 5.8, 1100, "1次全明星", "征战近20年的老将控卫,更衣室灵魂。"), + ("MIN", "贾登·麦克丹尼尔斯", "Jaden McDaniels", "SF", 3, 206, 86, "美国", 2020, 28, 2300, + 12.4, 4.8, 1.8, 1.0, 1.0, 30.0, 11.2, 4.2, 1.6, 340, "", "长臂防守者,外线大锁。"), + # ---- 雷霆 OKC + ("OKC", "谢伊·吉尔杰斯-亚历山大", "Shai Gilgeous-Alexander", "PG", 2, 198, 88, "加拿大", 2018, 11, 5500, + 32.8, 5.4, 6.8, 1.8, 1.0, 34.5, 25.8, 5.0, 5.8, 560, "2026总冠军+FMVP,2025常规赛MVP,5次全明星,3次最佳一阵", "SGA,联盟新一代王者,2025-26赛季带队68胜夺冠并荣膺FMVP。"), + ("OKC", "杰伦·威廉姆斯", "Jalen Williams", "SG", 8, 198, 95, "美国", 2022, 12, 3300, + 22.4, 5.6, 5.4, 1.6, 0.8, 33.0, 19.2, 4.8, 4.6, 260, "2026全明星,2026进步最快球员", "雷霆二当家,攻防全面的全能锋卫。"), + ("OKC", "切特·霍姆格伦", "Chet Holmgren", "C", 7, 216, 94, "美国", 2022, 2, 3000, + 18.4, 9.6, 2.8, 0.8, 2.6, 31.0, 16.8, 8.8, 2.4, 220, "2026总冠军,2024新秀一阵", "瘦高独角兽,护框+三分,雷霆内线核心。"), + ("OKC", "以赛亚·哈滕施泰因", "Isaiah Hartenstein", "C", 55, 213, 113, "德国", 2017, 43, 2800, + 12.4, 10.8, 3.2, 0.9, 1.3, 28.0, 9.8, 8.6, 2.6, 420, "2026总冠军", "德国蓝领中锋,策应出色,雷霆内线屏障。"), + ("OKC", "吕冈茨·多尔特", "Luguentz Dort", "SG", 5, 193, 100, "加拿大", 2019, 0, 1800, + 12.8, 4.2, 2.0, 1.2, 0.4, 29.0, 11.8, 3.8, 1.6, 400, "2026总冠军,2025最佳防守一阵", "落选秀逆袭的防守尖兵,外线大锁。"), + ("OKC", "亚历克斯·卡鲁索", "Alex Caruso", "PG", 9, 196, 84, "美国", 2016, 0, 1500, + 8.8, 3.4, 3.2, 1.5, 0.6, 26.0, 8.8, 3.2, 3.0, 480, "2026总冠军,2020总冠军(湖人),2023最佳防守一阵", "冠军拼图,球商极高的防守后卫。"), + # ---- 开拓者 POR + ("POR", "斯库特·亨德森", "Scoot Henderson", "PG", 0, 188, 89, "美国", 2023, 3, 1500, + 16.8, 3.6, 6.4, 1.0, 0.3, 31.0, 15.2, 3.4, 5.8, 200, "", "爆发力十足的年轻控卫。"), + ("POR", "谢登·夏普", "Shaedon Sharpe", "SG", 17, 198, 91, "加拿大", 2022, 7, 1400, + 18.4, 4.8, 3.2, 0.8, 0.4, 31.0, 16.2, 4.4, 2.8, 240, "", "运动天赋出众的得分后卫。"), + ("POR", "德安德烈·艾顿", "Deandre Ayton", "C", 2, 213, 113, "巴哈马", 2018, 1, 3400, + 15.8, 10.4, 1.8, 0.7, 1.0, 29.0, 16.2, 10.4, 1.8, 500, "2021总决赛(太阳),2019新秀一阵", "2018状元秀,技术细腻的中锋。"), + ("POR", "杰拉米·格兰特", "Jerami Grant", "PF", 9, 203, 95, "美国", 2014, 39, 3000, + 16.4, 4.2, 2.8, 0.8, 0.9, 31.0, 14.8, 4.2, 2.2, 700, "", "攻防均衡的锋线老将。"), + ("POR", "德尼·阿夫迪亚", "Deni Avdija", "SF", 8, 206, 102, "以色列", 2020, 9, 1800, + 14.8, 6.4, 3.4, 1.0, 0.6, 30.0, 13.2, 5.8, 2.9, 350, "", "全能锋线,以色列篮球名片。"), + # ---- 爵士 UTA + ("UTA", "劳里·马尔卡宁", "Lauri Markkanen", "PF", 23, 213, 109, "芬兰", 2017, 7, 4000, + 22.8, 8.4, 2.4, 0.8, 0.6, 32.0, 18.8, 7.4, 1.8, 520, "1次全明星,2023进步最快球员", "芬兰司机,空间型大前锋。"), + ("UTA", "科林·塞克斯顿", "Collin Sexton", "PG", 2, 188, 86, "美国", 2018, 8, 1800, + 18.8, 3.2, 4.8, 0.9, 0.2, 30.0, 18.2, 3.0, 4.0, 450, "", "进攻欲望极强的年轻后卫。"), + ("UTA", "沃克·凯斯勒", "Walker Kessler", "C", 24, 216, 111, "美国", 2022, 22, 1200, + 10.8, 11.2, 1.4, 0.5, 2.8, 28.0, 9.8, 10.4, 1.2, 240, "2023盖帽王", "护框巨兽,篮下终结效率高。"), + ("UTA", "基昂特·乔治", "Keyonte George", "SG", 3, 193, 84, "美国", 2023, 16, 900, + 15.2, 3.6, 5.2, 0.8, 0.2, 30.0, 13.2, 3.2, 4.6, 180, "", "潜力双能卫,投射出众。"), + ("UTA", "约翰·科林斯", "John Collins", "PF", 20, 206, 103, "美国", 2017, 19, 2500, + 14.2, 8.4, 1.8, 0.7, 1.0, 28.0, 15.2, 8.2, 1.5, 550, "", "能飞善扣的现代内线。"), + # ---- 勇士 GSW + ("GSW", "斯蒂芬·库里", "Stephen Curry", "PG", 30, 188, 84, "美国", 2009, 7, 5800, + 26.8, 4.8, 6.4, 1.0, 0.3, 33.0, 24.8, 4.7, 6.4, 1000, "4届总冠军(勇士),2届MVP(1次全票),11次全明星,历史三分王", "历史第一射手,改变了篮球的打法,勇士王朝灵魂。"), + ("GSW", "吉米·巴特勒", "Jimmy Butler", "SF", 10, 201, 104, "美国", 2011, 30, 4800, + 18.8, 5.6, 5.2, 1.4, 0.4, 32.0, 18.2, 5.4, 4.6, 850, "2025总冠军+FMVP,6次全明星,5次最佳阵容", "硬汉代表,2025年加盟勇士即夺冠并荣膺FMVP。"), + ("GSW", "德雷蒙德·格林", "Draymond Green", "PF", 23, 198, 104, "美国", 2012, 35, 2400, + 9.2, 6.8, 6.2, 1.2, 0.9, 28.0, 8.8, 6.8, 5.6, 850, "4届总冠军,2017最佳防守球员,4次全明星", "勇士体系发动机,防守大脑。"), + ("GSW", "乔纳森·库明加", "Jonathan Kuminga", "PF", 0, 201, 102, "刚果(美国)", 2021, 7, 1800, + 17.8, 5.4, 3.2, 0.9, 0.6, 30.0, 14.8, 4.8, 2.6, 320, "", "天赋锋线,冲击力十足。"), + ("GSW", "巴迪·希尔德", "Buddy Hield", "SG", 12, 193, 100, "巴哈马", 2016, 6, 1800, + 13.4, 3.4, 2.4, 0.8, 0.3, 26.0, 15.8, 4.2, 2.6, 800, "2023三分大赛冠军", "高产射手,三分线外一站就是威胁。"), + ("GSW", "布兰丁·波杰姆斯基", "Brandin Podziemski", "PG", 5, 193, 93, "美国", 2023, 19, 700, + 10.8, 4.8, 3.8, 1.0, 0.3, 26.0, 9.8, 5.2, 3.4, 180, "", "球商高的年轻后卫,全面。"), + # ---- 湖人 LAL + ("LAL", "勒布朗·詹姆斯", "LeBron James", "SF", 23, 206, 113, "美国", 2003, 1, 5200, + 25.8, 7.8, 8.4, 1.2, 0.6, 34.0, 27.1, 7.5, 7.4, 1550, "4届总冠军+FMVP,4届MVP,21次全明星,历史得分王", "GOAT候选人,篮球史上最伟大的球员之一,第23个赛季依旧顶级。"), + ("LAL", "安东尼·戴维斯", "Anthony Davis", "C", 3, 208, 115, "美国", 2012, 1, 5800, + 24.8, 11.4, 3.6, 1.2, 2.2, 34.0, 24.2, 10.6, 2.6, 800, "2020总冠军,9次全明星,4次最佳防守阵容", "浓眉,攻防一体的历史级大前锋。"), + ("LAL", "奥斯汀·里夫斯", "Austin Reaves", "SG", 15, 196, 89, "美国", 2021, 0, 1400, + 18.4, 4.2, 5.6, 1.0, 0.3, 32.0, 15.2, 3.8, 4.4, 280, "2023季中锦标赛冠军", "落选秀逆袭,湖人第三巨头。"), + ("LAL", "八村塁", "Rui Hachimura", "PF", 28, 203, 104, "日本", 2019, 9, 1700, + 13.8, 5.2, 1.8, 0.7, 0.4, 28.0, 12.8, 4.8, 1.4, 400, "日本篮球第一人", "日本男篮核心,湖人首发锋线。"), + ("LAL", "贾里德·范德比尔特", "Jarred Vanderbilt", "PF", 2, 203, 97, "美国", 2018, 41, 1100, + 6.8, 5.6, 1.4, 1.0, 0.5, 22.0, 6.8, 5.4, 1.2, 340, "", "防守能量型锋线,拼抢积极。"), + ("LAL", "道尔顿·克内克特", "Dalton Knecht", "SF", 4, 198, 98, "美国", 2024, 17, 600, + 11.8, 3.4, 1.6, 0.6, 0.2, 25.0, 10.8, 3.2, 1.4, 130, "", "大龄新秀射手,即战力强。"), + # ---- 快船 LAC + ("LAC", "科怀·伦纳德", "Kawhi Leonard", "SF", 2, 201, 104, "美国", 2011, 15, 5000, + 22.8, 6.4, 4.2, 1.4, 0.8, 32.0, 19.8, 6.4, 3.0, 700, "2届总冠军+FMVP(2014/2019),6次全明星,2届DPOY", "机器人,攻防一体,健康时是联盟最强锋线之一。"), + ("LAC", "詹姆斯·哈登", "James Harden", "PG", 1, 196, 100, "美国", 2009, 3, 3400, + 18.4, 5.6, 8.4, 1.2, 0.7, 33.0, 24.1, 5.6, 7.1, 1100, "2018MVP,10次全明星,3届得分王,2届助攻王", "大胡子,历史级双能卫,快船组织核心。"), + ("LAC", "诺曼·鲍威尔", "Norman Powell", "SG", 24, 193, 98, "美国", 2015, 46, 2000, + 18.8, 3.6, 2.4, 0.9, 0.3, 30.0, 14.2, 3.0, 1.8, 600, "2019总冠军(猛龙)", "得分效率极高的第六人型得分手。"), + ("LAC", "伊维察·祖巴茨", "Ivica Zubac", "C", 40, 213, 109, "克罗地亚", 2016, 32, 1400, + 14.2, 11.2, 2.0, 0.6, 1.4, 29.0, 11.2, 9.4, 1.6, 550, "", "扎实的欧洲中锋,快船内线支柱。"), + ("LAC", "克里斯·邓恩", "Kris Dunn", "PG", 8, 191, 93, "美国", 2016, 5, 800, + 7.8, 3.8, 4.2, 1.6, 0.5, 24.0, 8.2, 3.8, 4.0, 420, "2025最佳防守一阵", "防守型控卫,撕咬式防守。"), + # ---- 太阳 PHX + ("PHX", "凯文·杜兰特", "Kevin Durant", "PF", 35, 208, 109, "美国", 2007, 2, 5200, + 27.4, 6.8, 5.2, 0.9, 1.2, 35.0, 27.3, 7.0, 4.4, 1050, "2届总冠军+FMVP(勇士),2014MVP,14次全明星,4届得分王", "死神,历史顶级得分手,38岁依旧高效。"), + ("PHX", "德文·布克", "Devin Booker", "SG", 1, 198, 93, "美国", 2015, 13, 5200, + 26.8, 4.6, 6.8, 1.0, 0.4, 35.0, 24.8, 4.2, 5.2, 700, "4次全明星,2021总决赛,单场70分先生", "太阳核心,进攻万花筒。"), + ("PHX", "布拉德利·比尔", "Bradley Beal", "SG", 3, 193, 94, "美国", 2012, 3, 5000, + 17.8, 3.8, 4.6, 1.0, 0.5, 32.0, 21.8, 4.2, 4.4, 800, "3次全明星", "得分能力出众的老将后卫。"), + ("PHX", "尤素夫·努尔基奇", "Jusuf Nurkic", "C", 20, 213, 132, "波黑", 2014, 16, 1700, + 10.8, 9.8, 3.2, 0.8, 0.9, 27.0, 12.8, 9.4, 2.4, 600, "", "重型中锋,策应能力强。"), + ("PHX", "格雷森·阿伦", "Grayson Allen", "SG", 8, 193, 90, "美国", 2018, 21, 1600, + 11.8, 3.4, 3.0, 0.9, 0.2, 27.0, 11.2, 3.2, 2.8, 500, "2024三分大赛冠军", "强硬射手,太阳外线炮台。"), + # ---- 国王 SAC + ("SAC", "多曼塔斯·萨博尼斯", "Domantas Sabonis", "C", 11, 211, 109, "立陶宛", 2016, 11, 4000, + 19.8, 12.4, 6.2, 0.9, 0.6, 34.0, 16.2, 10.8, 4.8, 650, "3次全明星,2023篮板王", "两双机器,国王内线核心。"), + ("SAC", "德玛尔·德罗赞", "DeMar DeRozan", "SF", 10, 198, 100, "美国", 2009, 9, 2800, + 21.4, 4.2, 4.8, 0.9, 0.4, 33.0, 21.2, 4.4, 4.0, 1100, "6次全明星,2018最佳阵容二阵", "中距离大师,冷血关键先生。"), + ("SAC", "马利克·蒙克", "Malik Monk", "SG", 0, 191, 91, "美国", 2017, 11, 1800, + 16.8, 3.4, 4.8, 0.8, 0.4, 30.0, 14.2, 3.0, 4.0, 500, "", "进攻万花筒第六人。"), + ("SAC", "基根·穆雷", "Keegan Murray", "PF", 13, 203, 98, "美国", 2022, 4, 1400, + 15.2, 5.8, 1.8, 0.9, 0.7, 31.0, 13.8, 5.4, 1.6, 260, "", "稳定3D锋线,投篮扎实。"), + ("SAC", "基恩·埃利斯", "Keon Ellis", "SG", 23, 196, 79, "美国", 2022, 0, 600, + 8.8, 3.2, 2.4, 1.3, 0.5, 25.0, 7.8, 2.9, 2.0, 180, "", "落选秀,防守拼命三郎。"), +] diff --git a/seed_teams.py b/seed_teams.py new file mode 100644 index 0000000..5282dfd --- /dev/null +++ b/seed_teams.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +"""球队 / 运动 / 联赛 / 排名 种子数据(2025-26 赛季,模拟)""" + +SPORTS = [ + ("basketball", "篮球", "Basketball"), +] + +LEAGUES = [ + ("NBA", "美国职业篮球联赛", "National Basketball Association", "美国", "2025-26"), +] + +# (code, 中文名, 英文名, 城市, 主场, 建队, 总冠军数, 主教练, 简介) +TEAMS = [ + ("BOS", "波士顿凯尔特人", "Boston Celtics", "波士顿", "TD花园球馆", 1946, 18, "乔·马祖拉", "NBA历史最悠久豪门之一,2024年夺得队史第18冠,双探花塔图姆与布朗领衔。"), + ("NYK", "纽约尼克斯", "New York Knicks", "纽约", "麦迪逊广场花园", 1946, 2, "汤姆·锡伯杜", "坐落于篮球麦加麦迪逊广场花园,2025年交易得到唐斯后重返争冠行列。"), + ("BKN", "布鲁克林篮网", "Brooklyn Nets", "布鲁克林", "巴克莱中心", 1967, 0, "霍尔迪·费尔南德斯", "2026年选秀大会用状元签选中卡梅隆·布泽尔,进入重建新时代。"), + ("PHI", "费城76人", "Philadelphia 76ers", "费城", "富国银行中心", 1946, 3, "尼克·纳斯", "恩比德与马克西双核带队,近年来屡受伤病困扰。"), + ("TOR", "多伦多猛龙", "Toronto Raptors", "多伦多", "丰业银行球馆", 1995, 1, "达尔科·拉贾科维奇", "2019年队史首冠,巴恩斯与巴雷特组成的加拿大双星。"), + ("MIL", "密尔沃基雄鹿", "Milwaukee Bucks", "密尔沃基", "费哲论坛球馆", 1968, 2, "道格·里弗斯", "字母哥2021年带队夺冠,利拉德加盟组成内外双核。"), + ("CHI", "芝加哥公牛", "Chicago Bulls", "芝加哥", "联合中心", 1966, 6, "比利·多诺万", "乔丹时代六冠王朝,如今拉文与武切维奇带队重建中。"), + ("CLE", "克利夫兰骑士", "Cleveland Cavaliers", "克利夫兰", "火箭按揭球馆", 1970, 1, "肯尼·阿特金森", "2025-26赛季常规赛58胜高居东部第二,米切尔领衔。"), + ("IND", "印第安纳步行者", "Indiana Pacers", "印第安纳波利斯", "甘布里吉球馆", 1967, 0, "里克·卡莱尔", "哈利伯顿组织核心,2025年闯入东部决赛。"), + ("DET", "底特律活塞", "Detroit Pistons", "底特律", "小凯撒球馆", 1941, 3, "J.B.比克斯塔夫", "坎宁安领衔的年轻球队,2025年重返季后赛。"), + ("MIA", "迈阿密热火", "Miami Heat", "迈阿密", "卡塞亚中心", 1988, 3, "埃里克·斯波尔斯特拉", "铁血文化代表,2023年黑八闯入总决赛,阿德巴约与希罗双核。"), + ("ATL", "亚特兰大老鹰", "Atlanta Hawks", "亚特兰大", "州立农场球馆", 1946, 1, "奎因·斯奈德", "特雷·杨领衔,2025年交易得到丹尼尔斯补强防守。"), + ("ORL", "奥兰多魔术", "Orlando Magic", "奥兰多", "安利中心", 1989, 0, "贾马尔·莫斯利", "班凯罗与瓦格纳的锋线双星,联盟最年轻强队之一。"), + ("WAS", "华盛顿奇才", "Washington Wizards", "华盛顿", "第一资本球馆", 1961, 1, "布莱恩·基夫", "重建中的年轻球队,2024年榜眼萨尔领衔。"), + ("CHA", "夏洛特黄蜂", "Charlotte Hornets", "夏洛特", "光谱中心", 1988, 0, "查尔斯·李", "拉梅洛·鲍尔与米勒的后场组合,天赋十足。"), + ("DAL", "达拉斯独行侠", "Dallas Mavericks", "达拉斯", "美航中心", 1980, 1, "杰森·基德", "2011年诺维茨基带队夺冠,东契奇与欧文双核。"), + ("HOU", "休斯顿火箭", "Houston Rockets", "休斯顿", "丰田中心", 1967, 2, "伊梅·乌度卡", "申京与杰伦·格林领衔的青年军,2025年闯入西部半决赛。"), + ("MEM", "孟菲斯灰熊", "Memphis Grizzlies", "孟菲斯", "联邦快递球馆", 1995, 0, "泰勒·詹金斯", "莫兰特领衔的防守强队,杰克逊2023年最佳防守球员。"), + ("NOP", "新奥尔良鹈鹕", "New Orleans Pelicans", "新奥尔良", "冰沙国王中心", 2002, 0, "威利·格林", "锡安与英格拉姆的双核,健康时冲击力十足。"), + ("SAS", "圣安东尼奥马刺", "San Antonio Spurs", "圣安东尼奥", "AT&T中心", 1967, 5, "格雷格·波波维奇", "波波维奇治下五冠王朝,2025年交易得到福克斯辅佐文班亚马。"), + ("DEN", "丹佛掘金", "Denver Nuggets", "丹佛", "波尔球馆", 1967, 1, "迈克尔·马龙", "约基奇2023年带队夺得队史首冠,三届MVP坐镇。"), + ("MIN", "明尼苏达森林狼", "Minnesota Timberwolves", "明尼苏达", "目标中心", 1989, 0, "克里斯·芬奇", "爱德华兹领衔,2024年闯入西部决赛创造队史。"), + ("OKC", "俄克拉荷马雷霆", "Oklahoma City Thunder", "俄克拉荷马城", "佩科姆中心", 1967, 2, "马克·戴格诺特", "2025-26赛季68胜+总冠军,亚历山大荣膺总决赛MVP,联盟新王。"), + ("POR", "波特兰开拓者", "Portland Trail Blazers", "波特兰", "摩达中心", 1970, 1, "昌西·比卢普斯", "1977年唯一一冠,如今亨德森与夏普的年轻后场。"), + ("UTA", "犹他爵士", "Utah Jazz", "盐湖城", "德尔塔中心", 1974, 0, "威尔·哈迪", "马尔卡宁领衔的重建球队。"), + ("GSW", "金州勇士", "Golden State Warriors", "金州", "大通中心", 1946, 7, "史蒂夫·科尔", "库里领衔的王朝球队,2025年巴特勒加盟后再夺一冠。"), + ("LAL", "洛杉矶湖人", "Los Angeles Lakers", "洛杉矶", "加密网球馆", 1947, 17, "JJ·雷迪克", "詹姆斯与戴维斯双核,历史豪门与凯尔特人并列17冠。"), + ("LAC", "洛杉矶快船", "Los Angeles Clippers", "洛杉矶", "直觉巨蛋", 1970, 0, "泰伦·卢", "伦纳德与哈登领衔,2024年启用新主场直觉巨蛋。"), + ("PHX", "菲尼克斯太阳", "Phoenix Suns", "菲尼克斯", "足迹中心", 1968, 0, "迈克·布登霍尔泽", "杜兰特与布克双核,2021年闯入总决赛憾负雄鹿。"), + ("SAC", "萨克拉门托国王", "Sacramento Kings", "萨克拉门托", "黄金1号中心", 1923, 1, "迈克·布朗", "小萨博尼斯与德罗赞领衔,2023年终结16年季后赛荒。"), +] + +# (队code, 分区, 排名, 胜, 负) 2025-26 常规赛最终排名(模拟) +STANDINGS = [ + ("OKC", "西部", 1, 68, 14), ("DEN", "西部", 2, 57, 25), ("LAL", "西部", 3, 55, 27), + ("GSW", "西部", 4, 53, 29), ("DAL", "西部", 5, 51, 31), ("MEM", "西部", 6, 50, 32), + ("HOU", "西部", 7, 49, 33), ("MIN", "西部", 8, 48, 34), ("LAC", "西部", 9, 47, 35), + ("SAC", "西部", 10, 44, 38), ("PHX", "西部", 11, 42, 40), ("SAS", "西部", 12, 38, 44), + ("POR", "西部", 13, 30, 52), ("UTA", "西部", 14, 25, 57), ("NOP", "西部", 15, 24, 58), + ("BOS", "东部", 1, 62, 20), ("CLE", "东部", 2, 58, 24), ("NYK", "东部", 3, 54, 28), + ("MIL", "东部", 4, 52, 30), ("IND", "东部", 5, 49, 33), ("MIA", "东部", 6, 46, 36), + ("ORL", "东部", 7, 45, 37), ("PHI", "东部", 8, 43, 39), ("ATL", "东部", 9, 41, 41), + ("CHI", "东部", 10, 38, 44), ("BKN", "东部", 11, 35, 47), ("TOR", "东部", 12, 32, 50), + ("DET", "东部", 13, 30, 52), ("WAS", "东部", 14, 22, 60), ("CHA", "东部", 15, 21, 61), +] diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..6da749d --- /dev/null +++ b/start.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# NBA球迷大全 启动脚本 +# 用法: ./start.sh [stop|restart|status|seed] +DIR="$(cd "$(dirname "$0")" && pwd)" +PORT=16090 +PY=/home/hz1/miniconda3/envs/openclaw/bin/python3 +LOG="$DIR/logs/app.log" +PID_FILE="$DIR/logs/app.pid" + +start() { + if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then + echo "已在运行 PID=$(cat "$PID_FILE")" + return + fi + cd "$DIR" + nohup "$PY" api.py >> "$LOG" 2>&1 & + echo $! > "$PID_FILE" + sleep 2 + echo "✅ NBA球迷大全 已启动 http://$(hostname -I 2>/dev/null | awk '{print $1}'):$PORT (PID $(cat "$PID_FILE"))" + echo " 日志: $LOG" +} + +stop() { + if [ -f "$PID_FILE" ]; then + kill "$(cat "$PID_FILE")" 2>/dev/null + rm -f "$PID_FILE" + echo "已停止" + else + echo "未在运行" + fi +} + +case "${1:-start}" in + start) start ;; + stop) stop ;; + restart) stop; sleep 1; start ;; + status) + if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then + echo "运行中 PID=$(cat "$PID_FILE")"; curl -s -m 5 "http://127.0.0.1:$PORT/api/health" | head -c 300; echo + else + echo "未运行" + fi ;; + seed) cd "$DIR" && "$PY" seed.py "${2:-}" ;; + *) echo "用法: $0 [start|stop|restart|status|seed]"; exit 1 ;; +esac diff --git a/static/app.js b/static/app.js new file mode 100644 index 0000000..3c30c7b --- /dev/null +++ b/static/app.js @@ -0,0 +1,276 @@ +/* NBA球迷大全 前端逻辑(原生 JS,无构建) */ +const $ = (s) => document.querySelector(s); +const $$ = (s) => [...document.querySelectorAll(s)]; +const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); + +const chatHistory = []; +let chatBusy = false; + +/* ================= 标签页切换 ================= */ +$$(".tab").forEach((t) => t.addEventListener("click", () => { + $$(".tab").forEach((x) => x.classList.remove("active")); + $$(".view").forEach((x) => x.classList.remove("active")); + t.classList.add("active"); + $("#view-" + t.dataset.view).classList.add("active"); + loadView(t.dataset.view); +})); + +function loadView(v) { + if (v === "teams") loadTeams(); + else if (v === "players") loadPlayers(""); + else if (v === "games") loadGames(""); + else if (v === "news") loadNews(""); + else if (v === "persons") loadPersons(""); + else if (v === "standings") loadStandings(""); +} + +/* ================= 对话 ================= */ +function addMsg(role, html) { + const div = document.createElement("div"); + div.className = `msg ${role}`; + div.innerHTML = `
${role === "user" ? "🧑" : "🤖"}
${html}
`; + $("#chat-list").appendChild(div); + $("#chat-list").scrollTop = $("#chat-list").scrollHeight; + return div; +} +function showTyping() { + const div = document.createElement("div"); + div.className = "msg bot"; + div.innerHTML = `
🤖
`; + $("#chat-list").appendChild(div); + $("#chat-list").scrollTop = $("#chat-list").scrollHeight; + return div; +} +async function sendChat(text) { + if (chatBusy) return; + chatBusy = true; + $("#send-btn").disabled = true; + addMsg("user", esc(text)); + const typing = showTyping(); + try { + const r = await fetch("/api/chat", { method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ message: text, history: chatHistory.slice(-8) }) }); + const d = await r.json(); + typing.remove(); + if (d.error) { addMsg("bot", `⚠️ ${esc(d.error)}`); return; } + let html = esc(d.reply).replace(/\n/g, "
"); + if (d.sources && d.sources.length) { + html += "
" + d.sources.map((s) => `来源:${esc(s.tool)}`).join(""); + } + addMsg("bot", html); + chatHistory.push({ user: text, assistant: d.reply }); + if (chatHistory.length > 20) chatHistory.splice(0, chatHistory.length - 20); + } catch (e) { + typing.remove(); + addMsg("bot", "⚠️ 网络异常,请稍后再试。"); + } finally { + chatBusy = false; + $("#send-btn").disabled = false; + } +} +$("#send-btn").addEventListener("click", () => { const v = $("#chat-input").value.trim(); if (v) { $("#chat-input").value = ""; sendChat(v); } }); +$("#chat-input").addEventListener("keydown", (e) => { if (e.key === "Enter") $("#send-btn").click(); }); + +/* 快捷问题 */ +async function loadChips() { + try { + const qs = await (await fetch("/api/suggestions")).json(); + $("#chips").innerHTML = qs.map((q) => ``).join(""); + $$("#chips button").forEach((b) => b.addEventListener("click", () => sendChat(b.textContent))); + } catch (e) {} +} +loadChips(); + +/* ================= 通用请求 ================= */ +async function getJSON(url) { const r = await fetch(url); if (!r.ok) throw new Error(r.status); return r.json(); } + +/* ================= 球队 ================= */ +async function loadTeams() { + const teams = await getJSON("/api/teams?limit=50"); + $("#grid-teams").innerHTML = teams.map((t) => ` +
+

${esc(t.name)} ${esc(t.name_en)}

+
🏙️ ${esc(t.city)} · ${esc(t.arena)}
🏆 总冠军 ×${t.champion_count} · 建队 ${t.founded} 年
🧑‍🏫 主帅:${esc(t.head_coach)}
+
`).join(""); +} +async function openTeam(id) { + const d = await getJSON(`/api/teams/${id}`); + const t = d.team; + const roster = d.roster.map((p) => `
${esc(p.name)} ${esc(p.position)} #${p.number} — 场均 ${p.season.pts}分
`).join(""); + const games = d.recent_games.map((g) => `
${esc(g.game_time.slice(0, 10))} ${esc(g.away_team)} ${g.away_score ?? "?"} : ${g.home_score ?? "?"} ${esc(g.home_team)}(${g.status === "finished" ? "已结束" : "未开始"})
`).join(""); + openModal(` +

${esc(t.name)} ${esc(t.name_en)}

+
${esc(t.city)} · ${esc(t.arena)} · 建队 ${t.founded} · 总冠军 ×${t.champion_count}
+

${esc(t.intro)}

+
🧑‍🏫 主教练
${esc(t.head_coach)}
+
⭐ 主要球员(本赛季场均)
${roster || "暂无"} +
📅 近期比赛
${games || "暂无"} + `); +} + +/* ================= 球员 ================= */ +async function loadPlayers(q) { + const players = await getJSON(`/api/players?q=${encodeURIComponent(q)}&limit=60`); + $("#grid-players").innerHTML = players.map((p) => ` +
+

${esc(p.name)} ${esc(p.position)} #${p.number}

+
${esc(p.team || "")} · ${esc(p.country)}
+
本季:${p.season.pts} 分 / ${p.season.reb} 板 / ${p.season.ast} 助
+
`).join(""); +} +$("#btn-players").addEventListener("click", () => loadPlayers($("#search-players").value.trim())); +$("#search-players").addEventListener("keydown", (e) => { if (e.key === "Enter") $("#btn-players").click(); }); +async function openPlayer(id) { + const p = await getJSON(`/api/players/${id}`); + openModal(` +

${esc(p.name)} ${esc(p.name_en)}

+
${esc(p.team || "自由球员")} · ${esc(p.position)} · #${p.number} · ${esc(p.country)}
+
+
身高${p.height_cm} cm
+
体重${p.weight_kg} kg
+
选秀${esc(p.draft)}
+
年薪$${(p.salary_m / 100).toFixed(2)} 亿
+
本季场均${p.season.pts}分 ${p.season.reb}板 ${p.season.ast}助
+
本季防守${p.season.stl}断 ${p.season.blk}帽
+
生涯场均${p.career.pts}分 ${p.career.reb}板 ${p.career.ast}助
+
生涯场次${p.career.games} 场
+
+
🏅 荣誉
${esc(p.awards || "暂无")}
+
📝 简介
${esc(p.bio || "暂无")}
+ `); +} + +/* ================= 比赛 ================= */ +let gameStatusFilter = ""; +async function loadGames(q) { + const url = `/api/games?q=${encodeURIComponent(q)}&limit=40`; + const games = await getJSON(url); + const list = games.filter((g) => !gameStatusFilter || g.status === gameStatusFilter); + $("#list-games").innerHTML = list.map((g) => { + const finished = g.status === "finished"; + const score = finished ? `${g.away_score} : ${g.home_score}` : "VS"; + return `
+
+ ${esc(g.round_name)} + ${finished ? "已结束" : g.status === "scheduled" ? "未开始" : "进行中"} +
+
+ ${esc(g.away_team)} + ${score} + ${esc(g.home_team)} +
+
🕐 ${esc(g.game_time)} · ${esc(g.venue)} · ${esc(g.broadcast)}
+
`; + }).join("") || '
没有符合条件的比赛
'; +} +$$("#view-games .btn.small").forEach((b) => b.addEventListener("click", () => { + $$("#view-games .btn.small").forEach((x) => x.classList.remove("active")); + b.classList.add("active"); + gameStatusFilter = b.dataset.status; + loadGames($("#search-games").value.trim()); +})); +$("#btn-games").addEventListener("click", () => loadGames($("#search-games").value.trim())); +$("#search-games").addEventListener("keydown", (e) => { if (e.key === "Enter") $("#btn-games").click(); }); +async function openGame(id) { + const g = await getJSON(`/api/games/${id}`); + const finished = g.status === "finished"; + const head = finished + ? `
+ ${esc(g.away_team)} ${g.away_score} : ${g.home_score} ${esc(g.home_team)}
` + : `
${esc(g.away_team)} VS ${esc(g.home_team)}
`; + const stats = (g.box_score || []).map((s) => ` + ${esc(s.team_name)}${esc(s.player_name)} + ${s.points}${s.rebounds}${s.assists} + ${s.steals}${s.blocks}${s.minutes}`).join(""); + openModal(` +

${esc(g.round_name)}

+ ${head} +
🕐 ${esc(g.game_time)} · ${esc(g.venue)} · ${esc(g.broadcast)}
+ ${stats ? `
📊 球员技术统计
+ ${stats}
球队球员得分篮板助攻抢断盖帽分钟
` : ""} + `); +} + +/* ================= 新闻 ================= */ +async function loadNews(q) { + const news = await getJSON(`/api/news?q=${encodeURIComponent(q)}&limit=30`); + $("#list-news").innerHTML = news.map((n) => ` +
+
+ ${n.kind === "wiki" ? "百科" : "新闻"} +

${esc(n.title)}

+
+
🕐 ${esc(n.publish_time)} · ${esc(n.source)}${n.author ? " · " + esc(n.author) : ""}
+
${esc(n.summary || n.content || "")}…
+
`).join(""); +} +$("#btn-news").addEventListener("click", () => loadNews($("#search-news").value.trim())); +$("#search-news").addEventListener("keydown", (e) => { if (e.key === "Enter") $("#btn-news").click(); }); +async function openNews(id) { + const n = await getJSON(`/api/news/${id}`); + openModal(` +

${esc(n.title)}

+
🕐 ${esc(n.publish_time)} · ${esc(n.source)} · ${esc(n.author || "")} · ${n.kind === "wiki" ? "百科词条" : "新闻"}
+
${esc(n.content).replace(/\n/g, "
")}
+ `); +} + +/* ================= 人物 ================= */ +let personRoleFilter = ""; +async function loadPersons(q) { + const persons = await getJSON(`/api/persons?q=${encodeURIComponent(q)}&limit=60`); + const list = persons.filter((p) => !personRoleFilter || p.role === personRoleFilter); + $("#grid-persons").innerHTML = list.map((p) => ` +
+

${esc(p.name)} ${esc(p.name_en || "")}

+
${esc(p.role_cn)}${esc(p.title || "")}${p.team ? " · " + esc(p.team) : ""}
+
${esc((p.bio || "").slice(0, 60))}…
+
`).join(""); +} +$$("#view-persons .btn.small").forEach((b) => b.addEventListener("click", () => { + $$("#view-persons .btn.small").forEach((x) => x.classList.remove("active")); + b.classList.add("active"); + personRoleFilter = b.dataset.role; + loadPersons($("#search-persons").value.trim()); +})); +$("#btn-persons").addEventListener("click", () => loadPersons($("#search-persons").value.trim())); +$("#search-persons").addEventListener("keydown", (e) => { if (e.key === "Enter") $("#btn-persons").click(); }); +async function openPerson(id) { + const p = await getJSON(`/api/persons/${id}`); + openModal(` +

${esc(p.name)} ${esc(p.name_en || "")}

+
${esc(p.role_cn)} · ${esc(p.title || "")}${p.team ? " · " + esc(p.team) : ""}
+
📝 简介
${esc(p.bio || "暂无")}
+
🏅 成就
${esc(p.achievements || "暂无")}
+ `); +} + +/* ================= 排名 ================= */ +let standingsConf = ""; +async function loadStandings() { + const rows = await getJSON(`/api/standings?conf=${encodeURIComponent(standingsConf)}`); + const groups = {}; + rows.forEach((r) => { (groups[r.conference] = groups[r.conference] || []).push(r); }); + $("#wrap-standings").innerHTML = Object.entries(groups).map(([conf, list]) => ` +

${conf}赛区

+ + ${list.map((r) => ` + + `).join("")} +
排名球队胜率战绩
${r.rank}${esc(r.team)}${r.wins}${r.losses}${r.win_pct}%${r.wins}-${r.losses}
`).join(""); +} +$$("#view-standings .btn.small").forEach((b) => b.addEventListener("click", () => { + $$("#view-standings .btn.small").forEach((x) => x.classList.remove("active")); + b.classList.add("active"); + standingsConf = b.dataset.conf; + loadStandings(); +})); + +/* ================= 弹窗 ================= */ +function openModal(html) { + $("#modal-body").innerHTML = html; + $("#modal").classList.remove("hidden"); +} +$("#modal-close").addEventListener("click", () => $("#modal").classList.add("hidden")); +$("#modal").addEventListener("click", (e) => { if (e.target.id === "modal") $("#modal").classList.add("hidden"); }); +document.addEventListener("keydown", (e) => { if (e.key === "Escape") $("#modal").classList.add("hidden"); }); diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..f969d0a --- /dev/null +++ b/static/index.html @@ -0,0 +1,105 @@ + + + + + +NBA球迷大全 + + + +
+ + +
+ +
+ +
+
+
+
+
🤖
+
+

你好,我是NBA球迷大全助手!可以问我任何关于比赛、球员、球队、新闻、人物的问题,我会基于数据库给你准确答案~

+

试试:2026年总决赛谁赢了? / 库里本赛季场均多少分? / 介绍一下杨毅

+
+
+
+
+
+ + +
+
+
+ + +
+
+
+
+
+
+
+ + + + + +
+
+
+
+
+
+
+
+
+ + + + + + + + +
+
+
+
+
+ + + +
+
+
+
+ + + + +
NBA球迷大全 v1.0 · 数据为模拟演示数据(2025-26 赛季) · LLM: DeepSeek · 向量: Chroma + bge-large-zh
+ + + + diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..f4792cf --- /dev/null +++ b/static/style.css @@ -0,0 +1,110 @@ +:root { + --bg: #0d1117; --bg2: #161b22; --card: #1c2333; --line: #2d3748; + --txt: #e6edf3; --sub: #8b949e; --orange: #f97316; --orange2: #fb923c; + --blue: #38bdf8; --green: #34d399; --red: #f87171; --radius: 12px; +} +* { margin: 0; padding: 0; box-sizing: border-box; } +body { background: var(--bg); color: var(--txt); font-family: "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; min-height: 100vh; display: flex; flex-direction: column; } + +/* ---------- 顶栏 ---------- */ +.topbar { background: linear-gradient(135deg, #1a120b 0%, #241a10 60%, #1c2333 100%); border-bottom: 1px solid var(--line); padding: 14px 24px; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; position: sticky; top: 0; z-index: 10; } +.logo { display: flex; align-items: center; gap: 12px; } +.logo-ball { font-size: 34px; filter: drop-shadow(0 0 8px rgba(249,115,22,.6)); } +.logo h1 { font-size: 22px; letter-spacing: 1px; background: linear-gradient(90deg, #fb923c, #f97316, #fbbf24); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } +.logo .sub { font-size: 12px; color: var(--sub); margin-top: 2px; } +.tabs { display: flex; gap: 6px; flex-wrap: wrap; } +.tab { background: transparent; border: 1px solid var(--line); color: var(--sub); padding: 8px 14px; border-radius: 999px; cursor: pointer; font-size: 14px; transition: .2s; } +.tab:hover { color: var(--txt); border-color: var(--orange2); } +.tab.active { background: linear-gradient(135deg, #f97316, #ea580c); color: #fff; border-color: transparent; box-shadow: 0 2px 12px rgba(249,115,22,.35); } + +main { flex: 1; width: 100%; max-width: 1200px; margin: 0 auto; padding: 20px 16px 40px; } +.view { display: none; } +.view.active { display: block; } + +/* ---------- 对话 ---------- */ +.chat-wrap { display: flex; flex-direction: column; height: calc(100vh - 190px); min-height: 480px; } +.chat-list { flex: 1; overflow-y: auto; padding: 8px 4px 16px; display: flex; flex-direction: column; gap: 14px; scroll-behavior: smooth; } +.msg { display: flex; gap: 10px; max-width: 88%; } +.msg.user { align-self: flex-end; flex-direction: row-reverse; } +.avatar { width: 38px; height: 38px; border-radius: 50%; background: var(--card); border: 1px solid var(--line); display: flex; align-items: center; justify-content: center; font-size: 19px; flex-shrink: 0; } +.msg.user .avatar { background: linear-gradient(135deg, #f97316, #ea580c); border: none; } +.bubble { background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 12px 16px; font-size: 14.5px; line-height: 1.75; white-space: pre-wrap; word-break: break-word; } +.msg.user .bubble { background: #2a1c10; border-color: #7c3a1e; } +.bubble b { color: var(--orange2); } +.bubble em { color: var(--blue); font-style: normal; } +.bubble .hint { color: var(--sub); font-size: 13px; margin-top: 6px; } +.bubble ul { margin: 6px 0 6px 18px; } +.bubble li { margin: 3px 0; } +.src-tag { display: inline-block; font-size: 11px; color: var(--green); border: 1px solid #1f5c43; background: #0f2a1e; border-radius: 999px; padding: 1px 8px; margin-top: 8px; margin-right: 6px; } +.typing { display: inline-flex; gap: 4px; padding: 6px 2px; } +.typing i { width: 7px; height: 7px; border-radius: 50%; background: var(--sub); animation: blink 1.2s infinite; } +.typing i:nth-child(2) { animation-delay: .2s; } .typing i:nth-child(3) { animation-delay: .4s; } +@keyframes blink { 0%,80%,100% {opacity:.25} 40% {opacity:1} } + +.chips { display: flex; gap: 8px; flex-wrap: wrap; padding: 8px 0 10px; } +.chips button { background: var(--card); border: 1px solid var(--line); color: var(--txt); padding: 6px 12px; border-radius: 999px; font-size: 12.5px; cursor: pointer; transition: .15s; } +.chips button:hover { border-color: var(--orange2); color: var(--orange2); } + +.input-bar { display: flex; gap: 10px; background: var(--bg2); border: 1px solid var(--line); border-radius: 999px; padding: 8px 10px 8px 18px; } +#chat-input { flex: 1; background: transparent; border: none; outline: none; color: var(--txt); font-size: 15px; } +#send-btn { background: linear-gradient(135deg, #f97316, #ea580c); border: none; color: #fff; padding: 10px 24px; border-radius: 999px; cursor: pointer; font-size: 14px; font-weight: 600; } +#send-btn:disabled { opacity: .5; cursor: wait; } + +/* ---------- 通用数据区 ---------- */ +.toolbar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 16px; } +.search { background: var(--bg2); border: 1px solid var(--line); color: var(--txt); border-radius: 999px; padding: 8px 16px; font-size: 14px; outline: none; flex: 1; min-width: 180px; max-width: 360px; } +.search:focus { border-color: var(--orange2); } +.btn { background: var(--orange); border: none; color: #fff; border-radius: 999px; padding: 8px 18px; cursor: pointer; font-size: 14px; } +.btn.small { background: transparent; border: 1px solid var(--line); color: var(--sub); padding: 6px 12px; font-size: 13px; } +.btn.small.active { color: var(--orange2); border-color: var(--orange2); background: rgba(249,115,22,.08); } + +.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 14px; } +.card { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: 14px 16px; cursor: pointer; transition: .18s; } +.card:hover { transform: translateY(-3px); border-color: var(--orange2); box-shadow: 0 6px 20px rgba(0,0,0,.35); } +.card h3 { font-size: 15.5px; margin-bottom: 4px; } +.card .en { color: var(--sub); font-size: 12px; } +.card .meta { color: var(--sub); font-size: 12.5px; margin-top: 6px; line-height: 1.6; } +.card .tag { display: inline-block; background: rgba(249,115,22,.12); color: var(--orange2); border-radius: 6px; padding: 1px 7px; font-size: 11px; margin-right: 5px; } +.big-num { font-size: 17px; font-weight: 700; color: var(--orange2); } + +.list { display: flex; flex-direction: column; gap: 10px; } +.list-item { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: 13px 16px; cursor: pointer; transition: .15s; } +.list-item:hover { border-color: var(--orange2); } +.list-item h3 { font-size: 15px; } +.list-item .meta { color: var(--sub); font-size: 13px; margin-top: 3px; } + +.score-line { display: flex; align-items: center; gap: 10px; font-size: 15px; } +.score-line .vs { color: var(--sub); font-size: 12px; } +.score-line .win { color: var(--green); font-weight: 700; } +.score-line .lose { color: var(--sub); } +.score-big { font-weight: 800; font-size: 18px; color: var(--txt); margin: 0 2px; } +.status-pill { font-size: 11px; border-radius: 999px; padding: 2px 8px; } +.status-finished { color: var(--green); background: #0f2a1e; } +.status-scheduled { color: var(--blue); background: #0c2233; } + +.table-wrap { overflow-x: auto; } +table { width: 100%; border-collapse: collapse; background: var(--card); border-radius: var(--radius); overflow: hidden; } +th, td { padding: 10px 12px; text-align: left; font-size: 13.5px; border-bottom: 1px solid var(--line); } +th { background: var(--bg2); color: var(--sub); font-weight: 600; font-size: 12.5px; } +tr:hover td { background: rgba(249,115,22,.05); } +td.num { text-align: center; } + +/* ---------- 弹窗 ---------- */ +.modal { position: fixed; inset: 0; background: rgba(0,0,0,.7); display: flex; align-items: center; justify-content: center; z-index: 100; padding: 20px; } +.modal.hidden { display: none; } +.modal-box { background: var(--bg2); border: 1px solid var(--line); border-radius: 16px; max-width: 760px; width: 100%; max-height: 85vh; overflow-y: auto; padding: 26px; position: relative; } +.modal-close { position: absolute; top: 12px; right: 14px; background: none; border: none; color: var(--sub); font-size: 18px; cursor: pointer; } +.modal-close:hover { color: var(--txt); } +.modal-box h2 { font-size: 21px; margin-bottom: 2px; } +.modal-box .en { color: var(--sub); font-size: 13px; } +.kv { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 8px 14px; margin: 14px 0; } +.kv div { background: var(--card); border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px; font-size: 13px; } +.kv .k { color: var(--sub); font-size: 11.5px; display: block; } +.kv .v { font-size: 14px; font-weight: 600; } +.section-title { font-size: 14px; color: var(--orange2); margin: 16px 0 8px; font-weight: 600; } +.news-body { line-height: 1.9; font-size: 14.5px; color: #c9d4e0; } + +.footer { text-align: center; color: var(--sub); font-size: 12px; padding: 18px; border-top: 1px solid var(--line); } + +::-webkit-scrollbar { width: 8px; height: 8px; } +::-webkit-scrollbar-thumb { background: var(--line); border-radius: 4px; } diff --git a/tools.py b/tools.py new file mode 100644 index 0000000..9626a8d --- /dev/null +++ b/tools.py @@ -0,0 +1,397 @@ +# -*- coding: utf-8 -*- +""" +结构化查询工具层:供大模型函数调用(tools)与前端 REST API 共用。 +所有函数返回 JSON 友好的 dict/list,并带 _source 来源标记。 +新增数据域(如 CBA、足球)时:SQL 按 league/sport 过滤即可复用全部函数。 +""" +import re +from datetime import datetime, timedelta + +from db import query, query_one, fuzzy +import vector_store + +MAX_SHOW = 6 # 工具默认返回条数上限 + +# 绰号 → 库内正式名(查询时自动展开) +ALIASES = { + "字母哥": "扬尼斯·阿德托昆博", "希腊怪兽": "扬尼斯·阿德托昆博", + "老詹": "勒布朗·詹姆斯", "詹皇": "勒布朗·詹姆斯", "皇帝": "勒布朗·詹姆斯", + "浓眉": "安东尼·戴维斯", "SGA": "谢伊·吉尔杰斯-亚历山大", + "华子": "安东尼·爱德华兹", "文班": "维克托·文班亚马", "约老师": "尼古拉·约基奇", + "大胡子": "詹姆斯·哈登", "死神": "凯文·杜兰特", "大帝": "乔尔·恩比德", + "追梦": "德雷蒙德·格林", "獭兔": "杰森·塔图姆", "东子": "卢卡·东契奇", + "泡椒": "保罗·乔治", "小卡": "科怀·伦纳德", "卡皇": "亚历克斯·卡鲁索", + "切特": "切特·霍姆格伦", "杰威": "杰伦·威廉姆斯", +} + + +def expand_aliases(q): + """把绰号替换为正式名(长匹配优先)""" + for nick, real in sorted(ALIASES.items(), key=lambda x: -len(x[0])): + if nick in q: + q = q.replace(nick, real) + return q + + +def _split_terms(q): + """把'约基奇和字母哥'/'湖人、勇士'拆成多个关键词(绰号已展开,仅去标点)""" + q = expand_aliases(q) + terms = [t.strip() for t in re.split(r"[和与跟、,,;;vs VS 及\s]", q) if t.strip()] + terms = [re.sub(r"^[\s\-—::]+|[\s\-—::]+$", "", t) for t in terms] + seen, out = set(), [] + for t in terms: + if t and t not in seen: + seen.add(t) + out.append(t) + return out + + +def _match_by_truncation(base_sql, term, args_factory, max_drop=10): + """渐进截断匹配:整词无结果时逐个丢弃尾部字符重试(处理'字母哥谁得分多'类问句尾巴) + base_sql 需含 ? 占位;args_factory(like) 生成查询参数""" + t = term + for _ in range(max_drop + 1): + if len(t) < 2: + break + rows = query(base_sql, args_factory(f"%{fuzzy(t)}%")) + if rows: + return rows + t = t[:-1] + return [] + + +def _fmt_team(t): + return {"id": t["id"], "name": t["name"], "name_en": t["name_en"], "code": t["code"], + "city": t["city"], "arena": t["arena"], "founded": t["founded"], + "champion_count": t["champion_count"], "head_coach": t["head_coach"], "intro": t["intro"]} + + +def _fmt_player(p): + return {"id": p["id"], "name": p["name"], "name_en": p["name_en"], "team": p.get("team_name"), + "position": p["position"], "number": p["number"], "height_cm": p["height_cm"], + "weight_kg": p["weight_kg"], "country": p["country"], "draft": f"{p['draft_year']}年 第{p['draft_pick']}顺位" if p["draft_year"] else "落选秀", + "salary_m": p["salary_m"], "season": {"pts": p["season_pts"], "reb": p["season_reb"], + "ast": p["season_ast"], "stl": p["season_stl"], "blk": p["season_blk"], "min": p["season_min"]}, + "career": {"pts": p["career_pts"], "reb": p["career_reb"], "ast": p["career_ast"], "games": p["career_games"]}, + "awards": p["awards"], "bio": p["bio"]} + + +def _fmt_game(g, with_team=True): + d = {"id": g["id"], "round_name": g["round_name"], "game_time": g["game_time"], + "status": g["status"], "venue": g["venue"], "broadcast": g["broadcast"], + "home_score": g["home_score"], "away_score": g["away_score"]} + if with_team: + d["home_team"] = g["home_name"] or g["home_code"] + d["away_team"] = g["away_name"] or g["away_code"] + return d + + +_STAT_INTENT = re.compile(r"技术统计|统计|数据|谁得分|得分最高|表现|box|scorer|G\d|第.场") + + +def _attach_top_scorers(game_rows, max_games=3): + """为比赛附加双方得分前三球员(供技术统计类问题)""" + for g in game_rows[:max_games]: + gid = g["id"] + rows = query("""SELECT gs.points, p.name AS player_name, t.name AS team_name + FROM game_player_stats gs + JOIN players p ON gs.player_id=p.id JOIN teams t ON gs.team_id=t.id + WHERE gs.game_id=? ORDER BY gs.points DESC LIMIT 6""", (gid,)) + if rows: + g["top_scorers"] = [{"player": r["player_name"], "team": r["team_name"], "points": r["points"]} + for r in rows] + + +# ================================================================== 球队 +def search_teams(query_text, limit=MAX_SHOW): + q = expand_aliases((query_text or "").strip()) + if not q: + rows = query("SELECT * FROM teams ORDER BY name LIMIT ?", (limit,)) + else: + seen, out = {}, [] + sql = """SELECT * FROM teams WHERE name LIKE ? ESCAPE '\\' OR name_en LIKE ? ESCAPE '\\' + OR code LIKE ? ESCAPE '\\' OR city LIKE ? ESCAPE '\\' ORDER BY name LIMIT ?""" + for term in _split_terms(q)[:4]: + rows = _match_by_truncation(sql, term, lambda like: (like, like, like, like, limit)) + for r in rows: + if r["id"] not in seen: + seen[r["id"]] = r + out.append(r) + rows = out[:limit] + return {"_source": "teams", "results": [_fmt_team(r) for r in rows]} + + +def get_team(team_id): + t = query_one("SELECT * FROM teams WHERE id=?", (team_id,)) + if not t: + return None + return _fmt_team(t) + + +# ================================================================== 球员 +def search_players(query_text, limit=MAX_SHOW): + q = expand_aliases((query_text or "").strip()) + if not q: + rows = query("""SELECT p.*, t.name AS team_name FROM players p LEFT JOIN teams t ON p.team_id=t.id + ORDER BY p.season_pts DESC LIMIT ?""", (limit,)) + return {"_source": "players", "results": [_fmt_player(r) for r in rows]} + seen, out = {}, [] + sql = """SELECT p.*, t.name AS team_name FROM players p + LEFT JOIN teams t ON p.team_id=t.id + WHERE p.name LIKE ? ESCAPE '\\' OR p.name_en LIKE ? ESCAPE '\\' + OR t.name LIKE ? ESCAPE '\\' + ORDER BY p.season_pts DESC LIMIT ?""" + for term in _split_terms(q)[:4]: + rows = _match_by_truncation(sql, term, lambda like: (like, like, like, limit)) + for r in rows: + if r["id"] not in seen: + seen[r["id"]] = r + out.append(r) + return {"_source": "players", "results": [_fmt_player(r) for r in out[:limit]]} + + +def get_player(player_id): + p = query_one("""SELECT p.*, t.name AS team_name FROM players p LEFT JOIN teams t ON p.team_id=t.id + WHERE p.id=?""", (player_id,)) + return _fmt_player(p) if p else None + + +# ================================================================== 比赛 +_TEAM_PAT = None + + +def _match_team_ids(text): + """从文本中找出命中的球队名(支持中文/英文/缩写)""" + global _TEAM_PAT + teams = query("SELECT id, name, name_en, code FROM teams") + hits = [] + for t in teams: + names = [t["name"], t["name_en"], t["code"]] + for n in names: + if n and len(n) >= 2 and n.lower() in (text or "").lower(): + hits.append(t["id"]) + break + return hits + + +def search_games(query_text, limit=10): + q = (query_text or "").strip() + now = datetime.now() + sql = """SELECT g.*, ht.name AS home_name, ht.code AS home_code, + at.name AS away_name, at.code AS away_code + FROM games g JOIN teams ht ON g.home_team_id=ht.id JOIN teams at ON g.away_team_id=at.id""" + conds, args = [], [] + + tids = _match_team_ids(q) + if tids: + marks = ",".join("?" for _ in tids) + conds.append(f"(g.home_team_id IN ({marks}) OR g.away_team_id IN ({marks}))") + args += tids * 2 + if q and ("已结束" in q or "结束" in q or "比分" in q or "结果" in q or "谁赢" in q): + conds.append("g.status='finished'") + if q and ("未开始" in q or "即将" in q or "赛程" in q or "预告" in q): + conds.append("g.status='scheduled'") + if q and "总决赛" in q: + conds.append("g.round_name='总决赛'") + if q and "季后赛" in q: + conds.append("g.round_name LIKE '季后赛%' OR g.round_name IN ('总决赛','东部决赛','西部决赛')") + + # 时间语义 + order = "g.game_time DESC" + if "总决赛" in q or "系列赛" in q: + order = "g.game_time ASC" # 系列赛按时间正序,便于模型按场次引用 + if re.search(r"最近|最新|上一场|上一轮", q): + conds.append("g.status='finished'") + elif re.search(r"下一场|即将|赛程|预告|未来", q): + conds.append("g.status='scheduled'") + order = "g.game_time ASC" + elif re.search(r"明天|明日", q): + conds.append("date(g.game_time)=date('now','localtime','+1 day')") + elif re.search(r"今天|今日", q): + conds.append("date(g.game_time)=date('now','localtime')") + + where = ("WHERE " + " AND ".join(conds)) if conds else "" + sql += f" {where} ORDER BY {order} LIMIT ?" + args.append(limit) + rows = query(sql, args) + results = [_fmt_game(r) for r in rows] + if _STAT_INTENT.search(q): + # 若点名了具体场次(G6/第六场/日期),只保留该场并附完整得分榜 + specific = re.search(r"G(\d)|第([一二三四五六七八九十])场|(\d{1,2})月(\d{1,2})日|(\d{4}-\d{2}-\d{2})", q) + if specific: + key = (specific.group(1) or "") + kept = [] + for g in results: + gno = g["game_time"][8:10] + if key: + try: + gno = str(int(g["game_time"][8:10])) + except Exception: + gno = "" + if gno == key: + kept.append(g) + elif specific.group(5): + if g["game_time"][:10] == specific.group(5): + kept.append(g) + elif specific.group(3): + if g["game_time"][5:7] == specific.group(3) and g["game_time"][8:10] == specific.group(4): + kept.append(g) + if kept: + results = kept + _attach_top_scorers(results, max_games=len(results)) + return {"_source": "games", "results": results} + + +def get_game_detail(game_id): + g = query_one("""SELECT g.*, ht.name AS home_name, ht.code AS home_code, + at.name AS away_name, at.code AS away_code + FROM games g JOIN teams ht ON g.home_team_id=ht.id JOIN teams at ON g.away_team_id=at.id + WHERE g.id=?""", (game_id,)) + if not g: + return None + detail = _fmt_game(g) + stats = query("""SELECT gs.*, p.name AS player_name, p.position, t.name AS team_name + FROM game_player_stats gs + JOIN players p ON gs.player_id=p.id JOIN teams t ON gs.team_id=t.id + WHERE gs.game_id=? ORDER BY gs.team_id, gs.points DESC""", (game_id,)) + detail["box_score"] = stats + return detail + + +# ================================================================== 排名 +def search_standings(query_text, limit=20): + q = (query_text or "").strip() + tids = _match_team_ids(q) + sql = """SELECT s.*, t.name AS team_name, t.code + FROM standings s JOIN teams t ON s.team_id=t.id""" + conds, args = [], [] + if tids: + marks = ",".join("?" for _ in tids) + conds.append(f"s.team_id IN ({marks})") + args += tids + if "东部" in q: + conds.append("s.conference='东部'") + if "西部" in q: + conds.append("s.conference='西部'") + where = ("WHERE " + " AND ".join(conds)) if conds else "" + rows = query(f"{sql} {where} ORDER BY s.conference DESC, s.rank ASC LIMIT ?", args + [limit]) + return {"_source": "standings", "results": [ + {"team": r["team_name"], "code": r["code"], "conference": r["conference"], "rank": r["rank"], + "wins": r["wins"], "losses": r["losses"], "win_pct": round(r["win_pct"] * 100, 1)} for r in rows]} + + +# ================================================================== 新闻(SQL 关键词 + 向量语义 + 可选 rerank 融合) +def search_news(query_text, limit=5): + q = (query_text or "").strip() + seen, out = set(), [] + # 1) SQL 关键词命中(标题+正文+标签) + if q: + like = f"%{fuzzy(q)}%" + rows = query("""SELECT * FROM news WHERE kind='news' AND (title LIKE ? ESCAPE '\\' OR content LIKE ? ESCAPE '\\' + OR tags LIKE ? ESCAPE '\\') ORDER BY publish_time DESC LIMIT ?""", + (like, like, like, limit)) + for r in rows: + seen.add(r["id"]) + out.append({"id": r["id"], "title": r["title"], "content": r["content"][:220], + "publish_time": r["publish_time"], "source": r["source"], "tags": r["tags"]}) + # 2) 向量语义补充 + try: + hits = vector_store.query_vectors(q, n_results=limit * 3) + cands = [] + for h in hits: + nid = h["metadata"].get("news_id") + if nid in seen or h["metadata"].get("kind") != "news": + continue + n = query_one("SELECT * FROM news WHERE id=?", (nid,)) + if n: + cands.append({"id": nid, "title": n["title"], "content": n["content"][:220], + "publish_time": n["publish_time"], "source": n["source"], + "tags": n["tags"], "_score": h["distance"]}) + cands.sort(key=lambda x: x["_score"]) + for c in cands[: max(0, limit - len(out))]: + out.append({k: v for k, v in c.items() if k != "_score"}) + except Exception as e: + pass + return {"_source": "news", "results": out} + + +# ================================================================== 人物 +def search_persons(query_text, limit=MAX_SHOW): + q = (query_text or "").strip() + if not q: + rows = query("""SELECT p.*, t.name AS team_name FROM persons p LEFT JOIN teams t ON p.team_id=t.id + ORDER BY p.id LIMIT ?""", (limit,)) + else: + like = f"%{fuzzy(q)}%" + rows = query("""SELECT p.*, t.name AS team_name FROM persons p LEFT JOIN teams t ON p.team_id=t.id + WHERE p.name LIKE ? ESCAPE '\\' OR p.name_en LIKE ? ESCAPE '\\' + OR p.role LIKE ? ESCAPE '\\' OR p.role_cn LIKE ? ESCAPE '\\' + OR t.name LIKE ? ESCAPE '\\' OR p.bio LIKE ? ESCAPE '\\' + ORDER BY p.id LIMIT ?""", (like, like, like, like, like, like, limit)) + return {"_source": "persons", "results": [ + {"id": r["id"], "name": r["name"], "name_en": r["name_en"], "role": r["role"], + "role_cn": r["role_cn"], "title": r["title"], "team": r["team_name"], + "bio": r["bio"], "achievements": r["achievements"]} for r in rows]} + + +# ================================================================== 知识百科(纯向量检索) +def search_knowledge(query_text, limit=3): + q = (query_text or "").strip() + try: + hits = vector_store.query_vectors(q, n_results=limit) + except Exception: + return {"_source": "knowledge", "results": []} + out = [] + for h in hits: + if h["metadata"].get("kind") != "wiki": + continue + n = query_one("SELECT * FROM news WHERE id=?", (h["metadata"].get("news_id"),)) + if n: + out.append({"title": n["title"], "content": n["content"][:400]}) + return {"_source": "knowledge", "results": out} + + +# ================================================================== 工具注册表(供 LLM function calling) +TOOLS = [ + {"type": "function", "function": {"name": "search_teams", "description": "查询球队信息(名称/城市/主场/主教练/总冠军数),支持中文名、英文名、缩写", + "parameters": {"type": "object", "properties": {"query": {"type": "string", "description": "球队名或关键词,如'湖人'或'Lakers'"}, "limit": {"type": "integer", "description": "返回条数,默认6"}}, "required": ["query"]}}}, + {"type": "function", "function": {"name": "search_players", "description": "查询球员信息(球队/位置/本赛季与生涯数据/荣誉),支持中英文名或球队名", + "parameters": {"type": "object", "properties": {"query": {"type": "string", "description": "球员名或球队名,如'库里'或'Stephen Curry'"}, "limit": {"type": "integer", "description": "返回条数,默认6"}}, "required": ["query"]}}}, + {"type": "function", "function": {"name": "search_games", "description": "查询比赛信息(比分/时间/轮次),支持球队名+时间语义(最近/上一场/下一场/今天/明天),如'湖人最近比赛'、'总决赛第六场'", + "parameters": {"type": "object", "properties": {"query": {"type": "string", "description": "球队名或时间描述,如'勇士 最近'"}, "limit": {"type": "integer", "description": "返回条数,默认10"}}, "required": ["query"]}}}, + {"type": "function", "function": {"name": "get_game_detail", "description": "获取单场比赛详情及双方球员技术统计(得分/篮板/助攻等),参数为比赛ID", + "parameters": {"type": "object", "properties": {"game_id": {"type": "integer", "description": "比赛ID(先调用search_games获得)"}}, "required": ["game_id"]}}}, + {"type": "function", "function": {"name": "search_standings", "description": "查询球队排名(东西部/胜场/胜率),如'西部排名'或'湖人战绩'", + "parameters": {"type": "object", "properties": {"query": {"type": "string", "description": "球队名或'西部'/'东部'"}}, "required": ["query"]}}}, + {"type": "function", "function": {"name": "search_news", "description": "查询新闻资讯(交易/伤病/奖项/动态),支持关键词或语义描述", + "parameters": {"type": "object", "properties": {"query": {"type": "string", "description": "新闻关键词或话题,如'选秀'、'詹姆斯续约'"}, "limit": {"type": "integer", "description": "返回条数,默认5"}}, "required": ["query"]}}}, + {"type": "function", "function": {"name": "search_persons", "description": "查询篮球相关人物(教练/经纪人/评论员/主持人/总经理/传奇),如'波波维奇'、'杨毅'", + "parameters": {"type": "object", "properties": {"query": {"type": "string", "description": "人名或角色,如'勇士主教练'、'评论员'"}, "limit": {"type": "integer", "description": "返回条数,默认6"}}, "required": ["query"]}}}, + {"type": "function", "function": {"name": "search_knowledge", "description": "查询NBA知识百科(历史/规则/制度/纪录等),如'工资帽'、'选秀制度'、'三分球历史'", + "parameters": {"type": "object", "properties": {"query": {"type": "string", "description": "知识话题描述"}, "limit": {"type": "integer", "description": "返回条数,默认3"}}, "required": ["query"]}}}, +] + +TOOL_HANDLERS = { + "search_teams": search_teams, + "search_players": search_players, + "search_games": search_games, + "get_game_detail": get_game_detail, + "search_standings": search_standings, + "search_news": search_news, + "search_persons": search_persons, + "search_knowledge": search_knowledge, +} + + +def run_tool(name, args_dict): + """执行工具调用(统一异常兜底,schema参数名→函数参数名映射)""" + try: + fn = TOOL_HANDLERS.get(name) + if not fn: + return {"_source": "error", "results": [], "error": f"未知工具 {name}"} + args = dict(args_dict or {}) + if "query" in args and "query_text" in fn.__code__.co_varnames: + args["query_text"] = args.pop("query") + return fn(**args) + except Exception as e: + return {"_source": "error", "results": [], "error": f"工具执行失败: {e}"} diff --git a/vector_store.py b/vector_store.py new file mode 100644 index 0000000..f8e57b2 --- /dev/null +++ b/vector_store.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +""" +向量检索层:Embedding(16011) + Chroma(16010) 纯 REST 实现,无第三方客户端依赖。 +- embedding:OpenAI 兼容 /v1/embeddings(bge-large-zh-v1.5,1024维) +- rerank :Cohere 兼容 /v1/rerank(bge-reranker-v2-m3,可选增强) +- chroma :REST /api/v2(add/query 用 UUID,get/delete 用名字) + +扩展其他球类/联赛时:向量索引按 collection 隔离(nba_fan_knowledge_v1 / cba_... ),互不影响。 +""" +import json +import logging +import threading +import urllib.request +import urllib.error + +import requests + +from config import (CHROMA_HOST, CHROMA_PORT, CHROMA_COLLECTION, + EMBEDDING_API_URL, EMBEDDING_MODEL, USE_RERANK, + RERANK_API_URL, RERANK_MODEL) + +log = logging.getLogger("vector") + +_BASE = f"http://{CHROMA_HOST}:{CHROMA_PORT}/api/v2/tenants/default_tenant/databases/default_database/collections" +_lock = threading.Lock() + + +# ------------------------------------------------------------------ Embedding +def embed_texts(texts): + """返回 [[float...], ...] 向量列表""" + if isinstance(texts, str): + texts = [texts] + resp = requests.post(EMBEDDING_API_URL, json={"model": EMBEDDING_MODEL, "input": list(texts)}, timeout=120) + resp.raise_for_status() + data = resp.json().get("data", []) + return [d["embedding"] for d in sorted(data, key=lambda x: x["index"])] + + +def rerank(query, docs, top_k=5): + """docs: [{"id":..,"text":..}] → 按分数降序返回 top_k(带 score)""" + if not USE_RERANK or not docs: + return docs + try: + payload = {"query": query, "documents": docs, "model": RERANK_MODEL, "top_k": top_k} + resp = requests.post(RERANK_API_URL, json=payload, timeout=60) + resp.raise_for_status() + data = resp.json().get("data", []) + return [{"id": d["document"]["id"], "text": d["document"]["text"], "score": d["score"]} for d in data] + except Exception as e: # rerank 失败不阻断主流程 + log.warning("rerank failed: %s", e) + return docs + + +# ------------------------------------------------------------------ Chroma +def _http(method, url, payload=None, timeout=30): + req = urllib.request.Request(url, method=method) + if payload is not None: + req.add_header("Content-Type", "application/json") + req.data = json.dumps(payload).encode("utf-8") + try: + with urllib.request.urlopen(req, timeout=timeout) as r: + body = r.read().decode("utf-8") + return r.status, (json.loads(body) if body else {}) + except urllib.error.HTTPError as e: + body = e.read().decode("utf-8", "ignore") + raise RuntimeError(f"Chroma {method} {url} -> {e.code}: {body[:300]}") + + +def _get_collection_id(name): + """按名字查集合,返回 (id, 是否存在)""" + _, data = _http("GET", f"{_BASE}/{name}") + return data.get("id"), True + + +def ensure_collection(name=CHROMA_COLLECTION, space="cosine"): + """获取或创建集合,返回 collection_id""" + with _lock: + try: + cid, _ = _get_collection_id(name) + return cid + except RuntimeError: + pass + _, data = _http("POST", _BASE, { + "name": name, "configuration": {"hnsw": {"space": space}}, "get_or_create": True, + }) + return data["id"] + + +def collection_count(name=CHROMA_COLLECTION): + try: + cid, _ = _get_collection_id(name) + _, data = _http("GET", f"{_BASE}/{cid}/count") + return int(data) if isinstance(data, int) else int(data.get("count", 0)) + except Exception: + return 0 + + +def add_documents(ids, documents, metadatas, name=CHROMA_COLLECTION): + """按文档批量写入(内部自动 embedding)""" + if not ids: + return + cid = ensure_collection(name) + vectors = embed_texts(documents) # 批量计算向量 + payload = {"ids": list(ids), "embeddings": vectors, + "documents": list(documents), "metadatas": list(metadatas)} + _http("POST", f"{_BASE}/{cid}/add", payload, timeout=120) + + +def query_vectors(query_text, n_results=5, where=None, name=CHROMA_COLLECTION): + """语义检索:返回 [{id, document, distance, metadata}, ...](升序按相似度)""" + try: + cid, _ = _get_collection_id(name) + except RuntimeError: + return [] + vec = embed_texts(query_text)[0] + payload = {"query_embeddings": [vec], "n_results": n_results, + "include": ["documents", "metadatas", "distances"]} + if where: + payload["where"] = where + _, data = _http("POST", f"{_BASE}/{cid}/query", payload) + out = [] + for i, doc in enumerate(data.get("documents", [[]])[0]): + out.append({ + "id": data["ids"][0][i], + "document": doc, + "distance": data["distances"][0][i], + "metadata": data["metadatas"][0][i] if data.get("metadatas") else {}, + }) + return out + + +def reset_collection(name=CHROMA_COLLECTION): + """重建集合(清空全部数据),用于重新灌库""" + try: + _http("DELETE", f"{_BASE}/{name}") + except RuntimeError: + pass + return ensure_collection(name)