commit e11677809e3711c6c54d4a74368284b1dad6f99c Author: hz4th_coder Date: Sun Aug 16 23:59:14 2026 +0800 NBA球迷大全 v1.0.0:对话问答+数据浏览系统(DeepSeek+RAG+SQLite) 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)