Initial commit: 网页联机狼人杀(FastAPI+WebSocket 后端 / React+TS+Sass 前端)
This commit is contained in:
commit
7f5ea6bee8
55 files changed
+6999
No files matched your search
+15
@@ -0,0 +1,15 @@
|
|||||||
|
# ZCode workspace data
|
||||||
|
.zcode/
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
backend/.venv/
|
||||||
|
|
||||||
|
# 真实配置含密钥,只提交 config.example.json
|
||||||
|
config.json
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
frontend/node_modules/
|
||||||
|
frontend/dist/
|
||||||
|
frontend/tsconfig.tsbuildinfo
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
# 狼人杀 · Werewolf
|
||||||
|
|
||||||
|
一个 **网页联机狼人杀框架**:FastAPI + WebSocket 后端,React + TypeScript + Sass 前端,
|
||||||
|
支持 **真人玩家与 AI 玩家混合对局**,人数与板子可选,多人从不同浏览器/设备加入同一房间。
|
||||||
|
|
||||||
|
  
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- **人机混战**:真人玩家占座后,空位一键补满 AI;也可以全部由 AI 对战观战
|
||||||
|
- **真正的多人联机**:房间码制,局域网/公网多设备加入,断线自动重连不掉座
|
||||||
|
- **经典板子**:
|
||||||
|
- 经典 12 人局(4 狼 4 民 预言家/女巫/猎人/守卫)
|
||||||
|
- 标准 10 人局(3 狼 3 民 预女猎守)
|
||||||
|
- 快节奏 9 人局(3 狼 3 民 预女猎)
|
||||||
|
- **完整规则**:守卫守护(不可连守)→ 狼人刀人 → 预言家验人 → 女巫双药(首夜可自救、同守同救会死)
|
||||||
|
→ 警长竞选(警长 1.5 票、警徽流转、可撕徽)→ 白天发言 → 投票放逐 → 猎人开枪 → 屠边/屠城胜负判定
|
||||||
|
- **AI 玩家(LLM 驱动)**:接入任意 OpenAI 兼容大模型(DeepSeek / 智谱 / Kimi / OpenAI / 本地 Ollama …),
|
||||||
|
AI 有人设性格、第一人称发言、夜间决策与投票;**未配置密钥时自动回退内置启发式 AI,开箱即玩**
|
||||||
|
- **网页界面**:座位圈、阶段横幅与倒计时、个人私密信息面板(身份/狼队友/查验/药水)、公开+私密对局记录、
|
||||||
|
按阶段切换的行动面板、结算复盘弹窗
|
||||||
|
- **亮/暗主题**:默认清爽浅色(白底 + 靛蓝主色),右上角 🌙/☀️ 一键切换暗色(深蓝灰),选择自动记忆
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 环境要求
|
||||||
|
|
||||||
|
- [uv](https://docs.astral.sh/uv/)(管理 Python 后端环境,会自动安装 Python 解释器)
|
||||||
|
- [pnpm](https://pnpm.io/) + Node.js 18+(管理前端)
|
||||||
|
|
||||||
|
### 方式一:单进程游玩(推荐)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 构建前端
|
||||||
|
cd frontend
|
||||||
|
pnpm install
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# 2. 启动后端(自动托管前端构建产物)
|
||||||
|
cd ../backend
|
||||||
|
uv sync
|
||||||
|
uv run python run.py
|
||||||
|
```
|
||||||
|
|
||||||
|
打开 **http://127.0.0.1:8000** —— 填昵称、创建房间、把房间号发给朋友,
|
||||||
|
各自浏览器打开后输入房间号加入,空位补 AI,开始游戏。
|
||||||
|
|
||||||
|
> 局域网联机:同一 WiFi 下的朋友访问 `http://<你的IP>:8000` 即可。
|
||||||
|
|
||||||
|
### 方式二:前端开发模式(热更新)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 终端 1:后端
|
||||||
|
cd backend && uv sync && uv run uvicorn app.main:app --port 8000
|
||||||
|
|
||||||
|
# 终端 2:前端(Vite 会把 /api 与 /ws 代理到 8000 端口)
|
||||||
|
cd frontend && pnpm install && pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
打开 http://127.0.0.1:5173。
|
||||||
|
|
||||||
|
## 接入大模型 AI(可选)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
cp config.example.json config.json # Windows: copy config.example.json config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
编辑 `config.json`,填入任意 OpenAI 兼容服务:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"llm": {
|
||||||
|
"base_url": "https://api.deepseek.com/v1",
|
||||||
|
"api_key": "sk-xxxx",
|
||||||
|
"model": "deepseek-chat",
|
||||||
|
"temperature": 0.8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
常见服务商的 `base_url`:
|
||||||
|
|
||||||
|
| 服务商 | base_url | 模型示例 |
|
||||||
|
|---|---|---|
|
||||||
|
| DeepSeek | `https://api.deepseek.com/v1` | `deepseek-chat` |
|
||||||
|
| 智谱 GLM | `https://open.bigmodel.cn/api/paas/v4` | `glm-4-flash` |
|
||||||
|
| Moonshot Kimi | `https://api.moonshot.cn/v1` | `moonshot-v1-8k` |
|
||||||
|
| OpenAI | `https://api.openai.com/v1` | `gpt-4o-mini` |
|
||||||
|
| 本地 Ollama | `http://localhost:11434/v1` | `qwen2.5:7b` |
|
||||||
|
|
||||||
|
- 也可用环境变量覆盖:`WEREWOLF_LLM_BASE_URL` / `WEREWOLF_LLM_API_KEY` / `WEREWOLF_LLM_MODEL`
|
||||||
|
- **不配置或配置错误都不会影响游戏**:AI 自动回退到内置启发式逻辑(调用失败也会单次降级,不会卡局)
|
||||||
|
- 大厅页底部会显示当前 AI 状态(大模型已接入 / 启发式 AI)
|
||||||
|
|
||||||
|
## 房间操作说明
|
||||||
|
|
||||||
|
| 操作 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| 创建房间 | 大厅点击「创建房间」生成 4 位房间码,创建者成为房主 |
|
||||||
|
| 加入房间 | 输入房间号加入,自动分配空座位;对局中进入则为观战 |
|
||||||
|
| 补 AI | 房主可「+ 添加 AI」逐个加,或「⚡ 一键补满 AI」 |
|
||||||
|
| 开始游戏 | 座位满员后房主点击「开始游戏」,私发身份 |
|
||||||
|
| 断线重连 | 刷新/断网后重新进入同一房间自动回到原座位(对局中为观战) |
|
||||||
|
| 再来一局 | 结算后房主点击「回到房间」,全员身份重置回大厅 |
|
||||||
|
|
||||||
|
## 游戏规则细节
|
||||||
|
|
||||||
|
- **夜晚顺序**:守卫 → 狼人(狼队互相可见,先选的刀口生效)→ 预言家 → 女巫
|
||||||
|
- **女巫**:解药/毒药各一瓶,每夜限用一瓶;首夜可自救;同守同救会死
|
||||||
|
- **警长**:首日竞选,警长票计 1.5 票;警长死亡可移交警徽或撕徽
|
||||||
|
- **猎人**:被刀或被放逐时可开枪,被毒杀不能开枪
|
||||||
|
- **放逐**:平票无人出局,不翻牌
|
||||||
|
- **胜负**:默认屠边(狼人杀光全部神职或全部村民即获胜),房主可切换屠城(狼数 ≥ 好人数)
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
├── backend/ # uv 管理的 FastAPI 后端
|
||||||
|
│ ├── pyproject.toml
|
||||||
|
│ ├── config.example.json # 复制为 config.json 接入大模型
|
||||||
|
│ ├── run.py # 启动入口
|
||||||
|
│ ├── smoke_test.py # 纯 AI 完整对局冒烟测试
|
||||||
|
│ ├── ws_test.py # 双真人 + AI 无头联机测试
|
||||||
|
│ ├── side_player.py # 加入指定房间自动玩的随从客户端
|
||||||
|
│ └── app/
|
||||||
|
│ ├── main.py # REST + WebSocket + 静态托管
|
||||||
|
│ ├── config.py # 配置加载
|
||||||
|
│ └── game/
|
||||||
|
│ ├── roles.py # 角色定义
|
||||||
|
│ ├── board.py # 板子预设
|
||||||
|
│ ├── player.py # 玩家实体
|
||||||
|
│ ├── room.py # 房间/连接管理/快照广播
|
||||||
|
│ ├── engine.py # 昼夜状态机引擎
|
||||||
|
│ └── ai/ # LLM 客户端、提示词、Agent、启发式兜底
|
||||||
|
└── frontend/ # pnpm + Vite + React + TS + Sass 前端
|
||||||
|
├── vite.config.ts # dev 代理 /api /ws
|
||||||
|
└── src/
|
||||||
|
├── pages/ # Lobby 大厅 / Room 房间
|
||||||
|
├── components/ # 座位圈/阶段横幅/行动面板/信息面板/日志/结算
|
||||||
|
├── api/ws.ts # WebSocket 封装(自动重连)
|
||||||
|
├── types.ts # 与后端对齐的消息协议类型
|
||||||
|
└── styles/ # Sass 主题变量与全局样式
|
||||||
|
```
|
||||||
|
|
||||||
|
## 开发测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
uv run python smoke_test.py # 12 个内置 AI 完整对局
|
||||||
|
uv run python ws_test.py # 2 个程序化真人 + AI,覆盖建房/重连/重置
|
||||||
|
uv run python side_player.py 房间号 # 让一个自动玩家加入你的房间
|
||||||
|
```
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
3.12
|
||||||
Whitespace-only changes.
@@ -0,0 +1,67 @@
|
|||||||
|
"""配置加载:backend/config.json(可从 config.example.json 复制修改)。
|
||||||
|
|
||||||
|
未配置 api_key 时,AI 玩家自动回退为内置启发式 AI,游戏依然完整可玩。
|
||||||
|
支持环境变量覆盖:WEREWOLF_LLM_BASE_URL / WEREWOLF_LLM_API_KEY / WEREWOLF_LLM_MODEL。
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
BACKEND_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
CONFIG_PATH = BACKEND_DIR / "config.json"
|
||||||
|
EXAMPLE_PATH = BACKEND_DIR / "config.example.json"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LLMConfig:
|
||||||
|
base_url: str = ""
|
||||||
|
api_key: str = ""
|
||||||
|
model: str = ""
|
||||||
|
temperature: float = 0.8
|
||||||
|
|
||||||
|
@property
|
||||||
|
def configured(self) -> bool:
|
||||||
|
return bool(self.base_url and self.api_key and self.model)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ServerConfig:
|
||||||
|
host: str = "0.0.0.0"
|
||||||
|
port: int = 8000
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AppConfig:
|
||||||
|
llm: LLMConfig = field(default_factory=LLMConfig)
|
||||||
|
server: ServerConfig = field(default_factory=ServerConfig)
|
||||||
|
|
||||||
|
|
||||||
|
def load_config() -> AppConfig:
|
||||||
|
cfg = AppConfig()
|
||||||
|
source = CONFIG_PATH
|
||||||
|
if not source.exists() and EXAMPLE_PATH.exists():
|
||||||
|
source = EXAMPLE_PATH
|
||||||
|
if source.exists():
|
||||||
|
try:
|
||||||
|
raw = json.loads(source.read_text(encoding="utf-8"))
|
||||||
|
except (json.JSONDecodeError, OSError):
|
||||||
|
raw = {}
|
||||||
|
llm = raw.get("llm", {})
|
||||||
|
cfg.llm.base_url = llm.get("base_url", cfg.llm.base_url)
|
||||||
|
cfg.llm.api_key = llm.get("api_key", cfg.llm.api_key)
|
||||||
|
cfg.llm.model = llm.get("model", cfg.llm.model)
|
||||||
|
cfg.llm.temperature = float(llm.get("temperature", cfg.llm.temperature))
|
||||||
|
srv = raw.get("server", {})
|
||||||
|
cfg.server.host = srv.get("host", cfg.server.host)
|
||||||
|
cfg.server.port = int(srv.get("port", cfg.server.port))
|
||||||
|
|
||||||
|
cfg.llm.base_url = os.environ.get("WEREWOLF_LLM_BASE_URL", cfg.llm.base_url).strip()
|
||||||
|
cfg.llm.api_key = os.environ.get("WEREWOLF_LLM_API_KEY", cfg.llm.api_key).strip()
|
||||||
|
cfg.llm.model = os.environ.get("WEREWOLF_LLM_MODEL", cfg.llm.model).strip()
|
||||||
|
return cfg
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG = load_config()
|
||||||
Whitespace-only changes.
@@ -0,0 +1,4 @@
|
|||||||
|
from .agent import LLMAgent, make_agent
|
||||||
|
from .fallback import HeuristicAgent
|
||||||
|
|
||||||
|
__all__ = ["LLMAgent", "HeuristicAgent", "make_agent"]
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"""AI 玩家代理:优先 LLM,失败自动降级到启发式。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .fallback import HeuristicAgent
|
||||||
|
|
||||||
|
|
||||||
|
class LLMAgent:
|
||||||
|
def __init__(self, player) -> None:
|
||||||
|
self.player = player
|
||||||
|
self.fallback = HeuristicAgent(player)
|
||||||
|
from .llm_client import get_llm_client
|
||||||
|
self.client = get_llm_client()
|
||||||
|
|
||||||
|
async def decide(self, kind: str, ctx: dict):
|
||||||
|
try:
|
||||||
|
from .prompts import decision_messages
|
||||||
|
system, user = decision_messages(kind, ctx)
|
||||||
|
data = await self.client.chat_json(system, user)
|
||||||
|
target = data.get("target")
|
||||||
|
if target is None:
|
||||||
|
return None
|
||||||
|
target = int(target)
|
||||||
|
if ctx["options"] and target not in ctx["options"]:
|
||||||
|
return await self.fallback.decide(kind, ctx)
|
||||||
|
return target
|
||||||
|
except Exception:
|
||||||
|
return await self.fallback.decide(kind, ctx)
|
||||||
|
|
||||||
|
async def speak(self, ctx: dict) -> str:
|
||||||
|
try:
|
||||||
|
from .prompts import speech_messages
|
||||||
|
system, user = speech_messages(ctx)
|
||||||
|
text = await self.client.chat_text(system, user, timeout=40.0)
|
||||||
|
text = text.strip().strip('"').strip()
|
||||||
|
if not text:
|
||||||
|
return await self.fallback.speak(ctx)
|
||||||
|
return text[:300]
|
||||||
|
except Exception:
|
||||||
|
return await self.fallback.speak(ctx)
|
||||||
|
|
||||||
|
|
||||||
|
def make_agent(player):
|
||||||
|
from ...config import CONFIG
|
||||||
|
if CONFIG.llm.configured:
|
||||||
|
return LLMAgent(player)
|
||||||
|
return HeuristicAgent(player)
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
"""启发式兜底 AI:无 LLM 配置(或 LLM 调用失败)时保证游戏可玩。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import random
|
||||||
|
|
||||||
|
|
||||||
|
class HeuristicAgent:
|
||||||
|
def __init__(self, player) -> None:
|
||||||
|
self.player = player
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
async def decide(self, kind: str, ctx: dict):
|
||||||
|
handler = getattr(self, f"_decide_{kind}", None)
|
||||||
|
if handler is None:
|
||||||
|
return None
|
||||||
|
return handler(ctx)
|
||||||
|
|
||||||
|
async def speak(self, ctx: dict) -> str:
|
||||||
|
return self._speak(ctx)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ 内部
|
||||||
|
def _alive_others(self, ctx: dict) -> list[int]:
|
||||||
|
me = ctx["seat"]
|
||||||
|
return [p["seat"] for p in ctx["alive"] if p["seat"] != me]
|
||||||
|
|
||||||
|
def _claim_targets(self, ctx: dict) -> list[int]:
|
||||||
|
claimed = [s for s in ctx.get("claimed_seers", [])]
|
||||||
|
me = self.player
|
||||||
|
mates = set(ctx.get("teammates", []))
|
||||||
|
return [s for s in claimed
|
||||||
|
if s != me.seat and s not in mates]
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ 决策
|
||||||
|
def _decide_kill(self, ctx: dict):
|
||||||
|
victims = [o for o in ctx["options"]]
|
||||||
|
claims = self._claim_targets(ctx)
|
||||||
|
pool = [s for s in claims if s in victims]
|
||||||
|
if pool:
|
||||||
|
return random.choice(pool)
|
||||||
|
return random.choice(victims) if victims else None
|
||||||
|
|
||||||
|
def _decide_protect(self, ctx: dict):
|
||||||
|
claims = [s for s in self._claim_targets(ctx) if s in ctx["options"]]
|
||||||
|
if claims:
|
||||||
|
return random.choice(claims)
|
||||||
|
return random.choice(ctx["options"]) if ctx["options"] else None
|
||||||
|
|
||||||
|
def _decide_check(self, ctx: dict):
|
||||||
|
return random.choice(ctx["options"]) if ctx["options"] else None
|
||||||
|
|
||||||
|
def _decide_witch_save(self, ctx: dict):
|
||||||
|
target = ctx["options"][0] if ctx["options"] else None
|
||||||
|
if target is None:
|
||||||
|
return None
|
||||||
|
if target == ctx["seat"]:
|
||||||
|
return target # 首夜自救
|
||||||
|
if target in ctx.get("claimed_seers", []):
|
||||||
|
return target
|
||||||
|
return None # 留药
|
||||||
|
|
||||||
|
def _decide_witch_poison(self, ctx: dict):
|
||||||
|
return None # 保守:不毒
|
||||||
|
|
||||||
|
def _decide_shoot(self, ctx: dict):
|
||||||
|
return random.choice(ctx["options"]) if ctx["options"] else None
|
||||||
|
|
||||||
|
def _decide_elect(self, ctx: dict):
|
||||||
|
if ctx.get("role") == "seer":
|
||||||
|
return 1
|
||||||
|
if ctx.get("role") == "werewolf":
|
||||||
|
return 1 if random.random() < 0.3 else None
|
||||||
|
return 1 if random.random() < 0.15 else None
|
||||||
|
|
||||||
|
def _decide_sheriff_vote(self, ctx: dict):
|
||||||
|
return random.choice(ctx["options"]) if ctx["options"] else None
|
||||||
|
|
||||||
|
def _decide_badge(self, ctx: dict):
|
||||||
|
claims = [s for s in ctx.get("claimed_seers", []) if s in ctx["options"]]
|
||||||
|
if claims:
|
||||||
|
return random.choice(claims)
|
||||||
|
return random.choice(ctx["options"]) if ctx["options"] else None
|
||||||
|
|
||||||
|
def _decide_vote(self, ctx: dict):
|
||||||
|
claims = [s for s in self._claim_targets(ctx) if s in ctx["options"]]
|
||||||
|
if claims and ctx.get("role") != "werewolf":
|
||||||
|
# 好人跟跳预言家的对跳/划水者走:简单起见投被多疑的目标
|
||||||
|
pass
|
||||||
|
pool = [o for o in ctx["options"] if o != ctx["seat"]]
|
||||||
|
if not pool:
|
||||||
|
return None
|
||||||
|
if ctx.get("role") == "werewolf":
|
||||||
|
mates = set(ctx.get("teammates", []))
|
||||||
|
pool = [o for o in pool if o not in mates] or pool
|
||||||
|
return random.choice(pool)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ 发言
|
||||||
|
def _speak(self, ctx: dict) -> str:
|
||||||
|
role = ctx.get("role")
|
||||||
|
day = ctx.get("day", 1)
|
||||||
|
purpose = ctx.get("purpose")
|
||||||
|
seat = ctx["seat"]
|
||||||
|
alive = [p["seat"] for p in ctx["alive"] if p["seat"] != seat]
|
||||||
|
pick = random.choice(alive) if alive else None
|
||||||
|
if purpose == "election":
|
||||||
|
if role == "seer":
|
||||||
|
return "我是预言家,想拿警徽带队报查验,请大家支持我。"
|
||||||
|
return f"我{seat}号想为团队做点贡献,上警听听大家的发言。"
|
||||||
|
if role == "seer":
|
||||||
|
results = ";".join(
|
||||||
|
f"{r['seat']}号是{r['result']}" for r in ctx.get("seer_results", [])[-2:]) or "暂时没有信息"
|
||||||
|
return f"我是预言家,我的查验:{results}。请好人记住我的票。"
|
||||||
|
if role == "werewolf":
|
||||||
|
lines = [
|
||||||
|
f"我是个好人,昨天{pick}号的发言我觉得有点飘,大家可以听听。",
|
||||||
|
"我没有太多信息,先听后置位怎么说。",
|
||||||
|
f"这局信息很少,我倾向于{pick}号有问题,先投为敬。",
|
||||||
|
]
|
||||||
|
return random.choice(lines)
|
||||||
|
if role == "witch":
|
||||||
|
return "我的身份信息暂时不方便透露,从发言上看我觉得节奏有点乱,大家稳住。"
|
||||||
|
if role == "hunter":
|
||||||
|
return f"我是普通村民视角,{pick}号的发言我听不太懂,保留怀疑。"
|
||||||
|
if role == "guard":
|
||||||
|
return "我从守卫……呃,从好人视角看,这局要小心悍跳,先听大家的。"
|
||||||
|
if day == 1:
|
||||||
|
return f"我是村民,没有什么信息,{pick}号的发言给我感觉还行,先观察。"
|
||||||
|
return f"听到现在,我觉得{pick}号的发言前后有点矛盾,今天我可能跟票他。"
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
"""OpenAI 兼容异步客户端:JSON 决策与自由文本发言,自动降级重试。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
|
from ...config import CONFIG, LLMConfig
|
||||||
|
|
||||||
|
|
||||||
|
def parse_json_obj(text: str) -> dict | None:
|
||||||
|
if not text:
|
||||||
|
return None
|
||||||
|
text = text.strip()
|
||||||
|
text = re.sub(r"^```(?:json)?\s*|\s*```$", "", text, flags=re.S)
|
||||||
|
start, end = text.find("{"), text.rfind("}")
|
||||||
|
if start == -1 or end <= start:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
data = json.loads(text[start:end + 1])
|
||||||
|
return data if isinstance(data, dict) else None
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class LLMClient:
|
||||||
|
def __init__(self, cfg: LLMConfig) -> None:
|
||||||
|
from openai import AsyncOpenAI
|
||||||
|
self.model = cfg.model
|
||||||
|
self.temperature = cfg.temperature
|
||||||
|
self._client = AsyncOpenAI(
|
||||||
|
base_url=cfg.base_url, api_key=cfg.api_key, timeout=30, max_retries=0)
|
||||||
|
|
||||||
|
async def _create(self, system: str, user: str, *, json_mode: bool, timeout: float) -> str:
|
||||||
|
kwargs: dict = {
|
||||||
|
"model": self.model,
|
||||||
|
"temperature": self.temperature,
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": system},
|
||||||
|
{"role": "user", "content": user},
|
||||||
|
],
|
||||||
|
"timeout": timeout,
|
||||||
|
}
|
||||||
|
if json_mode:
|
||||||
|
kwargs["response_format"] = {"type": "json_object"}
|
||||||
|
try:
|
||||||
|
resp = await self._client.chat.completions.create(**kwargs)
|
||||||
|
return resp.choices[0].message.content or ""
|
||||||
|
except Exception:
|
||||||
|
kwargs.pop("response_format", None) # 部分兼容端点不支持 response_format
|
||||||
|
resp = await self._client.chat.completions.create(**kwargs)
|
||||||
|
return resp.choices[0].message.content or ""
|
||||||
|
|
||||||
|
async def chat_json(self, system: str, user: str, *, timeout: float = 22.0) -> dict:
|
||||||
|
last = ""
|
||||||
|
for _ in range(2):
|
||||||
|
text = await self._create(system, user, json_mode=True, timeout=timeout)
|
||||||
|
data = parse_json_obj(text)
|
||||||
|
if data is not None:
|
||||||
|
return data
|
||||||
|
last = text
|
||||||
|
raise ValueError(f"LLM 未返回有效 JSON: {last[:120]}")
|
||||||
|
|
||||||
|
async def chat_text(self, system: str, user: str, *, timeout: float = 40.0) -> str:
|
||||||
|
return (await self._create(system, user, json_mode=False, timeout=timeout)).strip()
|
||||||
|
|
||||||
|
|
||||||
|
_client: LLMClient | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_llm_client() -> LLMClient:
|
||||||
|
global _client
|
||||||
|
if _client is None:
|
||||||
|
_client = LLMClient(CONFIG.llm)
|
||||||
|
return _client
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
"""AI 玩家提示词:身份人设、决策 JSON、公开发言。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
RULES = """【狼人杀规则简述】
|
||||||
|
- 夜晚顺序:守卫守护 → 狼人袭击 → 预言家查验 → 女巫用药(解药/毒药各一瓶,每夜限一瓶,首夜可自救)。
|
||||||
|
- 守卫不能连续两晚守同一人;「同守同救」(守卫和女巫同时保护被袭者)会导致该玩家死亡。
|
||||||
|
- 白天:警长竞选(首日)→ 轮流发言 → 全员投票放逐(平票无人出局,警长票计1.5票)。
|
||||||
|
- 猎人被投票或被狼人杀死时可开枪带走一人(被毒杀不能开枪)。
|
||||||
|
- 胜负:狼人杀光所有神职或所有村民(屠边)即获胜;好人放逐所有狼人则获胜。"""
|
||||||
|
|
||||||
|
KIND_QUESTIONS = {
|
||||||
|
"kill": ("作为狼人,选择今晚袭击的目标",
|
||||||
|
"通常优先袭击跳预言家的玩家、发言逻辑威胁大的玩家;也要考虑票型,避开会被怀疑的队友策略。"),
|
||||||
|
"protect": ("作为守卫,选择今晚守护的玩家",
|
||||||
|
"通常守护跳预言家的玩家或你认为重要的神职;也可以空守。"),
|
||||||
|
"check": ("作为预言家,选择今晚查验的玩家",
|
||||||
|
"优先验身份模糊、立场可疑的玩家,积累信息。"),
|
||||||
|
"witch_save": ("作为女巫,决定是否使用解药救人",
|
||||||
|
"如果被袭者是跳预言家的关键角色或你自己(首夜),通常值得救;也要提防狼人骗药。"),
|
||||||
|
"witch_poison": ("作为女巫,决定是否使用毒药毒杀一人",
|
||||||
|
"只有在有较强把握某人是狼时才用毒,误毒好人会严重伤害好人阵营;可以不用。"),
|
||||||
|
"shoot": ("作为猎人,选择开枪带走的目标",
|
||||||
|
"带走你认为最像狼的玩家;没把握也可以不开枪。"),
|
||||||
|
"elect": ("决定是否上警竞选警长",
|
||||||
|
"跳预言家通常应该上警来报查验;狼人可以选择不上警隐藏身份或悍跳预言家。"),
|
||||||
|
"sheriff_vote": ("投票选出警长", "把票投给你认为能带领好人、发言可信的候选人。"),
|
||||||
|
"badge": ("作为死亡的警长,决定警徽流向",
|
||||||
|
"通常把警徽交给最信任的好人(比如你验过的好人);也可以撕毁警徽表达态度。"),
|
||||||
|
"vote": ("投票放逐一名玩家", "根据发言、逻辑和票型投出你最怀疑的狼人;没把握可以弃票。"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _state_text(ctx: dict) -> str:
|
||||||
|
alive = "、".join(f"{p['seat']}号{p['name']}" for p in ctx["alive"])
|
||||||
|
priv = "\n".join(ctx["private"]) if ctx["private"] else "无"
|
||||||
|
return (
|
||||||
|
f"【当前局面】第{ctx['day']}天,{ctx['phase_text']}\n"
|
||||||
|
f"【存活玩家】{alive}\n"
|
||||||
|
f"【你的私有信息】\n{priv}\n"
|
||||||
|
f"【对局记录(公开部分,最近在前为旧)】\n{ctx['log_text'] or '(暂无)'}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def decision_messages(kind: str, ctx: dict) -> tuple[str, str]:
|
||||||
|
title, tip = KIND_QUESTIONS.get(kind, (kind, ""))
|
||||||
|
options = ctx["options"]
|
||||||
|
opts_text = "、".join(str(o) + "号" for o in options) if options else "无"
|
||||||
|
allow_skip = kind in ("witch_save", "witch_poison", "shoot", "elect", "badge", "vote", "protect", "kill")
|
||||||
|
system = (
|
||||||
|
f"你正在玩一局狼人杀。你的名字是「{ctx['name']}」,座位号是{ctx['seat']}号。\n"
|
||||||
|
f"你的身份:{ctx['role_name']} —— {ctx['role_desc']}\n"
|
||||||
|
f"你的性格:{ctx['persona'] or '普通玩家'}\n"
|
||||||
|
f"{RULES}\n"
|
||||||
|
"你是一个真实的人类玩家。永远不要提及你是AI、模型或程序。"
|
||||||
|
)
|
||||||
|
user = (
|
||||||
|
f"{_state_text(ctx)}\n\n"
|
||||||
|
f"【你的决策】{title}。\n{tip}\n"
|
||||||
|
f"可选目标(座位号):{opts_text}\n"
|
||||||
|
+ ("你可以选择放弃(target 为 null)。\n" if allow_skip else "必须选择一个目标。\n")
|
||||||
|
+ '只输出 JSON:{"target": 座位号数字或null, "reason": "不超过30字的理由"}'
|
||||||
|
)
|
||||||
|
return system, user
|
||||||
|
|
||||||
|
|
||||||
|
def speech_messages(ctx: dict) -> tuple[str, str]:
|
||||||
|
purpose = ctx.get("purpose") or "day"
|
||||||
|
if purpose == "election":
|
||||||
|
task = "现在是警长竞选阶段,请发表你的竞选发言(表决心、报身份或藏身份,符合你的策略)。"
|
||||||
|
else:
|
||||||
|
task = "轮到你发言了。请分析当前局势,表达你的身份立场、怀疑对象和投票意向。"
|
||||||
|
system = (
|
||||||
|
f"你正在玩一局狼人杀。你的名字是「{ctx['name']}」,座位号是{ctx['seat']}号。\n"
|
||||||
|
f"你的身份:{ctx['role_name']} —— {ctx['role_desc']}\n"
|
||||||
|
f"你的性格:{ctx['persona'] or '普通玩家'}\n"
|
||||||
|
f"{RULES}\n"
|
||||||
|
"你是一个真实的人类玩家。永远不要提及你是AI、模型或程序。"
|
||||||
|
"发言要口语化、简短自然,像真人打字,可以引用别人的发言和票型,不要罗列条目。"
|
||||||
|
)
|
||||||
|
user = f"{_state_text(ctx)}\n\n【任务】{task}\n直接输出发言内容本身(2~5句话),不要任何前缀、引号或解释。"
|
||||||
|
return system, user
|
||||||
|
|
||||||
|
|
||||||
|
def options_json(options: list) -> str:
|
||||||
|
return json.dumps(options, ensure_ascii=False)
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"""板子(角色配置)预设与校验。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
from .roles import Role
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Board:
|
||||||
|
key: str
|
||||||
|
name: str
|
||||||
|
roles: list[Role] = field(default_factory=list)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def seat_count(self) -> int:
|
||||||
|
return len(self.roles)
|
||||||
|
|
||||||
|
|
||||||
|
BOARDS: dict[str, Board] = {
|
||||||
|
"classic12": Board(
|
||||||
|
"classic12", "经典12人局(4狼 4民 预女猎守)",
|
||||||
|
[Role.WEREWOLF] * 4 + [Role.VILLAGER] * 4
|
||||||
|
+ [Role.SEER, Role.WITCH, Role.HUNTER, Role.GUARD],
|
||||||
|
),
|
||||||
|
"standard10": Board(
|
||||||
|
"standard10", "标准10人局(3狼 3民 预女猎守)",
|
||||||
|
[Role.WEREWOLF] * 3 + [Role.VILLAGER] * 3
|
||||||
|
+ [Role.SEER, Role.WITCH, Role.HUNTER, Role.GUARD],
|
||||||
|
),
|
||||||
|
"quick9": Board(
|
||||||
|
"quick9", "快节奏9人局(3狼 3民 预女猎)",
|
||||||
|
[Role.WEREWOLF] * 3 + [Role.VILLAGER] * 3
|
||||||
|
+ [Role.SEER, Role.WITCH, Role.HUNTER],
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_board(key: str) -> Board:
|
||||||
|
return BOARDS.get(key, BOARDS["classic12"])
|
||||||
@@ -0,0 +1,785 @@
|
|||||||
|
"""游戏引擎:昼夜状态机、技能结算、警长竞选、胜负判定。
|
||||||
|
|
||||||
|
引擎通过 Room 与客户端通信;AI 玩家的决策交给 agent(LLM 或启发式兜底)。
|
||||||
|
所有等待玩家操作的实现方式:注册一个 Pending(含每个席位的 Future),
|
||||||
|
人类玩家的提交通过 Room.handle_message 落到 handle_action / handle_speech,
|
||||||
|
AI 玩家则由后台任务调用 agent 后写入同一个 Future,超时按默认值处理。
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
|
||||||
|
from .player import Player
|
||||||
|
from .roles import ROLE_INFO, Role
|
||||||
|
from .board import Board
|
||||||
|
|
||||||
|
|
||||||
|
class Pending:
|
||||||
|
"""一次「等待若干席位提交操作」的登记。"""
|
||||||
|
|
||||||
|
def __init__(self, key: str, seats: list[int], options: list[int] | None, text: str, *,
|
||||||
|
allow_skip: bool = False, input_text: bool = False,
|
||||||
|
first_wins: bool = False, purpose: str = "") -> None:
|
||||||
|
self.key = key
|
||||||
|
self.seats = list(seats)
|
||||||
|
self.options = options
|
||||||
|
self.text = text
|
||||||
|
self.allow_skip = allow_skip
|
||||||
|
self.input_text = input_text
|
||||||
|
self.first_wins = first_wins # 狼队刀口:第一个提交者生效
|
||||||
|
self.purpose = purpose # speech 用:day / election
|
||||||
|
self.futures: dict[int, asyncio.Future] = {}
|
||||||
|
self.shared: asyncio.Future | None = None
|
||||||
|
if first_wins:
|
||||||
|
self.shared = asyncio.get_running_loop().create_future()
|
||||||
|
self.deadline = 0.0 # loop.time()
|
||||||
|
self.resolved = False
|
||||||
|
|
||||||
|
def seconds_left(self) -> int:
|
||||||
|
return max(0, int(self.deadline - time.monotonic()))
|
||||||
|
|
||||||
|
|
||||||
|
class Engine:
|
||||||
|
def __init__(self, room, board: Board, settings: dict) -> None:
|
||||||
|
self.room = room
|
||||||
|
self.board = board
|
||||||
|
self.settings = settings
|
||||||
|
self.running = True
|
||||||
|
self.reveal = False
|
||||||
|
self.day = 1
|
||||||
|
self.phase = "role_reveal"
|
||||||
|
self.phase_text = "身份分发中"
|
||||||
|
self.winner: str | None = None
|
||||||
|
self.winner_text: str | None = None
|
||||||
|
self.log: list[dict] = [] # {text, visible(None=公开|set[seat]), tag}
|
||||||
|
self.sheriff_seat: int | None = None
|
||||||
|
self.candidates: list[int] = []
|
||||||
|
self.claimed_seers: set[int] = set()
|
||||||
|
self.seer_results: dict[int, list[dict]] = {}
|
||||||
|
self.potions: dict[int, dict] = {}
|
||||||
|
self.guard_last: int | None = None
|
||||||
|
self.current_speaker: int | None = None
|
||||||
|
self.last_votes: list[dict] = []
|
||||||
|
self.pending_night_deaths: list[int] = []
|
||||||
|
self.night_death_causes: dict[int, str] = {}
|
||||||
|
self._pending: dict[str, Pending] = {}
|
||||||
|
self._agents: dict[int, object] = {}
|
||||||
|
|
||||||
|
# ================================================================ 工具
|
||||||
|
def alive_players(self) -> list[Player]:
|
||||||
|
return [self.room.players[s] for s in sorted(self.room.players) if self.room.players[s].alive]
|
||||||
|
|
||||||
|
def alive_seats(self) -> list[int]:
|
||||||
|
return [p.seat for p in self.alive_players()]
|
||||||
|
|
||||||
|
def player(self, seat: int) -> Player:
|
||||||
|
return self.room.players[seat]
|
||||||
|
|
||||||
|
def _alive_role(self, role: Role) -> Player | None:
|
||||||
|
for p in self.alive_players():
|
||||||
|
if p.role == role:
|
||||||
|
return p
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _log(self, text: str, seat: int | None = None, tag: str = "system") -> None:
|
||||||
|
self.log.append({
|
||||||
|
"text": text,
|
||||||
|
"visible": None if seat is None else {seat},
|
||||||
|
"tag": tag,
|
||||||
|
})
|
||||||
|
|
||||||
|
def _log_public(self, text: str, tag: str = "system") -> None:
|
||||||
|
self._log(text, None, tag)
|
||||||
|
|
||||||
|
def _log_for(self, seats: set[int], text: str, tag: str = "system") -> None:
|
||||||
|
self.log.append({"text": text, "visible": set(seats), "tag": tag})
|
||||||
|
|
||||||
|
def _timeout(self, kind: str) -> int:
|
||||||
|
key = {"night": "night_seconds", "speech": "speech_seconds", "vote": "vote_seconds"}.get(kind, "vote_seconds")
|
||||||
|
return int(self.settings.get(key, 30))
|
||||||
|
|
||||||
|
def _agent(self, player: Player):
|
||||||
|
if player.seat not in self._agents:
|
||||||
|
from .ai import make_agent
|
||||||
|
self._agents[player.seat] = make_agent(player)
|
||||||
|
return self._agents[player.seat]
|
||||||
|
|
||||||
|
def is_candidate(self, seat: int) -> bool:
|
||||||
|
return seat in self.candidates
|
||||||
|
|
||||||
|
def visible_role_for(self, viewer_seat: int, target: Player) -> tuple[str | None, str | None]:
|
||||||
|
"""viewer 视角下 target 的角色是否可见(狼队友互见)。"""
|
||||||
|
viewer = self.room.players.get(viewer_seat)
|
||||||
|
if viewer and viewer.role == Role.WEREWOLF and target.role == Role.WEREWOLF:
|
||||||
|
return target.role.value, ROLE_INFO[target.role].name
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
# ================================================================ 主流程
|
||||||
|
async def run(self) -> None:
|
||||||
|
try:
|
||||||
|
self._assign_roles()
|
||||||
|
await self._set_phase("role_reveal", "身份分发")
|
||||||
|
self._send_role_logs()
|
||||||
|
await asyncio.sleep(1.5)
|
||||||
|
while not self.winner:
|
||||||
|
await self._night()
|
||||||
|
if self._check_win():
|
||||||
|
break
|
||||||
|
await self._dawn()
|
||||||
|
if self._check_win():
|
||||||
|
break
|
||||||
|
if self.day == 1:
|
||||||
|
await self._sheriff_election()
|
||||||
|
await self._day_speeches()
|
||||||
|
await self._day_vote()
|
||||||
|
if self._check_win():
|
||||||
|
break
|
||||||
|
self.day += 1
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
raise
|
||||||
|
except Exception as exc: # 防止引擎任务静默死亡
|
||||||
|
self._log_public(f"游戏异常终止:{exc!r}")
|
||||||
|
self.winner = self.winner or "good"
|
||||||
|
self.winner_text = self.winner_text or "游戏异常结束"
|
||||||
|
finally:
|
||||||
|
self.running = False
|
||||||
|
self.reveal = True
|
||||||
|
self.phase = "game_over"
|
||||||
|
self.phase_text = "游戏结束"
|
||||||
|
self.current_speaker = None
|
||||||
|
self.candidates = []
|
||||||
|
self._pending.clear()
|
||||||
|
for p in self.room.players.values():
|
||||||
|
p.speaking = False
|
||||||
|
await self.room.broadcast_snapshot()
|
||||||
|
|
||||||
|
def _assign_roles(self) -> None:
|
||||||
|
roles = list(self.board.roles)
|
||||||
|
random.shuffle(roles)
|
||||||
|
for seat in sorted(self.room.players):
|
||||||
|
p = self.room.players[seat]
|
||||||
|
p.reset_for_new_game(roles.pop())
|
||||||
|
if p.role == Role.SEER:
|
||||||
|
self.seer_results[seat] = []
|
||||||
|
if p.role == Role.WITCH:
|
||||||
|
self.potions[seat] = {"antidote": True, "poison": True}
|
||||||
|
|
||||||
|
def _send_role_logs(self) -> None:
|
||||||
|
wolves = sorted(p.seat for p in self.room.players.values() if p.role == Role.WEREWOLF)
|
||||||
|
for p in self.room.players.values():
|
||||||
|
info = ROLE_INFO[p.role]
|
||||||
|
line = f"【你的身份】{info.emoji} {info.name}({p.seat}号)\n{info.description}"
|
||||||
|
if p.role == Role.WEREWOLF:
|
||||||
|
mates = [s for s in wolves if s != p.seat]
|
||||||
|
line += "\n你的狼队友:" + ("、".join(f"{s}号" for s in mates) if mates else "无(独狼)")
|
||||||
|
self._log(line, seat=p.seat, tag="identity")
|
||||||
|
|
||||||
|
def _check_win(self) -> bool:
|
||||||
|
alive = self.alive_players()
|
||||||
|
wolves = [p for p in alive if p.role == Role.WEREWOLF]
|
||||||
|
gods = [p for p in alive if p.role and ROLE_INFO[p.role].camp == "god"]
|
||||||
|
civs = [p for p in alive if p.role and ROLE_INFO[p.role].camp == "civilian"]
|
||||||
|
if not wolves:
|
||||||
|
self.winner = "good"
|
||||||
|
self.winner_text = "🎉 好人阵营胜利!所有狼人已被消灭。"
|
||||||
|
return True
|
||||||
|
rule = self.settings.get("win_rule", "bian")
|
||||||
|
if rule == "bian":
|
||||||
|
if not gods:
|
||||||
|
self.winner = "wolf"
|
||||||
|
self.winner_text = "🐺 狼人阵营胜利!(屠边:神职已全部出局)"
|
||||||
|
return True
|
||||||
|
if not civs:
|
||||||
|
self.winner = "wolf"
|
||||||
|
self.winner_text = "🐺 狼人阵营胜利!(屠边:村民已全部出局)"
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
if len(wolves) >= len(gods) + len(civs):
|
||||||
|
self.winner = "wolf"
|
||||||
|
self.winner_text = "🐺 狼人阵营胜利!(屠城:狼人数量不少于好人)"
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def _set_phase(self, phase: str, text: str) -> None:
|
||||||
|
self.phase = phase
|
||||||
|
self.phase_text = text
|
||||||
|
await self.room.broadcast_snapshot()
|
||||||
|
|
||||||
|
# ================================================================ 夜晚
|
||||||
|
async def _night(self) -> None:
|
||||||
|
self.last_votes = []
|
||||||
|
self.current_speaker = None
|
||||||
|
self.pending_night_deaths = []
|
||||||
|
self.night_death_causes = {}
|
||||||
|
await self._set_phase("night", f"第{self.day}夜 · 天黑请闭眼")
|
||||||
|
self._log_public(f"—— 第{self.day}夜 ——", "phase")
|
||||||
|
|
||||||
|
kill_target: int | None = None
|
||||||
|
saved = False
|
||||||
|
poison_target: int | None = None
|
||||||
|
|
||||||
|
# 守卫
|
||||||
|
guard = self._alive_role(Role.GUARD)
|
||||||
|
protect: int | None = None
|
||||||
|
if guard:
|
||||||
|
opts = [s for s in self.alive_seats() if s != self.guard_last]
|
||||||
|
await self._set_phase("night", f"第{self.day}夜 · 守卫请睁眼")
|
||||||
|
res = await self._wait("protect", [guard.seat], opts, self._timeout("night"),
|
||||||
|
"请选择今晚要守护的玩家(可空守,不能连守同一人)",
|
||||||
|
allow_skip=True)
|
||||||
|
protect = res.get(guard.seat)
|
||||||
|
if protect is not None:
|
||||||
|
self._log(f"你守护了 {protect} 号", seat=guard.seat, tag="private")
|
||||||
|
else:
|
||||||
|
self._log("你今晚选择空守", seat=guard.seat, tag="private")
|
||||||
|
|
||||||
|
# 狼人
|
||||||
|
wolves = [p for p in self.alive_players() if p.role == Role.WEREWOLF]
|
||||||
|
if wolves:
|
||||||
|
await self._set_phase("night", f"第{self.day}夜 · 狼人请睁眼")
|
||||||
|
wolf_seats = [w.seat for w in wolves]
|
||||||
|
victims = [s for s in self.alive_seats() if s not in wolf_seats]
|
||||||
|
kill_target = await self._wait_first_wins(
|
||||||
|
"kill", wolf_seats, victims, self._timeout("night"),
|
||||||
|
"狼队行动:选择今晚袭击的目标(队友先选的生效)")
|
||||||
|
if kill_target is not None:
|
||||||
|
self._log_for(set(wolf_seats), f"狼队决定袭击 {kill_target} 号", "private-wolf")
|
||||||
|
|
||||||
|
# 预言家
|
||||||
|
seer = self._alive_role(Role.SEER)
|
||||||
|
if seer:
|
||||||
|
results = self.seer_results.setdefault(seer.seat, [])
|
||||||
|
checked = {r["seat"] for r in results}
|
||||||
|
opts = [s for s in self.alive_seats() if s != seer.seat and s not in checked]
|
||||||
|
if opts:
|
||||||
|
await self._set_phase("night", f"第{self.day}夜 · 预言家请睁眼")
|
||||||
|
res = await self._wait("check", [seer.seat], opts, self._timeout("night"),
|
||||||
|
"请选择今晚要查验的玩家")
|
||||||
|
target = res.get(seer.seat)
|
||||||
|
if target is not None:
|
||||||
|
camp = "狼人" if self.player(target).role == Role.WEREWOLF else "好人"
|
||||||
|
results.append({"seat": target, "result": camp})
|
||||||
|
self._log(f"查验结果:{target} 号是「{camp}」", seat=seer.seat, tag="private")
|
||||||
|
|
||||||
|
# 女巫
|
||||||
|
witch = self._alive_role(Role.WITCH)
|
||||||
|
if witch:
|
||||||
|
pot = self.potions[witch.seat]
|
||||||
|
if pot["antidote"] or pot["poison"]:
|
||||||
|
await self._set_phase("night", f"第{self.day}夜 · 女巫请睁眼")
|
||||||
|
if pot["antidote"] and kill_target is not None:
|
||||||
|
can_act = kill_target != witch.seat or self.day == 1 # 首夜可自救
|
||||||
|
if can_act:
|
||||||
|
hint = f"今晚 {kill_target} 号被袭击了。" + ("(你可以在首夜自救)" if kill_target == witch.seat else "")
|
||||||
|
res = await self._wait("witch_save", [witch.seat], [kill_target],
|
||||||
|
self._timeout("night"), hint + " 是否使用解药?",
|
||||||
|
allow_skip=True)
|
||||||
|
if res.get(witch.seat) is not None:
|
||||||
|
pot["antidote"] = False
|
||||||
|
saved = True
|
||||||
|
self._log(f"你使用解药救下了 {kill_target} 号", seat=witch.seat, tag="private")
|
||||||
|
else:
|
||||||
|
self._log(f"你选择不救 {kill_target} 号", seat=witch.seat, tag="private")
|
||||||
|
else:
|
||||||
|
self._log("你今晚被袭击了(非首夜,无法自救)", seat=witch.seat, tag="private")
|
||||||
|
if pot["poison"] and not saved:
|
||||||
|
opts = [s for s in self.alive_seats() if s != witch.seat]
|
||||||
|
res = await self._wait("witch_poison", [witch.seat], opts,
|
||||||
|
self._timeout("night"), "是否使用毒药?选择要毒杀的玩家",
|
||||||
|
allow_skip=True)
|
||||||
|
poison_target = res.get(witch.seat)
|
||||||
|
if poison_target is not None:
|
||||||
|
pot["poison"] = False
|
||||||
|
self._log(f"你毒杀了 {poison_target} 号", seat=witch.seat, tag="private")
|
||||||
|
else:
|
||||||
|
self._log("你今晚没有用毒", seat=witch.seat, tag="private")
|
||||||
|
|
||||||
|
# 结算夜晚死亡
|
||||||
|
self.guard_last = protect
|
||||||
|
deaths: list[int] = []
|
||||||
|
if kill_target is not None:
|
||||||
|
if protect == kill_target and saved:
|
||||||
|
deaths.append(kill_target) # 同守同救
|
||||||
|
self.night_death_causes[kill_target] = "wolf"
|
||||||
|
elif protect == kill_target or saved:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
deaths.append(kill_target)
|
||||||
|
self.night_death_causes[kill_target] = "wolf"
|
||||||
|
if poison_target is not None and poison_target not in deaths:
|
||||||
|
deaths.append(poison_target)
|
||||||
|
self.night_death_causes[poison_target] = "poison"
|
||||||
|
self.pending_night_deaths = deaths
|
||||||
|
|
||||||
|
async def _dawn(self) -> None:
|
||||||
|
await self._set_phase("dawn", f"第{self.day}天 · 天亮了")
|
||||||
|
self._log_public(f"—— 第{self.day}天 ——", "phase")
|
||||||
|
deaths = sorted(set(self.pending_night_deaths))
|
||||||
|
if not deaths:
|
||||||
|
self._log_public("昨夜是平安夜,无人出局。", "night")
|
||||||
|
else:
|
||||||
|
names = "、".join(f"{s}号" for s in deaths)
|
||||||
|
self._log_public(f"昨夜 {names} 出局。", "night")
|
||||||
|
for s in deaths:
|
||||||
|
p = self.player(s)
|
||||||
|
p.alive = False
|
||||||
|
cause = self.night_death_causes.get(s, "wolf")
|
||||||
|
self._log(f"你死了({'被毒杀' if cause == 'poison' else '被狼人袭击'})",
|
||||||
|
seat=s, tag="private")
|
||||||
|
await self.room.broadcast_snapshot()
|
||||||
|
|
||||||
|
if self.sheriff_seat is not None and self.sheriff_seat in deaths:
|
||||||
|
await self._badge_pass(self.sheriff_seat)
|
||||||
|
for s in deaths:
|
||||||
|
await self._maybe_hunter_shoot(s, poisoned=(self.night_death_causes.get(s) == "poison"))
|
||||||
|
|
||||||
|
async def _maybe_hunter_shoot(self, seat: int, *, poisoned: bool) -> None:
|
||||||
|
p = self.player(seat)
|
||||||
|
if p.role != Role.HUNTER:
|
||||||
|
return
|
||||||
|
if poisoned:
|
||||||
|
self._log("你被毒杀,无法开枪", seat=seat, tag="private")
|
||||||
|
return
|
||||||
|
opts = self.alive_seats()
|
||||||
|
if not opts:
|
||||||
|
return
|
||||||
|
await self._set_phase("dawn", f"{seat}号猎人 · 可发动技能")
|
||||||
|
self._log_public(f"{seat}号是猎人,可以开枪……", "system")
|
||||||
|
res = await self._wait("shoot", [seat], opts, self._timeout("vote"),
|
||||||
|
"你可以开枪带走一名玩家", allow_skip=True)
|
||||||
|
target = res.get(seat)
|
||||||
|
if target is None:
|
||||||
|
self._log_public(f"{seat}号猎人选择不开枪。")
|
||||||
|
return
|
||||||
|
victim = self.player(target)
|
||||||
|
victim.alive = False
|
||||||
|
self._log_public(f"🏹 {seat}号猎人开枪带走了 {target}号!", "night")
|
||||||
|
self._log("你死了(被猎人枪杀)", seat=target, tag="private")
|
||||||
|
await self.room.broadcast_snapshot()
|
||||||
|
if self.sheriff_seat == target:
|
||||||
|
await self._badge_pass(target)
|
||||||
|
|
||||||
|
async def _badge_pass(self, dead_sheriff: int) -> None:
|
||||||
|
self.sheriff_seat = None
|
||||||
|
opts = self.alive_seats()
|
||||||
|
if not opts:
|
||||||
|
return
|
||||||
|
await self._set_phase(self.phase, f"{dead_sheriff}号警长死亡 · 警徽流转")
|
||||||
|
self._log_public(f"{dead_sheriff}号警长的警徽等待移交……", "system")
|
||||||
|
res = await self._wait("badge", [dead_sheriff], opts, self._timeout("vote"),
|
||||||
|
"你可以将警徽移交给一名存活玩家,或撕毁警徽", allow_skip=True)
|
||||||
|
target = res.get(dead_sheriff)
|
||||||
|
if target is None:
|
||||||
|
self._log_public(f"{dead_sheriff}号撕毁了警徽,本局再无警长。")
|
||||||
|
else:
|
||||||
|
self.sheriff_seat = target
|
||||||
|
self._log_public(f"{dead_sheriff}号将警徽移交给了 {target}号。")
|
||||||
|
|
||||||
|
# ================================================================ 白天
|
||||||
|
async def _sheriff_election(self) -> None:
|
||||||
|
await self._set_phase("sheriff_elect", "警长竞选")
|
||||||
|
self._log_public("—— 警长竞选 ——", "phase")
|
||||||
|
alive = self.alive_seats()
|
||||||
|
res = await self._wait("elect", alive, [1], self._timeout("vote"),
|
||||||
|
"是否上警竞选警长?(警长投票计 1.5 票)", allow_skip=True)
|
||||||
|
self.candidates = [s for s in alive if res.get(s) == 1]
|
||||||
|
if not self.candidates:
|
||||||
|
self._log_public("无人上警,本局没有警长。")
|
||||||
|
self.candidates = []
|
||||||
|
await self.room.broadcast_snapshot()
|
||||||
|
return
|
||||||
|
if len(self.candidates) == 1:
|
||||||
|
self.sheriff_seat = self.candidates[0]
|
||||||
|
self._log_public(f"仅 {self.sheriff_seat}号 上警,自动当选警长。")
|
||||||
|
self._log("你当选了警长(投票计 1.5 票)", seat=self.sheriff_seat, tag="private")
|
||||||
|
self.candidates = []
|
||||||
|
await self.room.broadcast_snapshot()
|
||||||
|
return
|
||||||
|
|
||||||
|
self._log_public("上警玩家:" + "、".join(f"{s}号" for s in self.candidates))
|
||||||
|
for s in self.candidates:
|
||||||
|
if not self.player(s).alive:
|
||||||
|
continue
|
||||||
|
await self._speech_one(s, purpose="election")
|
||||||
|
|
||||||
|
voters = [s for s in self.alive_seats() if s not in self.candidates]
|
||||||
|
if not voters:
|
||||||
|
self.sheriff_seat = random.choice(self.candidates)
|
||||||
|
self._log_public(f"无人可投票,随机任命 {self.sheriff_seat}号 为警长。")
|
||||||
|
self._log("你当选了警长(投票计 1.5 票)", seat=self.sheriff_seat, tag="private")
|
||||||
|
self.candidates = []
|
||||||
|
await self.room.broadcast_snapshot()
|
||||||
|
return
|
||||||
|
|
||||||
|
await self._set_phase("sheriff_elect", "警长竞选 · 投票")
|
||||||
|
res = await self._wait("sheriff_vote", voters, list(self.candidates),
|
||||||
|
self._timeout("vote"), "请投票选出警长(不可弃票)")
|
||||||
|
self.last_votes = [{"voter": v, "target": t} for v, t in res.items() if t is not None]
|
||||||
|
counts: dict[int, float] = {}
|
||||||
|
for v, t in res.items():
|
||||||
|
if t is not None:
|
||||||
|
counts[t] = counts.get(t, 0) + 1.0
|
||||||
|
tally = ",".join(f"{s}号 {counts.get(s, 0):g} 票" for s in self.candidates)
|
||||||
|
top = max(counts.values()) if counts else 0
|
||||||
|
leaders = sorted([s for s, c in counts.items() if c == top])
|
||||||
|
if len(leaders) > 1:
|
||||||
|
self.sheriff_seat = random.choice(leaders)
|
||||||
|
self._log_public(f"警长竞选平票({tally}),随机由 {self.sheriff_seat}号 当选。")
|
||||||
|
else:
|
||||||
|
self.sheriff_seat = leaders[0]
|
||||||
|
self._log_public(f"{self.sheriff_seat}号当选警长!({tally})")
|
||||||
|
self._log("你当选了警长(投票计 1.5 票)", seat=self.sheriff_seat, tag="private")
|
||||||
|
self.candidates = []
|
||||||
|
await self.room.broadcast_snapshot()
|
||||||
|
|
||||||
|
async def _day_speeches(self) -> None:
|
||||||
|
await self._set_phase("speech", f"第{self.day}天 · 白天发言")
|
||||||
|
deaths = sorted(set(self.pending_night_deaths))
|
||||||
|
if deaths:
|
||||||
|
self._log_public("发言开始,从死者下家开始按座位顺序发言。", "phase")
|
||||||
|
start = deaths[0] % self.board.seat_count + 1
|
||||||
|
else:
|
||||||
|
base = self.sheriff_seat if self.sheriff_seat else 0
|
||||||
|
self._log_public("发言开始,从警长下家开始按座位顺序发言。", "phase")
|
||||||
|
start = base % self.board.seat_count + 1
|
||||||
|
alive = self.alive_seats()
|
||||||
|
order = [s for s in sorted(alive, key=lambda x: (x - start) % self.board.seat_count)]
|
||||||
|
for s in order:
|
||||||
|
if self.winner:
|
||||||
|
return
|
||||||
|
await self._speech_one(s, purpose="day")
|
||||||
|
|
||||||
|
async def _speech_one(self, seat: int, *, purpose: str) -> None:
|
||||||
|
p = self.player(seat)
|
||||||
|
if not p.alive:
|
||||||
|
return
|
||||||
|
p.speaking = True
|
||||||
|
self.current_speaker = seat
|
||||||
|
label = "竞选发言" if purpose == "election" else "发言"
|
||||||
|
await self._set_phase("speech", f"第{self.day}天 · {seat}号{label}中")
|
||||||
|
|
||||||
|
pend = Pending("speech", [seat], None, "轮到你发言了", input_text=True, purpose=purpose)
|
||||||
|
self._pending["speech"] = pend
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
fut = loop.create_future()
|
||||||
|
pend.futures[seat] = fut
|
||||||
|
pend.deadline = loop.time() + self._timeout("speech")
|
||||||
|
ai_task = None
|
||||||
|
if p.is_ai:
|
||||||
|
ai_task = asyncio.create_task(self._ai_speak(p, pend))
|
||||||
|
await self.room.broadcast_snapshot()
|
||||||
|
|
||||||
|
timeout = self._timeout("speech")
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(asyncio.shield(fut), timeout)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
self._pending.pop("speech", None)
|
||||||
|
if ai_task:
|
||||||
|
ai_task.cancel()
|
||||||
|
p.speaking = False
|
||||||
|
self.current_speaker = None
|
||||||
|
text = fut.result().strip() if fut.done() and not fut.cancelled() else ""
|
||||||
|
if not text:
|
||||||
|
text = "(过)"
|
||||||
|
self._log_public(f"{seat}号 {p.name}:{text}", "speech")
|
||||||
|
self._scan_claims(seat, text)
|
||||||
|
await self.room.broadcast_snapshot()
|
||||||
|
|
||||||
|
def _scan_claims(self, seat: int, text: str) -> None:
|
||||||
|
if ("预言家" in text) and ("我" in text or "本人" in text):
|
||||||
|
self.claimed_seers.add(seat)
|
||||||
|
|
||||||
|
async def _day_vote(self) -> None:
|
||||||
|
await self._set_phase("vote", f"第{self.day}天 · 投票放逐")
|
||||||
|
self._log_public("—— 投票 ——", "phase")
|
||||||
|
alive = self.alive_seats()
|
||||||
|
if len(alive) < 2:
|
||||||
|
return
|
||||||
|
res = await self._wait("vote", alive, alive, self._timeout("vote"),
|
||||||
|
"请投票放逐一名玩家(可弃票)", allow_skip=True)
|
||||||
|
self.last_votes = [{"voter": v, "target": t} for v, t in res.items()]
|
||||||
|
counts: dict[int, float] = {}
|
||||||
|
for v, t in res.items():
|
||||||
|
if t is None:
|
||||||
|
continue
|
||||||
|
weight = 1.5 if v == self.sheriff_seat else 1.0
|
||||||
|
counts[t] = counts.get(t, 0) + weight
|
||||||
|
detail = ",".join(f"{s}号 {counts.get(s, 0):g} 票" for s in alive if counts.get(s))
|
||||||
|
abstain = [v for v, t in res.items() if t is None]
|
||||||
|
if abstain:
|
||||||
|
detail += (";" if detail else "") + f"弃票:{'、'.join(str(v) + '号' for v in abstain)}"
|
||||||
|
self._log_public(f"投票结果:{detail or '无人投票'}", "vote")
|
||||||
|
await self.room.broadcast_snapshot()
|
||||||
|
|
||||||
|
if not counts:
|
||||||
|
self._log_public("无人得票,无人出局。")
|
||||||
|
return
|
||||||
|
top = max(counts.values())
|
||||||
|
leaders = sorted(s for s, c in counts.items() if c == top)
|
||||||
|
if len(leaders) > 1:
|
||||||
|
self._log_public(f"{'、'.join(str(s) + '号' for s in leaders)} 平票,本日无人出局。")
|
||||||
|
return
|
||||||
|
out = leaders[0]
|
||||||
|
victim = self.player(out)
|
||||||
|
victim.alive = False
|
||||||
|
self._log_public(f"⚖️ {out}号被投票放逐。", "vote")
|
||||||
|
self._log("你被投票放逐了", seat=out, tag="private")
|
||||||
|
await self.room.broadcast_snapshot()
|
||||||
|
if victim.role == Role.HUNTER:
|
||||||
|
await self._maybe_hunter_shoot(out, poisoned=False)
|
||||||
|
if self.sheriff_seat == out:
|
||||||
|
await self._badge_pass(out)
|
||||||
|
|
||||||
|
# ================================================================ 行动登记
|
||||||
|
def handle_action(self, seat: int, key: str | None, target) -> tuple[bool, str]:
|
||||||
|
if key is None:
|
||||||
|
return False, "缺少操作类型"
|
||||||
|
pend = self._pending.get(key)
|
||||||
|
if pend is None:
|
||||||
|
return False, "当前没有等待你的操作"
|
||||||
|
if pend.resolved:
|
||||||
|
return False, "该决定已完成(队友先提交了)"
|
||||||
|
if seat not in pend.seats:
|
||||||
|
return False, "现在不是你的行动阶段"
|
||||||
|
fut = pend.futures.get(seat)
|
||||||
|
if fut is not None and fut.done():
|
||||||
|
return False, "你已经提交过了"
|
||||||
|
if key == "vote" and target == seat:
|
||||||
|
return False, "不能投自己"
|
||||||
|
if target is None:
|
||||||
|
if not pend.allow_skip:
|
||||||
|
return False, "必须选择一个目标"
|
||||||
|
elif pend.options is not None and target not in pend.options:
|
||||||
|
return False, "无效的目标"
|
||||||
|
self._resolve(pend, seat, target)
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
def handle_speech(self, seat: int, text: str) -> tuple[bool, str]:
|
||||||
|
pend = self._pending.get("speech")
|
||||||
|
if pend is None:
|
||||||
|
return False, "当前没有轮到你发言"
|
||||||
|
if seat not in pend.seats:
|
||||||
|
return False, "当前没有轮到你发言"
|
||||||
|
fut = pend.futures.get(seat)
|
||||||
|
if fut is None or fut.done():
|
||||||
|
return False, "发言已提交"
|
||||||
|
text = (text or "").strip()[:300]
|
||||||
|
fut.set_result(text)
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
def _resolve(self, pend: Pending, seat: int, target) -> None:
|
||||||
|
if pend.resolved:
|
||||||
|
return
|
||||||
|
if pend.first_wins:
|
||||||
|
if pend.shared is not None and not pend.shared.done():
|
||||||
|
pend.shared.set_result(target)
|
||||||
|
pend.resolved = True
|
||||||
|
return
|
||||||
|
fut = pend.futures.get(seat)
|
||||||
|
if fut is not None and not fut.done():
|
||||||
|
fut.set_result(target)
|
||||||
|
|
||||||
|
async def _wait(self, key: str, seats: list[int], options: list[int] | None,
|
||||||
|
timeout: int, text: str, *, allow_skip: bool = False,
|
||||||
|
purpose: str = "") -> dict[int, int | None]:
|
||||||
|
pend = Pending(key, seats, options, text, allow_skip=allow_skip, purpose=purpose)
|
||||||
|
self._pending[key] = pend
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
pend.deadline = loop.time() + timeout
|
||||||
|
ai_tasks: list[asyncio.Task] = []
|
||||||
|
for s in seats:
|
||||||
|
p = self.player(s)
|
||||||
|
fut = loop.create_future()
|
||||||
|
pend.futures[s] = fut
|
||||||
|
if p.is_ai:
|
||||||
|
ai_tasks.append(asyncio.create_task(self._ai_act(p, pend)))
|
||||||
|
await self.room.broadcast_snapshot()
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(asyncio.gather(*pend.futures.values()), timeout=timeout)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
for t in ai_tasks:
|
||||||
|
t.cancel()
|
||||||
|
self._pending.pop(key, None)
|
||||||
|
results: dict[int, int | None] = {}
|
||||||
|
for s, fut in pend.futures.items():
|
||||||
|
try:
|
||||||
|
results[s] = fut.result() if fut.done() else None
|
||||||
|
except Exception:
|
||||||
|
results[s] = None
|
||||||
|
await self.room.broadcast_snapshot()
|
||||||
|
return results
|
||||||
|
|
||||||
|
async def _wait_first_wins(self, key: str, seats: list[int], options: list[int],
|
||||||
|
timeout: int, text: str) -> int | None:
|
||||||
|
pend = Pending(key, seats, options, text, allow_skip=True, first_wins=True)
|
||||||
|
self._pending[key] = pend
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
pend.deadline = loop.time() + timeout
|
||||||
|
ai_seats = [s for s in seats if self.player(s).is_ai]
|
||||||
|
ai_task = None
|
||||||
|
if ai_seats:
|
||||||
|
leader = random.choice(ai_seats) # 由一名 AI 狼主导提议,避免重复调用
|
||||||
|
ai_task = asyncio.create_task(self._ai_act(self.player(leader), pend))
|
||||||
|
await self.room.broadcast_snapshot()
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(asyncio.shield(pend.shared), timeout=timeout)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
if ai_task:
|
||||||
|
ai_task.cancel()
|
||||||
|
pend.resolved = True
|
||||||
|
self._pending.pop(key, None)
|
||||||
|
result = None
|
||||||
|
if pend.shared is not None and pend.shared.done() and not pend.shared.cancelled():
|
||||||
|
try:
|
||||||
|
result = pend.shared.result()
|
||||||
|
except Exception:
|
||||||
|
result = None
|
||||||
|
await self.room.broadcast_snapshot()
|
||||||
|
return result
|
||||||
|
|
||||||
|
# ================================================================ AI
|
||||||
|
def _ai_context(self, player: Player, pend: Pending | None = None, purpose: str = "") -> dict:
|
||||||
|
priv: list[str] = []
|
||||||
|
role = player.role
|
||||||
|
info = ROLE_INFO[role] if role else None
|
||||||
|
if role == Role.WEREWOLF:
|
||||||
|
mates = sorted(p.seat for p in self.room.players.values()
|
||||||
|
if p.role == Role.WEREWOLF and p.seat != player.seat)
|
||||||
|
priv.append("狼队友:" + ("、".join(f"{s}号" for s in mates) if mates else "无"))
|
||||||
|
if role == Role.SEER:
|
||||||
|
rs = self.seer_results.get(player.seat, [])
|
||||||
|
priv.append("查验记录:" + (";".join(f"{r['seat']}号是{r['result']}" for r in rs) if rs else "暂无"))
|
||||||
|
if role == Role.WITCH:
|
||||||
|
pot = self.potions[player.seat]
|
||||||
|
priv.append(f"解药{'还在' if pot['antidote'] else '已用'},毒药{'还在' if pot['poison'] else '已用'}")
|
||||||
|
if role == Role.GUARD:
|
||||||
|
priv.append(f"昨晚守护了{self.guard_last}号" if self.guard_last else "昨晚空守")
|
||||||
|
if self.sheriff_seat is not None:
|
||||||
|
priv.append(f"当前警长是{self.sheriff_seat}号(投票计1.5票)")
|
||||||
|
|
||||||
|
alive = self.alive_players()
|
||||||
|
log_lines = []
|
||||||
|
for e in self.log[-80:]:
|
||||||
|
if e["visible"] is None:
|
||||||
|
log_lines.append(e["text"])
|
||||||
|
log_text = "\n".join(log_lines)
|
||||||
|
if len(log_text) > 6000:
|
||||||
|
log_text = log_text[-6000:]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"seat": player.seat,
|
||||||
|
"name": player.name,
|
||||||
|
"persona": player.persona,
|
||||||
|
"role": role.value if role else "",
|
||||||
|
"role_name": info.name if info else "",
|
||||||
|
"role_desc": info.description if info else "",
|
||||||
|
"private": priv,
|
||||||
|
"day": self.day,
|
||||||
|
"phase_text": self.phase_text,
|
||||||
|
"alive": [{"seat": p.seat, "name": p.name} for p in alive],
|
||||||
|
"options": pend.options if pend else [],
|
||||||
|
"key": pend.key if pend else "",
|
||||||
|
"question": pend.text if pend else "",
|
||||||
|
"purpose": purpose,
|
||||||
|
"claimed_seers": sorted(self.claimed_seers),
|
||||||
|
"sheriff_seat": self.sheriff_seat,
|
||||||
|
"log_text": log_text,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def _ai_act(self, player: Player, pend: Pending) -> None:
|
||||||
|
try:
|
||||||
|
agent = self._agent(player)
|
||||||
|
ctx = self._ai_context(player, pend)
|
||||||
|
if pend.resolved:
|
||||||
|
return
|
||||||
|
target = await agent.decide(pend.key, ctx)
|
||||||
|
if target is not None and pend.options is not None and target not in pend.options:
|
||||||
|
target = None if pend.allow_skip else random.choice(pend.options)
|
||||||
|
self._resolve(pend, player.seat, target)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
if not pend.resolved and pend.allow_skip:
|
||||||
|
self._resolve(pend, player.seat, None)
|
||||||
|
|
||||||
|
async def _ai_speak(self, player: Player, pend: Pending) -> None:
|
||||||
|
try:
|
||||||
|
agent = self._agent(player)
|
||||||
|
ctx = self._ai_context(player, pend, purpose=pend.purpose)
|
||||||
|
text = await agent.speak(ctx)
|
||||||
|
fut = pend.futures.get(player.seat)
|
||||||
|
if fut is not None and not fut.done():
|
||||||
|
fut.set_result((text or "").strip()[:300])
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ================================================================ 快照
|
||||||
|
def snapshot_for(self, seat: int | None) -> dict:
|
||||||
|
players_game = {}
|
||||||
|
# 由 Room.snapshot_for 组装玩家视图,这里只补充对局相关字段
|
||||||
|
pend_view = None
|
||||||
|
if seat is not None:
|
||||||
|
for pend in self._pending.values():
|
||||||
|
if pend.resolved or seat not in pend.seats:
|
||||||
|
continue
|
||||||
|
fut = pend.futures.get(seat)
|
||||||
|
if fut is not None and fut.done():
|
||||||
|
continue
|
||||||
|
if pend.input_text:
|
||||||
|
pend_view = {
|
||||||
|
"key": pend.key, "options": [], "text": pend.text,
|
||||||
|
"allow_skip": False, "input_text": True,
|
||||||
|
"purpose": pend.purpose, "seconds_left": pend.seconds_left(),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
pend_view = {
|
||||||
|
"key": pend.key,
|
||||||
|
"options": pend.options or [],
|
||||||
|
"text": pend.text,
|
||||||
|
"allow_skip": pend.allow_skip,
|
||||||
|
"input_text": False,
|
||||||
|
"purpose": "",
|
||||||
|
"seconds_left": pend.seconds_left(),
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
me = None
|
||||||
|
if seat is not None:
|
||||||
|
p = self.room.players.get(seat)
|
||||||
|
if p is not None and p.role is not None:
|
||||||
|
me = {"seat": seat}
|
||||||
|
if p.role == Role.WEREWOLF:
|
||||||
|
me["teammates"] = sorted(q.seat for q in self.room.players.values()
|
||||||
|
if q.role == Role.WEREWOLF and q.seat != seat)
|
||||||
|
if p.role == Role.SEER:
|
||||||
|
me["seer_results"] = self.seer_results.get(seat, [])
|
||||||
|
if p.role == Role.WITCH:
|
||||||
|
me["potions"] = self.potions.get(seat)
|
||||||
|
|
||||||
|
log_view = []
|
||||||
|
for e in self.log:
|
||||||
|
if e["visible"] is None or (seat is not None and seat in e["visible"]):
|
||||||
|
log_view.append({"text": e["text"], "kind": "private" if e["visible"] else "public",
|
||||||
|
"tag": e["tag"]})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"me_extras": me,
|
||||||
|
"log": log_view,
|
||||||
|
"pending": pend_view,
|
||||||
|
"sheriff_seat": self.sheriff_seat,
|
||||||
|
"candidates": list(self.candidates),
|
||||||
|
"current_speaker": self.current_speaker,
|
||||||
|
"last_votes": self.last_votes,
|
||||||
|
"winner": self.winner,
|
||||||
|
"winner_text": self.winner_text,
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"""玩家实体:人类(持 WebSocket 连接)或 AI。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import secrets
|
||||||
|
|
||||||
|
from fastapi import WebSocket
|
||||||
|
|
||||||
|
from .roles import Role
|
||||||
|
|
||||||
|
|
||||||
|
class Player:
|
||||||
|
def __init__(self, seat: int, name: str, is_ai: bool = False) -> None:
|
||||||
|
self.seat = seat
|
||||||
|
self.name = name
|
||||||
|
self.is_ai = is_ai
|
||||||
|
self.token = secrets.token_hex(8)
|
||||||
|
self.role: Role | None = None
|
||||||
|
self.alive = True
|
||||||
|
self.ws: WebSocket | None = None # AI 恒为 None
|
||||||
|
self.speaking = False # 当前正在发言(仅用于 UI 高亮)
|
||||||
|
self.persona: str = "" # AI 性格(LLM 提示词用)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@property
|
||||||
|
def connected(self) -> bool:
|
||||||
|
return self.is_ai or self.ws is not None
|
||||||
|
|
||||||
|
def bind_ws(self, ws: WebSocket) -> None:
|
||||||
|
self.ws = ws
|
||||||
|
|
||||||
|
def unbind_ws(self, ws: WebSocket) -> None:
|
||||||
|
if self.ws is ws:
|
||||||
|
self.ws = None
|
||||||
|
|
||||||
|
def reset_for_new_game(self, role: Role) -> None:
|
||||||
|
self.role = role
|
||||||
|
self.alive = True
|
||||||
|
self.speaking = False
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
"""角色定义。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class Role(str, Enum):
|
||||||
|
WEREWOLF = "werewolf"
|
||||||
|
VILLAGER = "villager"
|
||||||
|
SEER = "seer"
|
||||||
|
WITCH = "witch"
|
||||||
|
HUNTER = "hunter"
|
||||||
|
GUARD = "guard"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class RoleInfo:
|
||||||
|
role: Role
|
||||||
|
name: str # 中文名
|
||||||
|
camp: str # "wolf" | "god" | "civilian"
|
||||||
|
emoji: str
|
||||||
|
description: str
|
||||||
|
|
||||||
|
|
||||||
|
ROLE_INFO: dict[Role, RoleInfo] = {
|
||||||
|
Role.WEREWOLF: RoleInfo(
|
||||||
|
Role.WEREWOLF, "狼人", "wolf", "🐺",
|
||||||
|
"每晚与同伴共同袭击一名玩家,白天隐藏身份,误导好人投票。",
|
||||||
|
),
|
||||||
|
Role.VILLAGER: RoleInfo(
|
||||||
|
Role.VILLAGER, "村民", "civilian", "👨🌾",
|
||||||
|
"没有主动技能,白天通过发言与投票找出狼人。",
|
||||||
|
),
|
||||||
|
Role.SEER: RoleInfo(
|
||||||
|
Role.SEER, "预言家", "god", "🔮",
|
||||||
|
"每晚查验一名玩家的阵营(好人 / 狼人),带领好人寻找狼人。",
|
||||||
|
),
|
||||||
|
Role.WITCH: RoleInfo(
|
||||||
|
Role.WITCH, "女巫", "god", "🧪",
|
||||||
|
"拥有一瓶解药和一瓶毒药。解药可救活当晚被袭击者,毒药可毒杀一名玩家,每夜只能用一瓶。首夜可自救。",
|
||||||
|
),
|
||||||
|
Role.HUNTER: RoleInfo(
|
||||||
|
Role.HUNTER, "猎人", "god", "🏹",
|
||||||
|
"被狼人袭击或被投票放逐时,可开枪带走一名玩家(被毒杀时无法开枪)。",
|
||||||
|
),
|
||||||
|
Role.GUARD: RoleInfo(
|
||||||
|
Role.GUARD, "守卫", "god", "🛡️",
|
||||||
|
"每晚守护一名玩家免受狼人袭击,不能连续两晚守护同一人。同守同救会死亡。",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
WOLF_TEAM = {Role.WEREWOLF}
|
||||||
|
GOD_TEAM = {Role.SEER, Role.WITCH, Role.HUNTER, Role.GUARD}
|
||||||
|
CIVILIAN_TEAM = {Role.VILLAGER}
|
||||||
|
|
||||||
|
|
||||||
|
def role_camp(role: Role) -> str:
|
||||||
|
return ROLE_INFO[role].camp
|
||||||
|
|
||||||
|
|
||||||
|
def is_wolf(role: Role) -> bool:
|
||||||
|
return role == Role.WEREWOLF
|
||||||
@@ -0,0 +1,418 @@
|
|||||||
|
"""房间与连接管理:座位分配、广播/私发快照、房主控制、断线重连。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import random
|
||||||
|
import secrets
|
||||||
|
import string
|
||||||
|
import time
|
||||||
|
|
||||||
|
from fastapi import WebSocket
|
||||||
|
|
||||||
|
from .board import BOARDS, Board, get_board
|
||||||
|
from .engine import Engine
|
||||||
|
from .player import Player
|
||||||
|
from .roles import ROLE_INFO
|
||||||
|
|
||||||
|
AI_NAMES = [
|
||||||
|
"李白", "杜甫", "白居易", "苏轼", "李清照", "辛弃疾", "王维", "杜牧",
|
||||||
|
"陶渊明", "屈原", "韩愈", "欧阳修", "王安石", "陆游", "孟浩然", "刘禹锡",
|
||||||
|
"曹操", "诸葛亮", "赵云", "司马懿",
|
||||||
|
]
|
||||||
|
|
||||||
|
AI_PERSONAS = [
|
||||||
|
"直率暴躁,容易上头,发言简短有力",
|
||||||
|
"谨慎理性,逻辑流,喜欢分析时间线和票型",
|
||||||
|
"幽默话痨,喜欢开玩笑,但关键时刻很准",
|
||||||
|
"老练沉稳,话不多但每句都有分量",
|
||||||
|
"激进冲锋,敢说敢投,喜欢带节奏",
|
||||||
|
"温柔细腻,善于观察别人的表情和语气",
|
||||||
|
"老好人形象,总是和稀泥,偶尔语出惊人",
|
||||||
|
"策略家,喜欢钓鱼和反向操作",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Spectator:
|
||||||
|
"""房间满员或对局中进入的观战者。"""
|
||||||
|
|
||||||
|
def __init__(self, ws: WebSocket, name: str) -> None:
|
||||||
|
self.seat = None
|
||||||
|
self.name = name
|
||||||
|
self.ws = ws
|
||||||
|
self.token = secrets.token_hex(8)
|
||||||
|
self.is_ai = False
|
||||||
|
self.note = ""
|
||||||
|
|
||||||
|
|
||||||
|
def _gen_code() -> str:
|
||||||
|
return "".join(random.choices("ABCDEFGHJKLMNPQRSTUVWXYZ23456789", k=4))
|
||||||
|
|
||||||
|
|
||||||
|
class RoomManager:
|
||||||
|
ROOM_TTL_SECONDS = 600 # 房间内没有真人连接超过该时长后回收
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.rooms: dict[str, Room] = {}
|
||||||
|
self._lock = asyncio.Lock()
|
||||||
|
self._sweeper: asyncio.Task | None = None
|
||||||
|
|
||||||
|
def get(self, code: str) -> Room | None:
|
||||||
|
return self.rooms.get(code.upper())
|
||||||
|
|
||||||
|
def create(self, board_key: str = "classic12") -> str:
|
||||||
|
for _ in range(100):
|
||||||
|
code = _gen_code()
|
||||||
|
if code not in self.rooms:
|
||||||
|
room = Room(code, board_key)
|
||||||
|
self.rooms[code] = room
|
||||||
|
self._ensure_sweeper()
|
||||||
|
return code
|
||||||
|
raise RuntimeError("无法生成房间号")
|
||||||
|
|
||||||
|
def remove(self, code: str) -> None:
|
||||||
|
self.rooms.pop(code, None)
|
||||||
|
|
||||||
|
def _ensure_sweeper(self) -> None:
|
||||||
|
if self._sweeper is None or self._sweeper.done():
|
||||||
|
self._sweeper = asyncio.create_task(self._sweep_loop())
|
||||||
|
|
||||||
|
async def _sweep_loop(self) -> None:
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(30)
|
||||||
|
try:
|
||||||
|
for code, room in list(self.rooms.items()):
|
||||||
|
if room.human_connected():
|
||||||
|
room.empty_since = None
|
||||||
|
continue
|
||||||
|
if room.empty_since is None:
|
||||||
|
room.empty_since = time.time()
|
||||||
|
elif time.time() - room.empty_since > self.ROOM_TTL_SECONDS:
|
||||||
|
self.rooms.pop(code, None)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
MANAGER = RoomManager()
|
||||||
|
|
||||||
|
|
||||||
|
class Room:
|
||||||
|
def __init__(self, code: str, board_key: str) -> None:
|
||||||
|
self.code = code
|
||||||
|
self.lock = asyncio.Lock() # 保护成员增删
|
||||||
|
self.send_lock = asyncio.Lock() # 串行化所有 WS 发送
|
||||||
|
self.players: dict[int, Player] = {}
|
||||||
|
self.spectators: list[Spectator] = []
|
||||||
|
self.host_seat: int | None = None
|
||||||
|
self.settings: dict = {
|
||||||
|
"board": board_key if board_key in BOARDS else "classic12",
|
||||||
|
"win_rule": "bian", # bian=屠边 cheng=屠城
|
||||||
|
"night_seconds": 45,
|
||||||
|
"speech_seconds": 50,
|
||||||
|
"vote_seconds": 30,
|
||||||
|
}
|
||||||
|
self.engine: Engine | None = None
|
||||||
|
self.engine_task: asyncio.Task | None = None
|
||||||
|
self.empty_since: float | None = None # 无真人连接的起始时间(供回收器使用)
|
||||||
|
self._ai_name_seq = 0
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ 基础
|
||||||
|
@property
|
||||||
|
def board(self) -> Board:
|
||||||
|
return get_board(self.settings["board"])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def seat_count(self) -> int:
|
||||||
|
return self.board.seat_count
|
||||||
|
|
||||||
|
def free_seats(self) -> list[int]:
|
||||||
|
return [s for s in range(1, self.seat_count + 1) if s not in self.players]
|
||||||
|
|
||||||
|
def game_running(self) -> bool:
|
||||||
|
return self.engine is not None and self.engine.running
|
||||||
|
|
||||||
|
def next_ai_name(self) -> tuple[str, str]:
|
||||||
|
name = AI_NAMES[self._ai_name_seq % len(AI_NAMES)]
|
||||||
|
persona = AI_PERSONAS[self._ai_name_seq % len(AI_PERSONAS)]
|
||||||
|
self._ai_name_seq += 1
|
||||||
|
return f"AI·{name}", persona
|
||||||
|
|
||||||
|
def settings_public(self) -> dict:
|
||||||
|
b = self.board
|
||||||
|
return {
|
||||||
|
"board": b.key,
|
||||||
|
"board_name": b.name,
|
||||||
|
"win_rule": self.settings["win_rule"],
|
||||||
|
"night_seconds": self.settings["night_seconds"],
|
||||||
|
"speech_seconds": self.settings["speech_seconds"],
|
||||||
|
"vote_seconds": self.settings["vote_seconds"],
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ 加入 / 离开
|
||||||
|
async def attach(self, ws: WebSocket, name: str, token: str | None):
|
||||||
|
"""返回 (成员, 错误信息)。成员是 Player 或 Spectator。"""
|
||||||
|
async with self.lock:
|
||||||
|
if token:
|
||||||
|
for p in self.players.values():
|
||||||
|
if not p.is_ai and p.token == token:
|
||||||
|
p.bind_ws(ws)
|
||||||
|
return p, ""
|
||||||
|
for s in list(self.spectators):
|
||||||
|
if s.token == token:
|
||||||
|
s.ws = ws
|
||||||
|
s.name = name
|
||||||
|
return s, ""
|
||||||
|
if self.game_running() or not self.free_seats():
|
||||||
|
spec = Spectator(ws, name)
|
||||||
|
spec.note = "对局进行中,你已进入观战" if self.game_running() else "房间座位已满,你已进入观战"
|
||||||
|
self.spectators.append(spec)
|
||||||
|
return spec, ""
|
||||||
|
seat = self.free_seats()[0]
|
||||||
|
player = Player(seat, name)
|
||||||
|
player.bind_ws(ws)
|
||||||
|
self.players[seat] = player
|
||||||
|
if self.host_seat is None or self.host_seat not in self.players:
|
||||||
|
self.host_seat = seat
|
||||||
|
return player, ""
|
||||||
|
|
||||||
|
async def detach(self, member, ws: WebSocket) -> None:
|
||||||
|
async with self.lock:
|
||||||
|
if isinstance(member, Spectator):
|
||||||
|
if member.ws is ws:
|
||||||
|
self.spectators.remove(member)
|
||||||
|
else:
|
||||||
|
member.unbind_ws(ws)
|
||||||
|
if not self.game_running():
|
||||||
|
self.players.pop(member.seat, None)
|
||||||
|
if self.host_seat == member.seat:
|
||||||
|
humans = [p for p in self.players.values()
|
||||||
|
if not p.is_ai and p.connected]
|
||||||
|
self.host_seat = humans[0].seat if humans else None
|
||||||
|
if self.human_connected():
|
||||||
|
await self.broadcast_snapshot()
|
||||||
|
|
||||||
|
def human_connected(self) -> bool:
|
||||||
|
return (any(not p.is_ai and p.connected for p in self.players.values())
|
||||||
|
or bool(self.spectators))
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ 消息处理
|
||||||
|
async def handle_message(self, member, data: dict) -> None:
|
||||||
|
mtype = data.get("type")
|
||||||
|
if mtype == "control":
|
||||||
|
if member.seat is None or member.seat != self.host_seat:
|
||||||
|
await self.send_error(member, "只有房主可以执行该操作")
|
||||||
|
return
|
||||||
|
await self.handle_control(data.get("op"), data)
|
||||||
|
elif mtype == "action":
|
||||||
|
if member.seat is None or self.engine is None:
|
||||||
|
return
|
||||||
|
ok, err = self.engine.handle_action(member.seat, data.get("action"), data.get("target"))
|
||||||
|
if not ok:
|
||||||
|
await self.send_error(member, err)
|
||||||
|
else:
|
||||||
|
await self.broadcast_snapshot()
|
||||||
|
elif mtype == "speech":
|
||||||
|
if member.seat is None or self.engine is None:
|
||||||
|
return
|
||||||
|
ok, err = self.engine.handle_speech(member.seat, str(data.get("text", "")))
|
||||||
|
if not ok:
|
||||||
|
await self.send_error(member, err)
|
||||||
|
else:
|
||||||
|
await self.broadcast_snapshot()
|
||||||
|
else:
|
||||||
|
await self.send_error(member, f"未知消息类型: {mtype}")
|
||||||
|
|
||||||
|
async def handle_control(self, op: str | None, data: dict) -> None:
|
||||||
|
err = ""
|
||||||
|
if op == "add_ai":
|
||||||
|
if self.game_running():
|
||||||
|
err = "对局中无法添加 AI"
|
||||||
|
else:
|
||||||
|
free = self.free_seats()
|
||||||
|
if not free:
|
||||||
|
err = "没有空位了"
|
||||||
|
else:
|
||||||
|
name, persona = self.next_ai_name()
|
||||||
|
ai = Player(free[0], name, is_ai=True)
|
||||||
|
ai.persona = persona
|
||||||
|
self.players[free[0]] = ai
|
||||||
|
elif op == "fill_ai":
|
||||||
|
if self.game_running():
|
||||||
|
err = "对局中无法添加 AI"
|
||||||
|
else:
|
||||||
|
for seat in self.free_seats():
|
||||||
|
name, persona = self.next_ai_name()
|
||||||
|
ai = Player(seat, name, is_ai=True)
|
||||||
|
ai.persona = persona
|
||||||
|
self.players[seat] = ai
|
||||||
|
elif op == "remove":
|
||||||
|
if self.game_running():
|
||||||
|
err = "对局中无法移除玩家"
|
||||||
|
else:
|
||||||
|
seat = data.get("seat")
|
||||||
|
p = self.players.get(seat)
|
||||||
|
if p is None or seat == self.host_seat:
|
||||||
|
err = "无法移除该玩家"
|
||||||
|
else:
|
||||||
|
del self.players[seat]
|
||||||
|
elif op == "update_settings":
|
||||||
|
s = data.get("settings") or {}
|
||||||
|
if self.game_running():
|
||||||
|
err = "对局中无法修改设置"
|
||||||
|
else:
|
||||||
|
if s.get("board") in BOARDS:
|
||||||
|
# 换板子会清空与板子人数冲突的玩家
|
||||||
|
self.settings["board"] = s["board"]
|
||||||
|
for seat in list(self.players):
|
||||||
|
if seat > self.seat_count:
|
||||||
|
del self.players[seat]
|
||||||
|
self.host_seat = next(
|
||||||
|
(p.seat for p in self.players.values() if not p.is_ai), None)
|
||||||
|
if s.get("win_rule") in ("bian", "cheng"):
|
||||||
|
self.settings["win_rule"] = s["win_rule"]
|
||||||
|
for k in ("night_seconds", "speech_seconds", "vote_seconds"):
|
||||||
|
if k in s:
|
||||||
|
try:
|
||||||
|
self.settings[k] = max(10, min(180, int(s[k])))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
elif op == "start":
|
||||||
|
err = await self.start_game()
|
||||||
|
elif op == "reset":
|
||||||
|
if self.engine is None or self.engine.running:
|
||||||
|
err = "对局尚未结束"
|
||||||
|
else:
|
||||||
|
for p in self.players.values():
|
||||||
|
p.role = None
|
||||||
|
p.alive = True
|
||||||
|
p.speaking = False
|
||||||
|
self.engine = None
|
||||||
|
self.engine_task = None
|
||||||
|
# 房主已离线时把房主移交给第一个在线真人
|
||||||
|
host = self.players.get(self.host_seat) if self.host_seat else None
|
||||||
|
if host is None or host.is_ai or not host.connected:
|
||||||
|
humans = [p for p in self.players.values() if not p.is_ai and p.connected]
|
||||||
|
self.host_seat = humans[0].seat if humans else None
|
||||||
|
else:
|
||||||
|
err = f"未知操作: {op}"
|
||||||
|
if err:
|
||||||
|
host = self.players.get(self.host_seat)
|
||||||
|
if host:
|
||||||
|
await self.send_error(host, err)
|
||||||
|
await self.broadcast_snapshot()
|
||||||
|
|
||||||
|
async def start_game(self) -> str:
|
||||||
|
if self.game_running():
|
||||||
|
return "对局已经开始了"
|
||||||
|
if len(self.players) < 4:
|
||||||
|
return "至少需要 4 名玩家"
|
||||||
|
if self.free_seats():
|
||||||
|
return "还有空位,请先补满玩家(可一键补 AI)"
|
||||||
|
board = self.board
|
||||||
|
self._ai_name_seq = 0
|
||||||
|
self.engine = Engine(self, board, dict(self.settings))
|
||||||
|
self.engine_task = asyncio.create_task(self.engine.run())
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ 发送
|
||||||
|
async def _send_json(self, ws: WebSocket, payload: dict) -> bool:
|
||||||
|
try:
|
||||||
|
async with self.send_lock:
|
||||||
|
await ws.send_text(json.dumps(payload, ensure_ascii=False))
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def send_error(self, member, message: str) -> None:
|
||||||
|
if member.ws is not None:
|
||||||
|
await self._send_json(member.ws, {"type": "error", "message": message})
|
||||||
|
|
||||||
|
async def send_snapshot_to(self, member) -> None:
|
||||||
|
if member.ws is not None:
|
||||||
|
await self._send_json(member.ws, {
|
||||||
|
"type": "snapshot",
|
||||||
|
"data": self.snapshot_for(member),
|
||||||
|
})
|
||||||
|
|
||||||
|
async def broadcast_snapshot(self) -> None:
|
||||||
|
members = [p for p in self.players.values() if not p.is_ai and p.ws is not None]
|
||||||
|
members += list(self.spectators)
|
||||||
|
for m in members:
|
||||||
|
await self.send_snapshot_to(m)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ 快照
|
||||||
|
def snapshot_for(self, member) -> dict:
|
||||||
|
seat = member.seat if isinstance(member, Player) else None
|
||||||
|
engine = self.engine
|
||||||
|
reveal = bool(engine and engine.reveal)
|
||||||
|
|
||||||
|
players_view = []
|
||||||
|
for s in range(1, self.seat_count + 1):
|
||||||
|
p = self.players.get(s)
|
||||||
|
if p is None:
|
||||||
|
players_view.append({
|
||||||
|
"seat": s, "name": None, "empty": True, "alive": False,
|
||||||
|
"is_ai": False, "connected": False, "speaking": False,
|
||||||
|
"sheriff": False, "candidate": False,
|
||||||
|
"role_key": None, "role_name": None, "is_me": False,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
role_key = role_name = None
|
||||||
|
if p.role is not None:
|
||||||
|
if reveal:
|
||||||
|
role_key, role_name = p.role.value, ROLE_INFO[p.role].name
|
||||||
|
elif engine is not None and seat is not None:
|
||||||
|
rk, rn = engine.visible_role_for(seat, p)
|
||||||
|
role_key, role_name = rk, rn
|
||||||
|
players_view.append({
|
||||||
|
"seat": s, "name": p.name, "empty": False, "alive": p.alive,
|
||||||
|
"is_ai": p.is_ai, "connected": p.connected, "speaking": p.speaking,
|
||||||
|
"sheriff": engine.sheriff_seat == s if engine else False,
|
||||||
|
"candidate": engine.is_candidate(s) if engine else False,
|
||||||
|
"role_key": role_key, "role_name": role_name, "is_me": s == seat,
|
||||||
|
})
|
||||||
|
|
||||||
|
me = None
|
||||||
|
if seat is not None:
|
||||||
|
p = self.players[seat]
|
||||||
|
me = {
|
||||||
|
"seat": seat, "name": p.name, "alive": p.alive,
|
||||||
|
"is_host": self.host_seat == seat, "is_ai": p.is_ai,
|
||||||
|
}
|
||||||
|
if p.role is not None:
|
||||||
|
info = ROLE_INFO[p.role]
|
||||||
|
me.update({
|
||||||
|
"role_key": p.role.value, "role_name": info.name,
|
||||||
|
"role_camp": info.camp, "role_emoji": info.emoji,
|
||||||
|
"role_desc": info.description,
|
||||||
|
})
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"type": "snapshot",
|
||||||
|
"room_code": self.code,
|
||||||
|
"phase": engine.phase if engine else "lobby",
|
||||||
|
"day": engine.day if engine else 0,
|
||||||
|
"phase_text": engine.phase_text if engine else "等待开局",
|
||||||
|
"seat_count": self.seat_count,
|
||||||
|
"host_seat": self.host_seat,
|
||||||
|
"spectator": seat is None,
|
||||||
|
"spectator_note": getattr(member, "note", "") if seat is None else "",
|
||||||
|
"settings": self.settings_public(),
|
||||||
|
"players": players_view,
|
||||||
|
"me": me,
|
||||||
|
"token": member.token,
|
||||||
|
"log": [],
|
||||||
|
"pending": None,
|
||||||
|
"sheriff_seat": None,
|
||||||
|
"candidates": [],
|
||||||
|
"current_speaker": None,
|
||||||
|
"last_votes": [],
|
||||||
|
"winner": None,
|
||||||
|
"winner_text": None,
|
||||||
|
}
|
||||||
|
if engine is not None:
|
||||||
|
data.update(engine.snapshot_for(seat))
|
||||||
|
extras = data.pop("me_extras", None)
|
||||||
|
if extras and data["me"] is not None:
|
||||||
|
data["me"].update(extras)
|
||||||
|
return data
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
"""FastAPI 入口:REST 接口 + WebSocket 端点 + 托管前端构建产物。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
|
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from .config import CONFIG
|
||||||
|
from .game.room import MANAGER
|
||||||
|
|
||||||
|
app = FastAPI(title="狼人杀", version="0.1.0")
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
DIST = ROOT / "frontend" / "dist"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- REST
|
||||||
|
class CreateRoomReq(BaseModel):
|
||||||
|
board: str = "classic12"
|
||||||
|
host_name: str = "房主"
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/health")
|
||||||
|
async def health():
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"llm_configured": CONFIG.llm.configured,
|
||||||
|
"model": CONFIG.llm.model if CONFIG.llm.configured else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/boards")
|
||||||
|
async def boards():
|
||||||
|
from .game.board import BOARDS
|
||||||
|
return [{"key": b.key, "name": b.name, "seat_count": b.seat_count}
|
||||||
|
for b in BOARDS.values()]
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/rooms")
|
||||||
|
async def create_room(req: CreateRoomReq):
|
||||||
|
code = MANAGER.create(req.board)
|
||||||
|
return {"code": code}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- WebSocket
|
||||||
|
@app.websocket("/ws/{code}")
|
||||||
|
async def websocket_endpoint(ws: WebSocket, code: str):
|
||||||
|
await ws.accept()
|
||||||
|
room = MANAGER.get(code)
|
||||||
|
if room is None:
|
||||||
|
await ws.send_json({"type": "error", "message": "房间不存在或已解散"})
|
||||||
|
await ws.close(code=4404)
|
||||||
|
return
|
||||||
|
name = (ws.query_params.get("name") or "玩家").strip()[:12] or "玩家"
|
||||||
|
token = ws.query_params.get("token") or None
|
||||||
|
member, err = await room.attach(ws, name, token)
|
||||||
|
await room.send_snapshot_to(member)
|
||||||
|
if err:
|
||||||
|
await room.send_error(member, err)
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
data = await ws.receive_json()
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
await room.handle_message(member, data)
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
await room.detach(member, ws)
|
||||||
|
except Exception:
|
||||||
|
await room.detach(member, ws)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 静态前端
|
||||||
|
@app.get("/{full_path:path}", include_in_schema=False)
|
||||||
|
async def spa(full_path: str):
|
||||||
|
if full_path.startswith("api/") or full_path == "api":
|
||||||
|
raise HTTPException(status_code=404)
|
||||||
|
if full_path and (DIST / full_path).is_file():
|
||||||
|
return FileResponse(DIST / full_path)
|
||||||
|
index = DIST / "index.html"
|
||||||
|
if index.is_file():
|
||||||
|
return FileResponse(index)
|
||||||
|
raise HTTPException(status_code=404, detail="前端未构建:请先在 frontend 目录执行 pnpm build")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
uvicorn.run(app, host=CONFIG.server.host, port=CONFIG.server.port)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"llm": {
|
||||||
|
"base_url": "https://api.deepseek.com/v1",
|
||||||
|
"api_key": "",
|
||||||
|
"model": "deepseek-chat",
|
||||||
|
"temperature": 0.8
|
||||||
|
},
|
||||||
|
"server": {
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"port": 8000
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[project]
|
||||||
|
name = "werewolf-backend"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "狼人杀游戏框架后端(FastAPI + WebSocket + LLM AI)"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
dependencies = [
|
||||||
|
"fastapi>=0.115",
|
||||||
|
"uvicorn[standard]>=0.30",
|
||||||
|
"openai>=1.40",
|
||||||
|
"pydantic>=2.7",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.uv]
|
||||||
|
package = false
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
"""启动脚本:uv run python run.py"""
|
||||||
|
from app.config import CONFIG
|
||||||
|
from app.main import app
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
uvicorn.run(app, host=CONFIG.server.host, port=CONFIG.server.port)
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
"""随从真人玩家:加入指定房间占 2 号位并自动行动到对局结束。用法: uv run python side_player.py <房间号>"""
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import random
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import websockets
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
random.seed(int(sys.argv[2]) if len(sys.argv) > 2 else 42)
|
||||||
|
CODE = sys.argv[1]
|
||||||
|
|
||||||
|
|
||||||
|
class Client:
|
||||||
|
def __init__(self, name):
|
||||||
|
self.name = name
|
||||||
|
self.snap = None
|
||||||
|
self.token = None
|
||||||
|
|
||||||
|
async def connect(self, code):
|
||||||
|
self.ws = await websockets.connect(f"ws://127.0.0.1:8000/ws/{code}?name={self.name}")
|
||||||
|
asyncio.create_task(self._recv())
|
||||||
|
|
||||||
|
async def _recv(self):
|
||||||
|
try:
|
||||||
|
async for raw in self.ws:
|
||||||
|
data = json.loads(raw)
|
||||||
|
if data["type"] == "snapshot":
|
||||||
|
self.snap = data["data"]
|
||||||
|
if self.snap.get("token"):
|
||||||
|
self.token = self.snap["token"]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def send(self, obj):
|
||||||
|
await self.ws.send(json.dumps(obj, ensure_ascii=False))
|
||||||
|
|
||||||
|
def pending(self):
|
||||||
|
return (self.snap or {}).get("pending")
|
||||||
|
|
||||||
|
def phase(self):
|
||||||
|
return (self.snap or {}).get("phase")
|
||||||
|
|
||||||
|
|
||||||
|
async def act(c):
|
||||||
|
p = c.pending()
|
||||||
|
if not p:
|
||||||
|
return False
|
||||||
|
key = p["key"]
|
||||||
|
if p.get("input_text"):
|
||||||
|
await c.send({"type": "speech", "text": "我先听大家的分析,保持好人视角推理。"})
|
||||||
|
return True
|
||||||
|
if key == "elect":
|
||||||
|
await c.send({"type": "action", "action": "elect", "target": random.choice([1, None])})
|
||||||
|
return True
|
||||||
|
options = p.get("options") or []
|
||||||
|
if p.get("allow_skip") and random.random() < 0.2:
|
||||||
|
await c.send({"type": "action", "action": key, "target": None})
|
||||||
|
return True
|
||||||
|
pool = options or [None]
|
||||||
|
await c.send({"type": "action", "action": key, "target": random.choice(pool)})
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
c = Client("随从")
|
||||||
|
await c.connect(CODE)
|
||||||
|
await asyncio.sleep(0.8)
|
||||||
|
me = (c.snap or {}).get("me") or {}
|
||||||
|
print(f"已加入 {CODE},座位 {me.get('seat')}", flush=True)
|
||||||
|
deadline = asyncio.get_event_loop().time() + 600
|
||||||
|
while asyncio.get_event_loop().time() < deadline:
|
||||||
|
await act(c)
|
||||||
|
if c.phase() == "game_over":
|
||||||
|
break
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
print("对局结束:", c.snap.get("winner_text"), "| 我的身份:",
|
||||||
|
next((p["role_name"] for p in c.snap["players"] if p.get("is_me")), "?"), flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
"""开发用冒烟测试:12 个 AI(启发式)打一整局,打印过程与结果。"""
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
|
||||||
|
from app.game.player import Player
|
||||||
|
from app.game.room import Room
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
room = Room("TEST", "classic12")
|
||||||
|
for seat in room.free_seats():
|
||||||
|
name, persona = room.next_ai_name()
|
||||||
|
ai = Player(seat, name, is_ai=True)
|
||||||
|
ai.persona = persona
|
||||||
|
room.players[seat] = ai
|
||||||
|
err = await room.start_game()
|
||||||
|
print("start_game:", repr(err))
|
||||||
|
if err:
|
||||||
|
return
|
||||||
|
await asyncio.wait_for(room.engine_task, timeout=600)
|
||||||
|
e = room.engine
|
||||||
|
print("\n=== 对局结束 ===")
|
||||||
|
print("winner:", e.winner, "|", e.winner_text)
|
||||||
|
print("总天数:", e.day)
|
||||||
|
print("--- 公开日志 ---")
|
||||||
|
for entry in e.log:
|
||||||
|
if entry["visible"] is None:
|
||||||
|
print(" *", entry["text"])
|
||||||
|
print("--- 身份 ---")
|
||||||
|
for seat in sorted(room.players):
|
||||||
|
p = room.players[seat]
|
||||||
|
print(f"{seat}号 {p.name}: {p.role.value if p.role else '?'} alive={p.alive}")
|
||||||
|
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
Generated
+829
@@ -0,0 +1,829 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 3
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
resolution-markers = [
|
||||||
|
"python_full_version >= '3.12' and sys_platform == 'emscripten'",
|
||||||
|
"python_full_version < '3.12' or sys_platform != 'emscripten'",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "annotated-doc"
|
||||||
|
version = "0.0.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "annotated-types"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyio"
|
||||||
|
version = "4.15.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "idna" },
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.15'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.5.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastapi"
|
||||||
|
version = "0.142.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "annotated-doc" },
|
||||||
|
{ name = "opentelemetry-api" },
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "starlette" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
{ name = "typing-inspection" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/56/4f/f7c30a73127e0a8bbffe788369b8359e530b01ae06e2757936fa35bc5e6d/fastapi-0.142.2.tar.gz", hash = "sha256:06366626f2e70576367714d9ab2fe8472e6c8456dba69b399f9f797ab5e92570", size = 466721, upload-time = "2026-09-30T09:51:11.277Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/b6/78aaf9141fb46742928c113f3cf6ef2259d538cb02b604b7656c1dc9883c/fastapi-0.142.2-py3-none-any.whl", hash = "sha256:bd5f4d81f1e93a88bcd77caf4dfe3c2dbffc3805407a0007e9a114c18b3a670b", size = 144409, upload-time = "2026-09-30T09:51:09.8Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "h11"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpcore2"
|
||||||
|
version = "2.13.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "h11" },
|
||||||
|
{ name = "truststore" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/cb/f3/1db7aa2bc2524062192bb0e0323969492d1883152a232fe36eea65f4e35c/httpcore2-2.13.1.tar.gz", hash = "sha256:e0aa977abe17e69a3b820a24542a6fa88702676d83880b8d194dcd18408e5103", size = 68071, upload-time = "2026-09-23T07:47:22.372Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/09/ba/a4568248771ce81957bfb7cc600264a40fbcda092391ee1c415c50be4bea/httpcore2-2.13.1-py3-none-any.whl", hash = "sha256:e1e05d4f25f7d7d496bfb96748f6f4b67657b03da069b3a68c36069f3db73d0a", size = 83423, upload-time = "2026-09-23T07:47:19.365Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httptools"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/d2/c3eedaef57de65c3cc5f8dc244cf12d09c84ad258a479055aad6db23206c/httptools-0.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168", size = 208428, upload-time = "2026-05-25T22:16:59.717Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/94/dfe435d90d0ef61ec0f2cc3d480eef78c59727c6c2ce039f433882f6131a/httptools-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d", size = 113366, upload-time = "2026-05-25T22:17:00.795Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/d4/13025f1a56e615dcb331e0bbe2d9a1143212b58c263385fc5d2e558f5bac/httptools-0.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376", size = 464676, upload-time = "2026-05-25T22:17:02.014Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/95/4c1c26c0b985f8a3331682d802598f14e32dc41bf7509266eb2c04ad4801/httptools-0.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d", size = 464235, upload-time = "2026-05-25T22:17:03.109Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/82/6735be2b0ca527718c431cdb8e5f70c3862c0844a687df0f572c51e11497/httptools-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085", size = 449809, upload-time = "2026-05-25T22:17:04.443Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/f9/5811c74f37a758c8a4aa3dc430375119d335947e883efc4664d8f3559a41/httptools-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124", size = 452174, upload-time = "2026-05-25T22:17:05.476Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/94/97b75870dea07b71e3ec535cebe525b08d723152e4c7d13fa887e51f4de2/httptools-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1b4c8e7a489a0d750d91894e9a8cdc295838f1924c0ca903ae993456fddec07", size = 90991, upload-time = "2026-05-25T22:17:06.75Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/88/1d21a36da8f5cb0fa49eafd4b169eba5608d57e75bbcf61845cbc6243216/httptools-0.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d", size = 208247, upload-time = "2026-05-25T22:17:07.843Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/42/cc4feea2945cb3051038f090c9b36bd5b8a9d7f5a894a506a8983e33fd1c/httptools-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5", size = 113064, upload-time = "2026-05-25T22:17:09.136Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/a6/febbb8b8db0f58b38e44ad6cb946e6a255ae49b55f2e8543408fb7501ccd/httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2", size = 523851, upload-time = "2026-05-25T22:17:10.106Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/e4/f90a0df0b83beff265b7e3b65f2a4cefd95792d4be0ac3e16049f2acd3c2/httptools-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09", size = 518842, upload-time = "2026-05-25T22:17:11.218Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/2d/0c9ac76dd2c893841fbf6498d6acec4f2442e1b7067f6e3e316a80e494e8/httptools-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a", size = 501238, upload-time = "2026-05-25T22:17:12.728Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/42/906adc91ae3a5fa9c59c0a2f21c139725bd7e5b41ae6acd485cd14123ebf/httptools-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745", size = 509567, upload-time = "2026-05-25T22:17:13.842Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/0b/4240efeb672751ee5b9b380cb0e3fdc050bc05f68adc7a8aefc4fcd9a69a/httptools-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150", size = 90918, upload-time = "2026-05-25T22:17:15.155Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5e/e5/8cfcabc5546e8022f168be28bcdaa128a240a0befdd03b59d558b4f18bd6/httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8", size = 205148, upload-time = "2026-05-25T22:17:16.333Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/0e/0fb14848c19a686c8062ff9067c1a48793e3224b47bc5b201535b6036fce/httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c", size = 111368, upload-time = "2026-05-25T22:17:17.586Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/1b/46f1cecf06b9bbde8e4b8c88034ac7908989e5ff7a3a388ef38392949c1f/httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7", size = 486447, upload-time = "2026-05-25T22:17:18.564Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/00/258bfc0837221f81d9725c45f9b948a6a6b2994a147a4fb66e85100c668f/httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d", size = 482448, upload-time = "2026-05-25T22:17:19.912Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/ab/d1cef3b5523f4d272a70f42a776c3169a2dddfe3a54de4b2ce4a36341528/httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681", size = 464460, upload-time = "2026-05-25T22:17:20.882Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/48/5d1d072442277bb2b3434e0e60690b8e8c23840ef7de8b6ea54040a536d3/httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683", size = 471312, upload-time = "2026-05-25T22:17:22.085Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/66/b96623b27e51a68199ef4efdda0613cced9233fe3062ac74e50749c5ad37/httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1", size = 90117, upload-time = "2026-05-25T22:17:23.074Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183, upload-time = "2026-05-25T22:17:24.004Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079, upload-time = "2026-05-25T22:17:25.216Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596, upload-time = "2026-05-25T22:17:26.186Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865, upload-time = "2026-05-25T22:17:27.542Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189, upload-time = "2026-05-25T22:17:28.546Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610, upload-time = "2026-05-25T22:17:29.816Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705, upload-time = "2026-05-25T22:17:31.133Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023, upload-time = "2026-05-25T22:17:32.401Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405, upload-time = "2026-05-25T22:17:33.742Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497, upload-time = "2026-05-25T22:17:34.732Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585, upload-time = "2026-05-25T22:17:35.813Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpx2"
|
||||||
|
version = "2.13.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "anyio", marker = "sys_platform != 'emscripten'" },
|
||||||
|
{ name = "httpcore2", marker = "sys_platform != 'emscripten'" },
|
||||||
|
{ name = "httpx2-jsfetch", marker = "python_full_version >= '3.12' and sys_platform == 'emscripten'" },
|
||||||
|
{ name = "idna" },
|
||||||
|
{ name = "truststore", marker = "sys_platform != 'emscripten'" },
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d5/44/474bef2a0e9d90f1715d32cb98b0738695ca17ba324095fb2497ed7fbd59/httpx2-2.13.1.tar.gz", hash = "sha256:e48744a19e3af5ee48313d0ce5fe941d5422fae5705ea922a4aabf94d7800dfa", size = 100405, upload-time = "2026-09-23T07:47:23.052Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d8/9c/6fe8931fd9f381042a9e4c7d5a7b4cbf7016b252bec0c99a49fce42c3326/httpx2-2.13.1-py3-none-any.whl", hash = "sha256:6dff50fabc270ee5fd25d845d0b078ed20564579744d6d962850975996d2f9a4", size = 95597, upload-time = "2026-09-23T07:47:20.995Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpx2-jsfetch"
|
||||||
|
version = "1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/0e5636363151a2a1795e0a77617168b9ca438e1748ec05fc9b5687f93d64/httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60", size = 6872, upload-time = "2026-08-07T00:13:07.492Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "3.20"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jiter"
|
||||||
|
version = "0.17.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9c/1f/8176d92e001f86505424b41664032ae26a882bc9ca41a32c803f373f9195/jiter-0.17.0.tar.gz", hash = "sha256:03e432f226a453851079fb84cd17c6da9991eab723e28d716f14ae3d906e0c12", size = 229037, upload-time = "2026-09-12T15:14:14.253Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/50/17afdaffcc8af4bf4fddf2b6c26d066553aa2221983f2affcde435fc2532/jiter-0.17.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:cfafd7be8b16ceadd298db542cead37cddc211c4c49e04ad2596924df18625b1", size = 290699, upload-time = "2026-09-12T15:11:30.085Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c9/e4/c185d32d5b3657ad84da26c84a9eb15f00aa1b39d6882fcc0052dba2d7c2/jiter-0.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8adca2e793288e5f1bb29279bb439d0d3cfbb50eddca7e7e6ffd42ff4f482406", size = 324246, upload-time = "2026-09-12T15:11:31.3Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/7a/8b8903bfe91a90a8fa1ec9b45d9fda5b6287a386693b69d720a882d73f3c/jiter-0.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30c692d567ba206c7cca38c9d1d0ccc70c9786290173c184d871ca12e9981ed7", size = 343275, upload-time = "2026-09-12T15:11:32.758Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/5d/6821fae2abc71a3c3a84bef8598d31fc4f27d9edfb55bd8f6c08afb8ef93/jiter-0.17.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81c83c0abe614446a283d994d2c07c4f58632dea2cdf66ba9e2921bb8ccd593e", size = 328034, upload-time = "2026-09-12T15:11:33.9Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/51/8e7a963b1c2dfdc01d6228b004f50a2a3d7c46f0549d7b096a5d15ef81d5/jiter-0.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:073dc68c1a700c8fc480e877864a6b6ffc887533e261f4380c08c16bf09d057a", size = 342457, upload-time = "2026-09-12T15:11:36.414Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/27/8b2a267e3bda45d9298331cacfe3521e761f0a2b05ad10a23c6548d08358/jiter-0.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:492f37230bbf9581ab2c17bcda862c249afb9ae2e3ab2dd6db59943bc4cc3153", size = 351271, upload-time = "2026-09-12T15:11:37.692Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/8f/5c74e5e142a6736833d7a991ab04d5c0738038dc44db05af0bb3cd2559e8/jiter-0.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5888fe5abc1ca2fa834a3e1b4c7ef0dcece286a7d7e95a609ef0934b777b9fc9", size = 346052, upload-time = "2026-09-12T15:11:39.722Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/9c/f54920f06d1696e80b1be841d56412871c6856b1b1e3b541b1ed35346554/jiter-0.17.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:84ac78df457e1ee3f7e733bd114823302ae8c5ad5542d7e6647d92ffaa090a04", size = 328915, upload-time = "2026-09-12T15:11:41.065Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/53/080f126863bceb055db9f1fd5431485eb493b35545a3c35e961ac18cd924/jiter-0.17.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7573e80232c5bcf80c24c038cf7e53a463f5c3b1dd1dd4109d66304f4dccc233", size = 337240, upload-time = "2026-09-12T15:11:42.356Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/76/3ab742823a0e0e70e143c6c90a482d9d90396ac2445e7b9483eb7245d3b5/jiter-0.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:11902505d401691720f5785c15b02204248526edee11b635cd6c40cd52b81599", size = 484917, upload-time = "2026-09-12T15:11:43.549Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/e0/8ca71bc8b9cc9ed96c9da565863e00f3bb875a8fb82abcf03e975e067902/jiter-0.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:64846211a2debe7c071d2146d2283d2b0c1c93dc8fd5fb7794faac2ca6061b5c", size = 521868, upload-time = "2026-09-12T15:11:44.704Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/02/81f8719dcedb75713082a2048405376c81f6546b75af8167f3bba01a1ed0/jiter-0.17.0-cp311-cp311-win32.whl", hash = "sha256:c19b9357309b8cc6de8a48fca8e44a8c9c2feaaa2f5896d037fa505d48fcab80", size = 187844, upload-time = "2026-09-12T15:11:45.874Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/8c/59693f348488f01ed12d862e99ab8da14961152d3e9c39b9b1ef363f3572/jiter-0.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:e654b6b04e39c9cb19cb8b04c6ddf1f2db07751fa14156413969fd78bad0e5cb", size = 231402, upload-time = "2026-09-12T15:11:47.083Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/89/fb35e286463cb9f01edc2c4e47df6e5477ee36bca5096414e9ea87985588/jiter-0.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:3ad556afc289f15d2b181b941982d01f06190863c07440185b9f354e1bd2def3", size = 187006, upload-time = "2026-09-12T15:11:48.245Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/f8/07bd8c3a23f7a8a6875e6a820bbffe1483a18f18f9398a91b5495123176e/jiter-0.17.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ebf918dfd6a74adc1b9ad71f63c4ab00902fcd3b7fd39f2e24d871db8d713b91", size = 291633, upload-time = "2026-09-12T15:11:49.431Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/5e/0de4c6f84ffefa6809ffc2d550b9a314365acf7e7ec9b6c7375d49047900/jiter-0.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:61aed66ee042b3b49ef85fdf75714234d055d89d8496ac1c6e47f89e7a30d5e4", size = 321695, upload-time = "2026-09-12T15:11:52.727Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/ac/befe2e82065bee37a0252081666ed2f48c1ac5f5c6c318c2de8168ba393d/jiter-0.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76eb4a5c20e86f9f848286f167024890f2862258a965d254774deb7fc1545ca1", size = 341967, upload-time = "2026-09-12T15:11:54.231Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/cd/9797c1e529746750ae589da7c1a8c24373f00d88e11a989f9e5eb1959079/jiter-0.17.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bcc064f99183a9cbe7f26ed648c352031a74145cd61ed75d34632c73eb46a5a8", size = 326546, upload-time = "2026-09-12T15:11:55.41Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/fd/e6914c38d6347bab4ebff2b1f0c0f191db276e7a1d5c376176757da42fe3/jiter-0.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73b64e69c4150748e020356d958af94bec33c70a0a93d665cfa8f6d580fe1a63", size = 340995, upload-time = "2026-09-12T15:11:58.211Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/7d/611b3abf6f88945b5474da5cdc6d1a185e805ac9bf446bb7766dcda6ea87/jiter-0.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0bc7f684b65bcda9c20434267577db71bf9905ceddd32b60d1d93278d8c8d3a", size = 352188, upload-time = "2026-09-12T15:11:59.414Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/f8/b6e513ecbdf3b3cebe587c2279281ecf775b729a58cf4cc7bdf898ded029/jiter-0.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c21265b251d99bbb40080d178a8953e35601d3a1564e05c4de4c0d2ca616797", size = 345025, upload-time = "2026-09-12T15:12:00.697Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/28/a8/fe26d06c5a6c5a4cfe703c5154c8a140da1305671eb3681aba9422d4f393/jiter-0.17.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:f3d7f7b34114f7ddc6d72a8e882d49de636b35d9fd12b4d420d3c5729f6c9812", size = 329180, upload-time = "2026-09-12T15:12:01.831Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/58/e6d66a26af40a20e62486feb7e222fd50f6e7aaa4f107abd89675dcc835b/jiter-0.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5078ab00664307fab2019b522a93aeb191122789f085daf5fd9e362154021d4a", size = 335805, upload-time = "2026-09-12T15:12:03.056Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/3e/96520aa2fef5ef831d95483a902140bfab83dcac9eaa74f7df61b5e50a1b/jiter-0.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:470e1b1e4c42f1ead2189166a299691871a2df5056c976e7fb96feafaf5f9d44", size = 484121, upload-time = "2026-09-12T15:12:04.414Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/8f/5d9d92fe538bf36ff481a2278c48147e59c1cf8eb2f7be665260665febe5/jiter-0.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:6eb6aedeb7352b8f3b6af9cbd67983840165c00428e63f1b420a85885128ea31", size = 521310, upload-time = "2026-09-12T15:12:05.612Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/06/a09f979b22e652afbc3de66c709b2ba92edcef555f7535ab937c86b4f21a/jiter-0.17.0-cp312-cp312-win32.whl", hash = "sha256:362bb47423886d45a9f705d2d9d4008c6eedd4e41eb1bab4e96fb6daa06b33fd", size = 185029, upload-time = "2026-09-12T15:12:06.994Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6c/d9/98265a005b2473ec2be5a84e2b64c2f65382c673879f1574845cd4bcd77c/jiter-0.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:9bd3caac219df476dd0cc3fe01d2f1581ed588906feac767abd9614c1c12f8b3", size = 227381, upload-time = "2026-09-12T15:12:08.823Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/11/2e05bf5a56e57a543ebb8f585074adf09383e99d7b062dac92eab1f4d57f/jiter-0.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:36ee6e69027396664e59995b9a635a947a5304ee9837279584a0bb8145c8f6b8", size = 183610, upload-time = "2026-09-12T15:12:10.374Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/eb/2c4a8075ed5ea02b56911e9375d4c8d7784572ff4af32e5a99ae0d071044/jiter-0.17.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1b18434638228c0c184281609bf3d9459026a0f1ea48fb76c205e3ef72069caa", size = 290991, upload-time = "2026-09-12T15:12:11.641Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/b1/34bfa29599d420423baac6ff7cada6674fe63d5a7a2ccb3900b904678783/jiter-0.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ec89771f4272b989487a6364e519db6bbaba323e8bbf949ac89a45ea9c18b7a3", size = 321425, upload-time = "2026-09-12T15:12:13.855Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/71/a5ac64a62a04aebd556afadab14a6b730001e16df87266ded943a100a1d9/jiter-0.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e3f052c671d5f425cca5ea5901cf11a831369fba4a55a3862cab93c323b4c3b", size = 343138, upload-time = "2026-09-12T15:12:15.046Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/01/dd/f761e320ea473314cb68612bc6a435393464dbd198051399b36848b4ebf3/jiter-0.17.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:785a216bbaf8f15fc974e964ced7322cd3d774bb0e86949edd78c6bffd6ba35b", size = 325805, upload-time = "2026-09-12T15:12:16.506Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/1a/27d8e40f0fb29bbc7a5adf30907144396a115dbe93d5d8976c054a6dfe96/jiter-0.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d85c558c9f8532bba287a990ac63767c7daf756f0d8c030219f62499b1fa228a", size = 340230, upload-time = "2026-09-12T15:12:17.682Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/c0/30bcde78a28155461f965d16b7aca4ffca6d17494d905f7a0bb072e6c64e/jiter-0.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c23849235d2142ce444b2b8c6eceee9f82f4cc0bd5c9081602e4155c6197807", size = 351343, upload-time = "2026-09-12T15:12:19.337Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/17/91420b156315ae22732f5ee1a7b5725a030aab9dc8fd7dcdacfb4aa588d3/jiter-0.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58df29268a95e910f17db7ec9178eb7f15aa8619aaca3575275c4e6b3f4fe4c5", size = 344990, upload-time = "2026-09-12T15:12:20.705Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/a2/ae6d5672644cc11127970277c9aeb0fa6fae376845587f5b0a8e8828167c/jiter-0.17.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:a277f97eba7d66b1ee27eb5dab5b774ff46a10c78d89a1d3dcce04ce1357c8ca", size = 328624, upload-time = "2026-09-12T15:12:23.859Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/62/45cb1162f6aa586536e4a973fc339d72dc6b08cca030d70a838a307aa778/jiter-0.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe15ddf316f1f1f643347d3a474e74ce61880c79a11ec5dca53df20c071bd3e8", size = 334731, upload-time = "2026-09-12T15:12:25.229Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/5f/45c1574b644da7deda0b7591c349520dcf83ce45b24d7ca19922dab1fc27/jiter-0.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:02adebb7ce6413c44d40af9ad59d1c1cd79630ccdcb6f7bdd2d461e48c03d8f9", size = 483649, upload-time = "2026-09-12T15:12:27.557Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/d3/ebea1ecb5b241c519f192b30215c79a8e47f42f1621acbcd6f8830728416/jiter-0.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:55d0e0e613a3f9ad600cf436e0e2b8057d1b52bcf1d91b2d36ac53451231e6a8", size = 520758, upload-time = "2026-09-12T15:12:28.99Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/e6/682b641ff0765ea9bdc349dbc7d223de5c8af8ec1abda0db3406992f92fe/jiter-0.17.0-cp313-cp313-win32.whl", hash = "sha256:2c45ad7c973ef33fe5114a953377b35a95240f4542c0724d9f781e47dc24bac7", size = 184334, upload-time = "2026-09-12T15:12:30.813Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/d2/9a49aac2b27af4cc5015e368c0cc3588491a532f717a668ffce1f1ac57da/jiter-0.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:a3cebb1fe4a1abb00465f3f8a17e09112603e8b7c59e5c3adbcd9f7815a64acd", size = 226601, upload-time = "2026-09-12T15:12:32.096Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/ce/9a43e9f614608eafa78de22aedcff54cd21324467b5d442d5c9b00244145/jiter-0.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:96b8b0c6dc5d78682f54a450785e075aa929cde768304cad363cd4efba5a82ac", size = 183103, upload-time = "2026-09-12T15:12:34.396Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/01/9e/23065f8e2c7a4c372c1b6f6622e4cfab4dc786cb5150052b1527e6a6a840/jiter-0.17.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:00d783a779c5664e16dbad5e3a3c3a75e128b07dd5f4765159658d9210a50ca5", size = 292210, upload-time = "2026-09-12T15:12:35.613Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/81/67b58647560bc82a4490d722caa8561d7a86a9f45d4fa620b7e5fe282c7a/jiter-0.17.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0619d806e260ecf0c2a64521942c94af5d547c9ec99b55ae4f51b538b5576a76", size = 321512, upload-time = "2026-09-12T15:12:36.907Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/07/6658359a25f55927f7f8bf0e16465dee2ccd0b2a1a5208acc0df8972e074/jiter-0.17.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc0288ce39190ee33fe6e4ec73161eed34e7e2da509b525546ca061778d62b64", size = 343897, upload-time = "2026-09-12T15:12:38.189Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/04/5d50a9f0319cbdc37fd53c27f8c313d46afc34f1b048219ae6d8ea068da4/jiter-0.17.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5a52a430d04225ffde633e6840bf2381d34c019ff98526b5929755b9052fb199", size = 326519, upload-time = "2026-09-12T15:12:39.532Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/c7/d02517832b29eb8275fdd0f4ce0f17b80f58cc4c3ebecd4d9ace990d633d/jiter-0.17.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37f33d327900bf2879613b3363fd48df97b4232d0c41f54bcf2e790c2fc40a71", size = 341369, upload-time = "2026-09-12T15:12:41.486Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/07/499b5f5603501cdd93a73a6a176dfad9c96555a3ae58ca9f8e3acba63dc9/jiter-0.17.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6cf564d43c4388149ca58ee571d0f5ccf875e20d1fd4662fd94cc0d1ea3b10ef", size = 352160, upload-time = "2026-09-12T15:12:42.721Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/75/b04013c7743269d4533ef4e746fc0ed678a143968dd7448658e3f51daad2/jiter-0.17.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:523c499235fb65add25d4bb01b1c4709ce695efdc7deb6c0a7bc515b5c44e0fb", size = 345018, upload-time = "2026-09-12T15:12:44.192Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1d/96/cbb6fd1e42a77c8412ec4643db95059b30cdfc635e387cc9193e098ce268/jiter-0.17.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:455e4ab35cb2a4a91a8404e08fd3c621bae433922e59bf1c494fe20a426b013b", size = 329244, upload-time = "2026-09-12T15:12:45.491Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/67/d3be402f398566a379bf40ae65be5c3505b14d9e95e0802a597ddde7ddee/jiter-0.17.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6871973bfbd4408f7f1c632b30bbb5bbd9671c1bc8650af6823e24b7be13709b", size = 335693, upload-time = "2026-09-12T15:12:46.935Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7f/8d/98e2c4130b93d64f1d67c89060b928d04102549bf05e64451c9e6024f9ca/jiter-0.17.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:77f6aac0137309b31448c1bdcda4c6c77077664a6d018ece8d94019c68a5a5b9", size = 484329, upload-time = "2026-09-12T15:12:48.361Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/5e/8da91e49f0fbca37c3489fb4cf3ad6676d4965f00ae5468bca3a2513737a/jiter-0.17.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:93946d89fa04d5ba64dd323a8dd8d901676cb8a3c81d99ae4f6c051a9b4c3f2f", size = 521358, upload-time = "2026-09-12T15:12:49.856Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/21/5388684a5a38af3557cd9c2424b9827c71809cff24373c75ef9d0d3dfba9/jiter-0.17.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:70f19a2ca8429f91e82eeffb2f51cb87bc2d6e953b009b91a92d29c3a16ccb03", size = 110459, upload-time = "2026-09-12T15:12:51.747Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/ad/58b3a93525d2ffca7f54d9dee441381990082bd1172fbeb8d6a3f72a4dc3/jiter-0.17.0-cp314-cp314-win32.whl", hash = "sha256:71dbd74314c5df52a1bccf7b8bca46d14e943af7a2012e73b23f49977ef194c8", size = 185043, upload-time = "2026-09-12T15:12:54.477Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/4a/1aa520eb6c359b262c14ff995ca7283837208ddfb1202082ce9d73cf214d/jiter-0.17.0-cp314-cp314-win_amd64.whl", hash = "sha256:ac3c6ee3264d6f5c44c617f90bc7e8b9e1587e7d6708c9d8f811cb65582ee312", size = 227163, upload-time = "2026-09-12T15:12:55.931Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/e4/5997f648794bd9b499491d0ff480b096cc9a9c65bdba29f57568e6aa1705/jiter-0.17.0-cp314-cp314-win_arm64.whl", hash = "sha256:6219adaf59711ba7063a52496e8ec6d3fa3e209d7827d83eee3b2abc780a1744", size = 183505, upload-time = "2026-09-12T15:12:58.196Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/4a/84a5ec271d09f7590b6073af5ee4abb44eab4ccace453b7e2c5ce45234ca/jiter-0.17.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:59bddbe6f9ffecc68d641e1e2d619ce64cf8a9e9eeb74e5c518f74fc87abf1b0", size = 321527, upload-time = "2026-09-12T15:12:59.394Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/71/9e1fd0045f5920b4c36be35c3f0f0dfd123668684f8ad352619d7aa44183/jiter-0.17.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6cb41cd1432f1dc19a231cf70b54d42b2c9f05085155859263fce06fa4d41388", size = 340865, upload-time = "2026-09-12T15:13:00.756Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/2b/14627fd2bc377f3dd09491bcace6b90e34b4d7fea2f1f3295031ff91f528/jiter-0.17.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd7790aa79c8b518e512ebcdfce9f11d8ef5f30efd43720c8a19a548b39fa489", size = 325412, upload-time = "2026-09-12T15:13:02.152Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4c/f3/8d5808f7bf0f456bde79e6393587183a0cee5f83d179fe1f7f1eff2ba067/jiter-0.17.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dbbfe4e3c21c8166980cddc5bee1a315df082454f007947dfb6fb73800768165", size = 340473, upload-time = "2026-09-12T15:13:03.485Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/da/1d8c7c6c4ae6b2423b94a81b6b907d37b28f87664e077427b531bf1b5313/jiter-0.17.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c286860abfe8b100cac1c02e225e5776eb9216edd71ba17cdb237da4af32bc9", size = 350757, upload-time = "2026-09-12T15:13:04.828Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/96/c1813dcca15c5a370145a448aaea7d1f83f6f0228a5f1130e79340ee385f/jiter-0.17.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f753eb70b1474a29e635e7542ff7312e6d6b951e0b25e8a2e8c34eeb1ddcd478", size = 345203, upload-time = "2026-09-12T15:13:06.131Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/f7/fc61cbcf2992d169ede13648fc3fd8e2d3171a3669dde43cd4db556549ac/jiter-0.17.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:eae86b1f027031e39db2e0e9c4842221edb7b8cd474d23f87a79b3bd4b651768", size = 328322, upload-time = "2026-09-12T15:13:07.392Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/88/46418a3abbdffb7dc41b314200360f24f75faaeb35573e81c92de322cce9/jiter-0.17.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5bf350452a43173e69e1fc74847c57a60e3d7515807287f29849baa2a85d8718", size = 336570, upload-time = "2026-09-12T15:13:08.666Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/28/b8a55b949be6306df8888e365a8df05441de8a7b11289f6957004302e41e/jiter-0.17.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:da139721f4b7cafdbff580a4f511ea24cb91f4909330c6b926a1ca53836c0a59", size = 482879, upload-time = "2026-09-12T15:13:10.037Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/3b/21d0afa53ba0680962c39f3eb95ed2946f8793369ed44b0c82b490723081/jiter-0.17.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:8079849db9a1371bfd90bad088458a8fb836261879df2233cc9632464ecf64e1", size = 520406, upload-time = "2026-09-12T15:13:11.456Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/03/bcbaf8b6b9ea23c2c074411f8ecfbb02d820abac5d0cb8f4e280209174a2/jiter-0.17.0-cp314-cp314t-win32.whl", hash = "sha256:8f770b0c77e5fac482e1ba03ca1a7e18286bfb213d749932a00a7e4cd5de5e06", size = 184434, upload-time = "2026-09-12T15:13:13.037Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/b5/5d6ce2c93ef6fe1241b37a9005547f9b6d58db1f07f39fe95807d4b98f51/jiter-0.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c4289293e5278d9314b00f15c37f2120fa51d3d68565292e715524c750e775a9", size = 227392, upload-time = "2026-09-12T15:13:14.933Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/4b/1e52baf90187606e33a7b8cfa8f96f5829acd7f01870077eb01059ab76d0/jiter-0.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4dfbfe5a6e1e80a7082af559f66386405025ec278833e0c649f69cbc6e1004cc", size = 182776, upload-time = "2026-09-12T15:13:16.239Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/fc/efe3ac75564ab10f53517958f5ccdc231fc7334af66c76776cb554a88967/jiter-0.17.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:84963d3f395ef5e9a32ce47155e08a7962fa292c159a10cb98b931cef1416925", size = 292143, upload-time = "2026-09-12T15:13:17.502Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/4c/46982118d91f9ffe9714319d21ec4f98d9b7e0cfd9062826c524a54de24e/jiter-0.17.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:ffa0380ad091de7d3fc33e17a97ff479851ee18a0a2a3ee56ff3215cdc886656", size = 321341, upload-time = "2026-09-12T15:13:19.133Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e7/12/9b1ac6ecc6307049913db54839ddba1c11c1ef72c5a8bbb5514bc3b50d1b/jiter-0.17.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:755079792868ce5d4938e83b91a0939b34fb858a1ca65a104f2d771bea57faa1", size = 344383, upload-time = "2026-09-12T15:13:20.508Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/b6/527cc72af836d824e9d4d666e64f0a1ca7eafd662a8da9657b78592172ba/jiter-0.17.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3bf4dc2b84a464117fb097d15a25c58d100d2692888e3b0d92df5b48ed16b7c0", size = 326841, upload-time = "2026-09-12T15:13:21.83Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/41/567f98617e88005b249503b933803f633ec6ba2d427cf4cc35e5c832125c/jiter-0.17.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02a360707033d8cef53f7f3480817a1489177a259ec6ec01e98c37e0b922ddca", size = 341354, upload-time = "2026-09-12T15:13:23.323Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/da/b29cda895b785f7d426e224638a885b6145a08ce853b381f34afe3e88c5d/jiter-0.17.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:300ce01ab0215e3dea4d00090143c909aedc65c0f809b3c07983e1d038f291b9", size = 351985, upload-time = "2026-09-12T15:13:26.526Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/5c/8a73829e7389e72ea298a450f2b3cb58e71a3e464b45f6d8753740f1c4f5/jiter-0.17.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:746243a080b4ca790b8499af3d7cf9825d5f5987933950cd818e767ee353d826", size = 346052, upload-time = "2026-09-12T15:13:27.887Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1d/2f/98d6001026932c095ba440925570123043bed29f5ff56158dfe729a9e81b/jiter-0.17.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:b550585523339b71cb852b811aae49d08d7601ad8ffe9f5dc1562f4c3d22fd87", size = 329159, upload-time = "2026-09-12T15:13:31.569Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/2e/708dc1d2678f092c31c12754e860cd8353e6a85ecbdb1010157edca0da9e/jiter-0.17.0-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0239520085cac678e77a606fd7e3f1c60c371d719790c5e3807388d3da4354c2", size = 336001, upload-time = "2026-09-12T15:13:32.846Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/f0/75a5ae38862f4eaf0fe2f8a9fbf6484c4890df04c06dcdffc45e36bca61a/jiter-0.17.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:eb2295da7c3769f6719b227a237aa6a5cfa6550e478bc838001b592c57e16575", size = 484281, upload-time = "2026-09-12T15:13:35.333Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/32/6636fae811c27c7f93e1b11fb5800de6a5c9e4269a27cf718e0b31218ad1/jiter-0.17.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:e088612ff90ebc9247e1a43074b72835804261c47e6a6c01cb3ddcb55360d688", size = 521300, upload-time = "2026-09-12T15:13:37.101Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/aa/12df7e0b0b1a2602e3d5a5a7104d7d9700f254b400f134a9b50955c4d231/jiter-0.17.0-cp315-cp315-win32.whl", hash = "sha256:0b52d52035b3907c5b1f6277857b29c1cbfc965e24e0f27330dbed83edb591ec", size = 185138, upload-time = "2026-09-12T15:13:38.901Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/ec/3dd2e495032cddde05723c1f4c743b67a23e55d2af244692a7f58f0cdae3/jiter-0.17.0-cp315-cp315-win_amd64.whl", hash = "sha256:10f5558eed511b830488003449d942bd75829ad6257dc58cb9a03e596a7777b1", size = 226950, upload-time = "2026-09-12T15:13:40.17Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c3/c7/ef85704e0a57e9cadb2babc05f6d7c5df4a1c75da1a6ee31e1986b0099a5/jiter-0.17.0-cp315-cp315-win_arm64.whl", hash = "sha256:fa13acf1046f95df808c64b1310705e143fab87aee73ae00cc42d640867fd2c1", size = 183618, upload-time = "2026-09-12T15:13:41.432Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/9a/a4b348349de68762b58d6713973d363ad80a1c741d0bf8def7975f0ecb26/jiter-0.17.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:af2f7501580f274b63c4b2283bc425f5df7edf06ae5b171e5f87d912ff359a20", size = 321155, upload-time = "2026-09-12T15:13:42.716Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/70/aebd6d0b5f0677de3a3d0bdc4a05fac949b97c4ede454c8809f180ac7b17/jiter-0.17.0-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10c5349312e5cb02b7a21e123a57665afa895953f05bf252a9dd4c13a572b7ab", size = 340985, upload-time = "2026-09-12T15:13:44.115Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/82/4c3b49796b5eb62f3f5046f957683f4ba0135fe1a60957c11180512460df/jiter-0.17.0-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:86f3f9343a288eb85a81ef20a752b2f84564296636db54a9fff0b5c8deaf1df2", size = 325670, upload-time = "2026-09-12T15:13:45.901Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/43/f6341ecb4872202a4ef150486fcee0e1ace4aa3da39b71b82061452cdd3a/jiter-0.17.0-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4607ec7d93355fbc25b8dc5189153cf21d66063b9f9cd04dd2774e6e783f9b6a", size = 340339, upload-time = "2026-09-12T15:13:47.442Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/c4/bc2c86e08fa065e03cb2fbc53b367c3640a7d257ef9d877b29118ea636b7/jiter-0.17.0-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10cd64a5720ad7f809ac5466ff1705813f1b6b510f195a73acafba0ac0e1f675", size = 350705, upload-time = "2026-09-12T15:13:48.848Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/67/91f12aa111cca6e3a197c3e36bf60a034bf9f122f6d41112a639e44217d8/jiter-0.17.0-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efe9f61bb30174d2f5c8396445c360c96c44e78164d0815dfe627ccf57849574", size = 345011, upload-time = "2026-09-12T15:13:50.215Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/cb/9f5556e8f6ec89755fb5a709d8eb8270c9a324e31079eda0dfbeca451b6e/jiter-0.17.0-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:370d8fe5bf201dc6925e8a84c81ac7291f74d9fd1778234fc79d517064a5c76b", size = 328268, upload-time = "2026-09-12T15:13:51.809Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/98/153f20680fb75781a490fb849940e2b00f95035c7aa054df592f36ed33fc/jiter-0.17.0-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6b303d88e6a0bda789ec4b7801c7bad68e27230ba1fe4baffc756d1fbd32dc9d", size = 337024, upload-time = "2026-09-12T15:13:53.095Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/af/59/b16c9be3a5035df4466cc72e888188c027562de90a723d290ab6814cb9d4/jiter-0.17.0-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:30793a24a31e968969757c9e08d830cbb15a2cd3c4959b4498b38f4b1c2258eb", size = 482766, upload-time = "2026-09-12T15:13:55.713Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/55/667dea313094024bef082175d6bfe8976f90d1c00c926af9df1d8e0eab48/jiter-0.17.0-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:686c93d86f2b426c803024b805bd161a6cd10e9627c23e901640eab646c0ad8a", size = 520367, upload-time = "2026-09-12T15:13:57.674Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/e3/4b1a43501fb9ed17b01d137e380cb0e8fdcb39a254ce31aa2ab95bc861ac/jiter-0.17.0-cp315-cp315t-win32.whl", hash = "sha256:86d703d9faa1ffc8ae4e9de0fa007712ed2171b5c0d93811a8e2e105ac729b0d", size = 184603, upload-time = "2026-09-12T15:13:59.27Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/f2/b8ee0372b6ebdf1bde5cc44495d5291d17f961065f5b48f8616cc67cac2e/jiter-0.17.0-cp315-cp315t-win_amd64.whl", hash = "sha256:42b0260445251b1bc520a63baa94a32d88e0f931fba234f1764db7feb7c72174", size = 227936, upload-time = "2026-09-12T15:14:00.472Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/b4/923a1215daba959aed8355973315cb3f81f53e0d01c5b211870a27b41f45/jiter-0.17.0-cp315-cp315t-win_arm64.whl", hash = "sha256:d47687806f9c54c84ea38733507081337922beca90ce819c7d852dd485bc0f23", size = 182977, upload-time = "2026-09-12T15:14:01.799Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/3b/05a917204413e2e09906dfa35240c1021227aeb56c7305abea9562c598b4/jiter-0.17.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:eaba834b72d573547b9d966465b3394b749d5e14208cc70acb63aca37619ab33", size = 288143, upload-time = "2026-09-12T15:14:02.998Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/e1/a1cd3c0cf8f79945939e4f8caae9990529f67d7f77f671769f73956329b9/jiter-0.17.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:51e1519d676a9f14dad9c2a411170d43b022ddb7989562df4e849b261ce127b2", size = 281847, upload-time = "2026-09-12T15:14:04.414Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/b4/9b797679e09f4a46c32986aeb3670bd9bc562fc0b373c7a0ee5c5dce1206/jiter-0.17.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ce4feb52493e3513335b2accdcd75605652e4632772d3c8c2f7b86954d7f39", size = 304797, upload-time = "2026-09-12T15:14:05.733Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/4a/0d77415b27a00d970e4e710f7c1de62e96a11c4cab3ed1add0015af04626/jiter-0.17.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29f49b325e0234e4ad9ecca5b861ffbd09b95ccac9bd46fa55841b6e56eea5fe", size = 307815, upload-time = "2026-09-12T15:14:07.105Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/31/4bb27f54333d3b9ef1e5bd3312dc0b4bbe59c68bb0885fdb40583a6b1567/jiter-0.17.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:454c4997d73cc466c71fd565d91e603b0274e48ea0c6b0b7a7aee6967e4ceb7c", size = 288415, upload-time = "2026-09-12T15:14:08.455Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/28/30/879570ecf82574eaea77c5eb10309f4b630dece5f2a556e9814a90ba3f2d/jiter-0.17.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:40d2c240f8f80b5b0f201b29f0ae129c81448c60c772227a41747b5e0026f6a2", size = 279113, upload-time = "2026-09-12T15:14:10.117Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/7a/1f0b8a35fbd079a4f1752c31a15dc99cf277f863747c459be0af39e900e5/jiter-0.17.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e05f5adbf68c4bd11e1610f394034d984152988e84be6f8314235ce6f2139e5", size = 303708, upload-time = "2026-09-12T15:14:11.445Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/8b/d76219ebdbcf3d4209d9d21a0810db4c8d0a6f88e3ee87d30bdea4e90d30/jiter-0.17.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2c0bf24c72fd0491405dce5d40194f2070e9021ce648c1a1d46234b93d848ff", size = 307147, upload-time = "2026-09-12T15:14:12.897Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openai"
|
||||||
|
version = "3.22.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "anyio" },
|
||||||
|
{ name = "httpx2" },
|
||||||
|
{ name = "jiter" },
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "sniffio" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e7/db/b135830095e2ae120ea04b0a53d3a1644eb65554385a8ef4c9cb4de3f5fd/openai-3.22.1.tar.gz", hash = "sha256:2575f961e4c1df8761f8eeb4d78e967de3f9b9f91b22f7120d60e393e970094d", size = 1751626, upload-time = "2026-09-30T00:25:55.548Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/32/74/e3b77d1157c8b9ad815a87d5719545097e5edb06156869c1657fbb6b797b/openai-3.22.1-py3-none-any.whl", hash = "sha256:43ff3de5457a8beb4afa177d4e0ba6841cfadd5219ebc62297fde6c8a9d401a6", size = 2090965, upload-time = "2026-09-30T00:25:53.524Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "opentelemetry-api"
|
||||||
|
version = "1.45.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/1f/dc/e12c1fe1ed8a7b7149777127b1a0e12ce5bd5a81d97408bedc2128c260f5/opentelemetry_api-1.45.0.tar.gz", hash = "sha256:711ede81773c8025c2c03dac0450bc89f3d30aea6eabcc815c570d4e35a963f7", size = 72115, upload-time = "2026-09-25T12:32:28.399Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/b9/040d1a1c7836922828e6480cd2366bb8fe0ebf75b413d2bb51a9b0e7f78f/opentelemetry_api-1.45.0-py3-none-any.whl", hash = "sha256:80e068aba7cd56c8b58512d6a36f8d25cb1dfaa0c0a4cc1c938ccf9f362d9cb3", size = 60020, upload-time = "2026-09-25T12:32:03.192Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydantic"
|
||||||
|
version = "2.13.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "annotated-types" },
|
||||||
|
{ name = "pydantic-core" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
{ name = "typing-inspection" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydantic-core"
|
||||||
|
version = "2.46.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/b6/81d2d19ea0be2c03664381b59f65fa72fc7969decedae00bc2c4ad835708/pydantic_core-2.46.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a1dee1b804ff4d11c663636cf15d2ea47e9f79cd56c033fb1cbf08924842a48f", size = 2074737, upload-time = "2026-08-28T09:57:57.711Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/18/b70da8300e292df4099684ea11b1958043580d2f50d2dc8bf7e542bdd84a/pydantic_core-2.46.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d625a186a65201c23a9e3b8ed9c47e90a026e03256608cc91851c6709096844f", size = 1921751, upload-time = "2026-08-28T09:57:59.265Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e7/1a/0d590341b6ffa4b4aca83508e6b8db4761aaeacfc15a25ca3815876d4797/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8507560a9284e1370bb048ed4282012fbef4e8d109875b95e884d228552061", size = 1948231, upload-time = "2026-08-28T09:58:00.678Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/1d/02eb35761c51f2f7b1b042d6ab4cda6600f0c8c88a2243b3f734376201e5/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f93c5fe914d75fbec9a49209b00da5f08e9e467d69da2b1510c81940cfd10be", size = 2020708, upload-time = "2026-08-28T09:58:02.267Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/ea/f86073830e35d508cc8ddf9c3d9e6e6840fcb88d34bf726b0b4710186f27/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c767f552b21b10f774aeac128e828eafb796adfa1b666a18bf6321453c3a", size = 2194914, upload-time = "2026-08-28T09:58:03.934Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/d7/fc36240d7791ce90939e51608568c33bfdae26202016f9770c229a487d86/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:701b2e04b560eeb4bddf7a25ab8ca476176e34fdbd9a0e18196f0d12d4685f0b", size = 2235622, upload-time = "2026-08-28T09:58:05.516Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/bc/3fa2d76b83162820a17da7f645b28d1cba99fc8e1e5fc6517067ec450fa1/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49776eab08766a08dfff7012f8b422dcd7e25e43b316eedf0477c24fcfa84b7c", size = 2062091, upload-time = "2026-08-28T09:58:07.135Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ab/9a/095d557bb492c90cd8a70a6dd048bf793d433d03d86c81c11e912e4cd049/pydantic_core-2.46.5-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:a2468d93d181667a7abd66e1b64bb9f76f361b0fef8faddf687456453576f5ee", size = 2089904, upload-time = "2026-08-28T09:58:08.814Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/98/7b76b1ad10a19a617a52aaa1d80e159115af939b095e86f8e756fd52e0df/pydantic_core-2.46.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53feb344243bb9510a9dec7bf3cf1b64d88a98af5dc7872a5160465f8b198c8e", size = 2132244, upload-time = "2026-08-28T09:58:10.435Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/32/7d6ca365fadba186a0c8f85de1a701663bce81efd309d9479be58687622f/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cd5214352ae68f3b5e9af7768bdc5253695ee069675db3480518420b3be881f2", size = 2143901, upload-time = "2026-08-28T09:58:12.033Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/09/eb9a6aa57f22fd1541a9c0aa2a1f3aeef3ec65347d33e10a6da2f43e0ee9/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:9432f3598db432cb51c5b37fdbf29a60fcccc79e30d37a05022776a6bc4ab689", size = 2299425, upload-time = "2026-08-28T09:58:13.614Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/f9/548a5bb9d4ba8cd26e26daf48052236f6b38bb61e7b7241fbc3c995719eb/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8feeac04b5794e513e710af2f9c87d49f31a6dc47967bb264a1fed61a8989bec", size = 2318566, upload-time = "2026-08-28T09:58:15.199Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/20/06454d18834c02c406c9133f1a3b485305fd9ee984f9636c2f730bef6a9d/pydantic_core-2.46.5-cp311-cp311-win32.whl", hash = "sha256:892a881d5f68c2b9ea304b7a6c2c60d9343df578a311b0f86b94bc8f1ffe8129", size = 1954258, upload-time = "2026-08-28T09:58:16.813Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/c2/718b9deb4b72453b5d8c7447a3b14cb77bef36917ef5f514e0948a4096a0/pydantic_core-2.46.5-cp311-cp311-win_amd64.whl", hash = "sha256:40375c2d05acec10323e45dfe2077ac44bc74659008614af5069034e2cfc781c", size = 2041030, upload-time = "2026-08-28T09:58:18.288Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/ea/c1d1a5b72d6e1ff7f377a4d9199f6591f095beb5b409a8a5d89f7238d939/pydantic_core-2.46.5-cp311-cp311-win_arm64.whl", hash = "sha256:28a6a556cd3b6066bea827857f9d9cce027c96f776e512f544a581f9e42161f8", size = 2009234, upload-time = "2026-08-28T09:58:19.929Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/3f/76358795aa7a8c6d4f36e2cb828ad1c90ee118e1393a9281664f5aade9d4/pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d", size = 2076516, upload-time = "2026-08-28T09:58:21.576Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/50/26b091836076ce4cb2fac264186936acc069e0595772cfd02a563bc4761a/pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e", size = 1922874, upload-time = "2026-08-28T09:58:23.766Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/09/f0/2a8ce3849e299d44e2d2c196b6082643a3235565a735cb51db7a6261f614/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29", size = 1951772, upload-time = "2026-08-28T09:58:25.435Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/46/ac0dc8bdd9e6048183a14eb127764e7ad9240021c17513074a4711b0e31e/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4", size = 2031832, upload-time = "2026-08-28T09:58:27.102Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/c2/339de5bef7be36301a2231eaa52e62163742c2281f11b5f4892bc79785cd/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a", size = 2208645, upload-time = "2026-08-28T09:58:28.948Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/a0/9ff22b797724262da14427abaed4dd1d864a139693fc5e7809114376a716/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62", size = 2265935, upload-time = "2026-08-28T09:58:30.625Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/a4/eb9409ec0736e50aa70a412f16c204ed149516846912f7e6724d4c73ee53/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2", size = 2066284, upload-time = "2026-08-28T09:58:32.289Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/02/7f6156ffc926857f1c37c07d9a388682865a81830ab6a1b637082c25e399/pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869", size = 2105889, upload-time = "2026-08-28T09:58:33.986Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/b1/e781d357ebe09fc929f995700f1b3503e8897f1cece183ecb1300d4d67e9/pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5", size = 2158006, upload-time = "2026-08-28T09:58:35.647Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/0a/644597d84ab400e50609c192120b85c9681c22d3a20461b9060a79be0a7a/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3", size = 2158408, upload-time = "2026-08-28T09:58:37.38Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/ee/ca3b7b3a4b3769ffe9ce9432a7c9be755de9593a46d3b0d54d0409323e44/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b", size = 2309609, upload-time = "2026-08-28T09:58:39.22Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/52/39fa1f451486019524ca685020390e7ca351832fd874530ba30c8628e6dc/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0", size = 2342618, upload-time = "2026-08-28T09:58:40.89Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/5e/468fc630568c61dcef3cd47ad32ffbeed9af643f49208d1ea86ab4f890c4/pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b", size = 1939475, upload-time = "2026-08-28T09:58:42.591Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/c9/4c19f41b84cf6b622a72fbeed7665b25d47a187d68d47d0d430c07f23268/pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8", size = 2043140, upload-time = "2026-08-28T09:58:44.272Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/af/dd/0c1a050299147c746e5256db16d645ab5efd4f78c59937d581a0524e74a2/pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084", size = 1997729, upload-time = "2026-08-28T09:58:46.13Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/37/5abe39a8372a61d3dc3c1338fc504281c01b32fdb3169cd7187153b56d3e/pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0", size = 2075885, upload-time = "2026-08-28T09:58:47.856Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/43/6323b1f8b217780454c61304bcd2b38ae4762f50754414124603ccc90bb2/pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff", size = 1922768, upload-time = "2026-08-28T09:58:49.58Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241, upload-time = "2026-08-28T09:58:51.511Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975, upload-time = "2026-08-28T09:58:53.166Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542, upload-time = "2026-08-28T09:58:55.017Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692, upload-time = "2026-08-28T09:58:56.767Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633, upload-time = "2026-08-28T09:58:58.531Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235, upload-time = "2026-08-28T09:59:00.421Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367, upload-time = "2026-08-28T09:59:02.442Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420, upload-time = "2026-08-28T09:59:04.2Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588, upload-time = "2026-08-28T09:59:06.048Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866, upload-time = "2026-08-28T09:59:08.539Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580, upload-time = "2026-08-28T09:59:10.402Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980, upload-time = "2026-08-28T09:59:12.396Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213, upload-time = "2026-08-28T09:59:14.245Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/af/1e/ecca01fce348f7e8afa9572441ff6f7d1cc70d21e4859f33944d10877e1e/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:c14ad3bdc85ee7f318742c457ca3968a92126d144b15721c759033bfb06296c2", size = 2075342, upload-time = "2026-08-28T10:00:51.353Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/4c/af80c7a8032dfc897040ad5cb772bebde529a381186499e6e29987f23f8c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0bddb4020d8f04175865ccd17eff3040874fc11fb593f424edb452653b4b947c", size = 1907219, upload-time = "2026-08-28T10:00:53.438Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/3e/54d89e2b092e778716bf6153634ef479e955f48c261090be23aa1e0fb0b5/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2471fd51c61c610e1dcf7de44d7299283661654d11264ab4802b303368d69c47", size = 1953393, upload-time = "2026-08-28T10:00:55.58Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/89/828ee90cda28ce17bdefaa3a6eaf74fe430e113295a10e6126beca559d6c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10ec717381bdbfafef34607824db4c91de69ff085e4fca3b2af91b4fa17e68a", size = 2099024, upload-time = "2026-08-28T10:00:57.794Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/dd/053c2e4303f791f3b8f8a14ab0b22008e8eb21d868c0c90b4f9be705b76a/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942", size = 2062540, upload-time = "2026-08-28T10:01:00.318Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/dd/a18df751a5e37dd51bfad7f68e766999125bebe68c9e1d10a493ad01bd63/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f", size = 1902040, upload-time = "2026-08-28T10:01:02.529Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/13/01d40f9d07ce8a779fd6e0bd8ad4fba91309500dd67b869e2e219d261a6d/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433", size = 1967479, upload-time = "2026-08-28T10:01:05.004Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/04/c81d4841331c2178b6fb09ae225425e110ed72d990c9fe556c4ec03d1013/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c", size = 2111034, upload-time = "2026-08-28T10:01:07.345Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/21/22102e9950b3049526d20e811b95396508377d87651edd2b80d2b3d28659/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ab4b66edffb32d9e951efb3814bd104b8367a7501b81b955cacb5726d897389f", size = 2071333, upload-time = "2026-08-28T10:01:09.636Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d8/18/87aefa427d191e6d3ab1447f1efc1cdcac86af1069239b133e8a0fd7f7c9/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:337639ba62a11acde6ef3aeb08c8ea755f8ef1fe5e513356c0f36a2b0d7568b0", size = 1912713, upload-time = "2026-08-28T10:01:12.285Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/93/fd89e9ad49b1805ca94d24ce1088b7d305f05c35ffafcedb9819d03588a0/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:413a717a410d0c817ef5b786a059415550b3794e1d0c2abffd9efb93a3d9f7b4", size = 2090926, upload-time = "2026-08-28T10:01:15.19Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/45/8e59dab6acf8d35f02f0a958980074f31038968bdb2c983fcae9d1efee03/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e449def1945a462c464331254e5a44fca7c3b4f9aedf59ec2f50f8066dd8e25", size = 2131303, upload-time = "2026-08-28T10:01:17.937Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/a5/e1d4dc5180dd887a9522efc1f8716b8692b7606b1d3273d7862eaf66be44/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a445486499897b88a7d6c310c88ed64dd37b1b59bfd7ae9107490bbb362f47d6", size = 2145128, upload-time = "2026-08-28T10:01:20.694Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/d7/ad493864a7fb21c0c4df98f965e2db430cb25a9d7369b5778d5016c09fd9/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2d330aaba8621b1edcec8ae2c4050f63b84ccf6d98723a8f212e9684713abf0e", size = 2294560, upload-time = "2026-08-28T10:01:23.495Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/8e/b41c84c913f29973a268e6c2b5bbf13c95adb9956c126d10da11ba3b2bef/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b6acfb46a814762367fb7ba0828b0a17d441b92ce249a0e007474c9072662dda", size = 2317531, upload-time = "2026-08-28T10:01:26.334Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/1d/068464f23075f66a8f1b806935e9cd9363ee446636ea70d2c22ee8659dbf/pydantic_core-2.46.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d0a24b40877af2de4950252be9d21eaf7fb07660f3c2cae1f56c6b599ada5266", size = 2140686, upload-time = "2026-08-28T10:01:28.947Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dotenv"
|
||||||
|
version = "1.2.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/74/26/2fbeedb218a787a5eea551c7532cac4e009f83d689dd2faa0d0353473f86/python_dotenv-1.2.4.tar.gz", hash = "sha256:f0d53e69935a851c0dcc78f3ab7aaccd8cabef0b92382b576b824212902873c0", size = 60824, upload-time = "2026-10-01T05:36:10Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/d1/38f3a3405989a89ac18390803e70c6ad7c7760da4f9b83cbeca0c44a0c72/python_dotenv-1.2.4-py3-none-any.whl", hash = "sha256:42269a8a5b3fd54ffa6f3d84b18abed50064717576b4ecf03dc4a55d8aa04fdc", size = 23266, upload-time = "2026-10-01T05:36:08.633Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyyaml"
|
||||||
|
version = "6.0.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sniffio"
|
||||||
|
version = "1.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "starlette"
|
||||||
|
version = "1.7.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "anyio" },
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7b/2b/3850dc6bf7ef71b088962eba31dafc6cffd2f96e577ebb0bb316df96da3e/starlette-1.7.0.tar.gz", hash = "sha256:c79f74ea63cff761804fbbfb182f1e0b440c2d07b164d24700c5a1bab5d6ff5d", size = 2736246, upload-time = "2026-09-23T07:30:26.35Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/d6/1ec1b290f9e0fb067899b61e1d37a30c923068bad260b216dbe37a7d2967/starlette-1.7.0-py3-none-any.whl", hash = "sha256:67f8e99895493dd2911a03f11314af6ceebeae4e704bb9f43dfc6a9db151c93e", size = 78980, upload-time = "2026-09-23T07:30:24.567Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "truststore"
|
||||||
|
version = "0.10.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.16.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-inspection"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uvicorn"
|
||||||
|
version = "0.54.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "click" },
|
||||||
|
{ name = "h11" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/da/34/30e9280707135d2cfc589dfff3cb796bd07a3aeb1a3e415ba09dd89d7bb4/uvicorn-0.54.0.tar.gz", hash = "sha256:a2e33cbfaa0306f8e6b0c13e0cb89d7d7a2da3e62b90c66e18c33d9807b28620", size = 112283, upload-time = "2026-09-25T06:52:37.601Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl", hash = "sha256:505bdb0f318731d45f1f712071fc781a8981f6847a31c902c9f5e652d4f67faf", size = 87427, upload-time = "2026-09-25T06:52:35.829Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
standard = [
|
||||||
|
{ name = "httptools" },
|
||||||
|
{ name = "python-dotenv" },
|
||||||
|
{ name = "pyyaml" },
|
||||||
|
{ name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" },
|
||||||
|
{ name = "watchfiles" },
|
||||||
|
{ name = "websockets" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uvloop"
|
||||||
|
version = "0.23.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/fa/42/02c739ce85fb2ee8d99212c61417da8140c6b87e9d97c430bea520d76044/uvloop-0.23.0.tar.gz", hash = "sha256:28d160f51ab4da3b187063652e643dea6831072add4adc1e6d62afbe73b6be27", size = 2559185, upload-time = "2026-10-01T03:17:04.4Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/b1/948067eab45d5307f04b34e50eb7bd1f7352aee866fa5f0706b061ddacf0/uvloop-0.23.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:24c58ae4a83e93a04c504bcc678125e36a0bfc44af928ad69444880c60f187a5", size = 1415276, upload-time = "2026-10-01T03:15:32.634Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/6f/ee3ee84c5d27f2f0a47ae8b67a6adeacf9841b193c0e07412a1403586ce2/uvloop-0.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0efdd55bddbd36bb2fcb842d64c0d5f6407c6958c68088cc25df8c09edc5b5fd", size = 779533, upload-time = "2026-10-01T03:15:34.062Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/0d/b5f69dae3736d96a8753c6ecd32d676ecd212be7ba3252e9c379ad9cc05c/uvloop-0.23.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8fcd721113260ffb5e38bf14a8725b17d431f34209f7d1c7005b667946e630b3", size = 3896377, upload-time = "2026-10-01T03:15:35.816Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/fd/8cbf6124607863399008ae4b0d2bb50c22ed83526deec28dca08d635eb6d/uvloop-0.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ab17b3a8aa754be0de0e397f7b95f13b14e56f077a4c6ae295e3d4afd199b325", size = 3956355, upload-time = "2026-10-01T03:15:37.688Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/7a/b73007866e7198519067a1f1afc343b4973ae924d2b7afcea67c44320a98/uvloop-0.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:80cac5cb90ed7b9b72a217a1d6982b15b829cdbd0ee6bc19b93e3a9e47fb0ac9", size = 3755618, upload-time = "2026-10-01T03:15:39.27Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/28/e50816f1ce38b97b28d62bc4adf7c82c33b7c68fa902e41a39adc8a3d189/uvloop-0.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:93087a845cdfb35753e539354ac9551bdd2ff528c202a98df0ae46e852bcf021", size = 3863192, upload-time = "2026-10-01T03:15:40.882Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/98/04e766a6de99e6f7f955ecb7829e8d5a557de3427cb85be2236de54dda0c/uvloop-0.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:93935ab27b6eaef4c3e5489aebc84284f0644592f7ab516df60ee1b27eaf5eb3", size = 1393055, upload-time = "2026-10-01T03:15:42.526Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/8a/499e7b863a848ede009539bce39806b66205da5f8779354228e785601144/uvloop-0.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4448e9124537620f9c25d004c227bb5104440b58955c19bbd312d910af919a63", size = 768909, upload-time = "2026-10-01T03:15:43.974Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/95/a880f8ce3b87ac5b307c354e8ee480be4658d24bf01f87921d57e3530b4a/uvloop-0.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7548ede3ee908cfabc0d068106e303a9a2d811af959cdf6ab85676344cedcda", size = 4419106, upload-time = "2026-10-01T03:15:45.551Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/27/c1d2f9fa977f8f42ea294604166df10e0027e6dc6cd17f85ede386c9bf36/uvloop-0.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:090865d8ce7a03986755a3ce711b7dd0d4b44eb14ab74368b717f3fad1180208", size = 4532597, upload-time = "2026-10-01T03:15:47.258Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/dd/2cb6a2c8a30ca55c07a882dd4ae4ceae0fa7d8c15b25b3b7cb9a4b6cf4ca/uvloop-0.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd6f2f81c7b9da99d301c0b16b82044e76fe887086e42e1590ecf520b94dbdac", size = 4230048, upload-time = "2026-10-01T03:15:49.119Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/52/29989cbaa4022dc4ef35c1dd60a4ab989e4c2065f341ed483ae71d2bd950/uvloop-0.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a6ac96da66c35bf789bdcde78a88dc7d56b7907d8379648c54adc1c61594575d", size = 4394152, upload-time = "2026-10-01T03:15:50.829Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/83/eb980d64e6dd5da46d4dc35755fa6afd6b5b47141437cf89615f1117c5a6/uvloop-0.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2dcff2d69be43e6559e5dad2c5a7a2dbfb60e05a77311b6c4b7a4a8123d86c65", size = 1412726, upload-time = "2026-10-01T03:15:52.49Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/c1/02a725e7698134c647904bdee6589e2be14a0e7fc9942c74f86e2b90d48b/uvloop-0.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:19c64108b507cd0bc140e400e3396bacebd9d504956aa7726272bf6de7d9aabb", size = 779071, upload-time = "2026-10-01T03:15:54.02Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/1d/cde53c79e8c01884ad1cdca8e407e086d523362cfe4139e2c2a8dde27304/uvloop-0.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1748321e3c59a14a75404b1ae8d5a8d81c4e201803ea0e14c1b6fd84421024b5", size = 4395323, upload-time = "2026-10-01T03:15:55.549Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/54/b12915bebbf99d7ae0796211e7f5977b95f069830dca45dc1a346d84125d/uvloop-0.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2cba180d6451822763eda8364f342435a873bcfb3849cbd82fdeca248ca65eb", size = 4480449, upload-time = "2026-10-01T03:15:57.362Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/8e/da6de68c31549a052a105fc76f5a9a204f6df22cb0909440aa4dbb06f9a2/uvloop-0.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dc61e4f9e37b507069dc7e659ae28bca7adcb04c993c3508214315d12c63f848", size = 4219177, upload-time = "2026-10-01T03:15:59.351Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/c3/1b53c6a89dc9c9d5cb75eb9a0b891ad69b32e1421ad3aa01617a9cbdcc78/uvloop-0.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7337b06a9f9ed9ea3049f04b76f65819db9b19bb832ee598e97b388eadf25e5f", size = 4346132, upload-time = "2026-10-01T03:16:01.064Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/a4/00e85345871c59c834a23c136c1771205856028ecc8ba940b3951178e59b/uvloop-0.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:b90397a50ad6332ed3e459c648ac20d182cce24a557354363ad85fc9ea4a17cd", size = 1421363, upload-time = "2026-10-01T03:16:02.599Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/a9/e5f0f3cfde30af3ec32eba8ec07bccdba2b5116afbd1ecc53edfeb0a0790/uvloop-0.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:be53e1d5f83de43dc175c87612ecc128d444b38e5c56cb3f807f5a73d6887476", size = 785177, upload-time = "2026-10-01T03:16:04.018Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/79/9ddf78f8cd75a15c14a09a57f59c587b8cd9d82802c5c8368b9c3ebefa0b/uvloop-0.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b3cbc4f96ddfa1fb88a78a69dd851369825b7816d9702eee8c4461505ba172e", size = 4381060, upload-time = "2026-10-01T03:16:05.642Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/20/57d63c44d32326878fcad5c63854afc9deb394ed95673c1b1a429178c79d/uvloop-0.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31e0cf90bc8fd88784f6802cdba968a51fb1aec1cc3feec74d862b2d371d1330", size = 4418891, upload-time = "2026-10-01T03:16:07.326Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/c5/0795abecda2cc3dfe41033f880a32a9ff103be4e6b177ac736833c153a0e/uvloop-0.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa8ed556fcc87a4091cf61587ef172fa104323dc89ecc085a618ba7ff8629a8f", size = 4214811, upload-time = "2026-10-01T03:16:09.13Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/18/9010dacd5221eec1bd79a4a83ac68f3db6a42d7bb657f7b640c4838ca6b6/uvloop-0.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f3fbfe82829d8e381426a289b87e59e585278728361db9ce975b88b51f64f410", size = 4294876, upload-time = "2026-10-01T03:16:10.875Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/08/f6384a03c771d00067cba4f542a69b2fc1a982e9fd78b357c2f788678d72/uvloop-0.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:7e35c9bc977760981693e1a7a51493b58ee5a501f9ebb1e547565ee40b6c6208", size = 1494811, upload-time = "2026-10-01T03:16:12.399Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/01/756a4fb24a449f313cf4a153eb0c6210b49cfe5539255ec9fb1e17d2c4ef/uvloop-0.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5bb9be71d9ee39b4359b832f9569518ec9bc08704194034e79e4958e6bc4d46d", size = 819396, upload-time = "2026-10-01T03:16:14.094Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/45/e314b0c600b14f53dad3a3c2d7a922a249a88225fd727652b53e1854b9dd/uvloop-0.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e84575f11873c109cf3962ad0bdf679094466184125f4cadcc41a73febff41f", size = 4734966, upload-time = "2026-10-01T03:16:15.815Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/0d/8686a7f0b1b2d55ebd770ba21f8e0e4ffa0cde5ab738f43ffb8264499052/uvloop-0.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbbdb8fcd5e7062e546eec1ac78c28bb21ae7df54c18f8e4b06e15a18d661a49", size = 4584963, upload-time = "2026-10-01T03:16:18.198Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/b2/034a2d47e435ac02357c42956246887167bdc0357bdd6ad31c5f6d94497b/uvloop-0.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:76345f51367fb1f23e08605c6efb18374f669be5b223658fbab6b17627950507", size = 4421388, upload-time = "2026-10-01T03:16:19.953Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/77/131f4b583e6b4b715c404a66b51c812d701db20f25c9018b188a2b00062c/uvloop-0.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c7ef4701a96553514b2688e342ef1bf2beae6cfd172d89a76c768292aabf405", size = 4402414, upload-time = "2026-10-01T03:16:21.716Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/3d/ee11f4718ea1280595c67ed25c83d4c92115dc100bbdfd192d3ed9339168/uvloop-0.23.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:f1341c6abcee1c31277cfe28d34e46196f2143ec3d755e6efe7452126e1f626d", size = 1418095, upload-time = "2026-10-01T03:16:23.241Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/0c/7ca516a0671418517d79a09d3ff2ccbb44af94c75711afa6e4cf58aa6f65/uvloop-0.23.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:e095f9e105af76593b4c183bb0bcbdae64bd913a59ec595732dc108b48730ab5", size = 784837, upload-time = "2026-10-01T03:16:24.666Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/35/95/75d4e28e596d505b7ae11de517646b4ca3d369fb8537ba755410380da11a/uvloop-0.23.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f673d835bdb1a60229cc3609a113fd2c9ce3f4a3c75ad4eaed111180c00199d2", size = 4380276, upload-time = "2026-10-01T03:16:26.389Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/99/68daf827ad62efaf4667d1f3fda127046d42161178396bdd93aab3684082/uvloop-0.23.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3f23f403a273900d57de6ee5ca0614c650f7f58563065dad1a4744498960e53", size = 4451496, upload-time = "2026-10-01T03:16:28.364Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/69/f67e696ee688f426a96f99099bae26fec14a1d0fa75dccdd6518ee267c0c/uvloop-0.23.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:cbe8d03d4efcccdb7fcedecbaa1e1fa02913eaf3a74cb933634a6bc6d2ea9e2a", size = 4212541, upload-time = "2026-10-01T03:16:30.014Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/6a/c8c436a9d7453297b4be70bdf6a9f9fc9400da45e0059ddf7b28ab63f4c7/uvloop-0.23.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:4f1798f56c6f4ba5ac11fa2869e5717926e4470d97a1dd42b4f59219d43b5027", size = 4319377, upload-time = "2026-10-01T03:16:31.705Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/2c/8fc15a03489299aab8a6212dfe0f137dc39836f915c87f7fd9d9ddd814de/uvloop-0.23.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:098a85e1393ef5202767b7e5fb41a32cd8bd81e6ee4af364c179801c4aa3f6d4", size = 1493428, upload-time = "2026-10-01T03:16:33.859Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/7c/05e4a210790229607f71460fcb2ed4a2c7bc72668d8a928ce577c22e38f8/uvloop-0.23.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:5a2bbad3a63007f7e9524d4903ba04fee252557c2acd86f9a3d4f91786695254", size = 818115, upload-time = "2026-10-01T03:16:35.45Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/65/14/a40b11c6c024213803b13955664a15754c72f64c873a33d986b26ec9ff5b/uvloop-0.23.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a08875543bbd4519faf30497506c9cda8a48470467ffdf967c7313c7a5981a8", size = 4734149, upload-time = "2026-10-01T03:16:37.025Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/83/f421a077712c1e87603bfec62744c3cd3a2f4b47378025db3d740df9af0d/uvloop-0.23.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12634f15e6625f78b3f2922f91404c4d7173487eba11746764153f556e9852dc", size = 4661763, upload-time = "2026-10-01T03:16:38.719Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/62/25dcaa6b7e7b48f82ce633854ce96597ab768f9650931f4f86c572de392c/uvloop-0.23.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:378188efbb1524f2219d05246a3e1e5907217848d2882144dff59585f1b81d55", size = 4421324, upload-time = "2026-10-01T03:16:40.488Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/46/04628239b43dcef703af314202a3307d6060918e2d76aa86c5b1188f5551/uvloop-0.23.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4b8e207c67d207a8608fec57e116511030af3495dc0109b8c333cf9cb412b16f", size = 4462501, upload-time = "2026-10-01T03:16:42.359Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "watchfiles"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "anyio" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b3/68/e6aa0b77d217b31f8f486ec0cdfe5e00e6e38dc0be657e7d85819b9faf0a/watchfiles-1.3.0.tar.gz", hash = "sha256:99aee4a07847c06820765fd7b1b49ceac4f3f711ccb7d104655a33231de1c207", size = 108119, upload-time = "2026-09-21T09:08:55.664Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/fa/c0b840d5d8bafe640925408ce11095948c8945bb61be1e67d0ab629b872a/watchfiles-1.3.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:000b9688fc8133037a8b075c8ebf98f32844ff8964dda61e85c1db547dafc441", size = 399703, upload-time = "2026-09-21T09:08:13.518Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/8a/894799b485fe9473ad10422a0d9668e53fb78e3a2b5cc6061159572844ad/watchfiles-1.3.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:bbc1198edfdc90fda0600f825aa94150f428dfcbf8138746f55998e0e660d64c", size = 397647, upload-time = "2026-09-21T09:08:15.16Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/61/f277cd5ead05f5d37b98b41fbdd0c943ee0c04d49738f62e902cb291cf43/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4af9c464d410e8c44b58ddf1cbec7c3a02c1cb2c6c80df9c74659ba2fbda0ab5", size = 454994, upload-time = "2026-09-21T09:08:16.587Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/66/ea4c01382975e53b1ac1b0a0ea1b353416b8127fb6e04f5c1c313119aab6/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb94e40b9a0db19636b5e3fed0a9803452ecc7381df13b385272abb779ee83ca", size = 460105, upload-time = "2026-09-21T09:08:17.961Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/c3/1a501b597817096c9831c255f6270c124ce0e2faeb74124be24dcc93b86d/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7398930a2b76b9dc67a4e6b8bfc7baf30946892a86f417a3920c07bdd6febc30", size = 493761, upload-time = "2026-09-21T09:08:19.327Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/16/77352d05e152c7786a016e9f1decdb4507e8aa5bcf3343c72142a0413d14/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:025b108f2d5cc2799cb941f79380df3e8b22bfb589c218e801767fe78d598c95", size = 577500, upload-time = "2026-09-21T09:08:20.723Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/fe/81001c55b24466fe9911135e50269fd3a29f43e446627a92adfab54553b6/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be9ef3cd403d756a304a0a08a112163c63da7ec72c8c9d083c98818291ff8f29", size = 469209, upload-time = "2026-09-21T09:08:22.346Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/cf/e3ad894ed5909a7c44b0707e89bc25d19a3d78b3a7aec846119443b6ae56/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5768b49e426fd5b550b012c866db347cdf15c398ef98dc557b6e6b72fa74cd1", size = 458353, upload-time = "2026-09-21T09:08:23.7Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7f/f5/2f106829e6e2bc91ad74614730f6df484202b9a4d20d927dbf2f08d37d44/watchfiles-1.3.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:d36c72fcdc08f143d87316a4c4c3b064e340da2c7383d39803950740ed418935", size = 468707, upload-time = "2026-09-21T09:08:25.167Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/37/2741ab776b978669c1f3bba1e573f1a184f727ab8b4d3a6d9c61090800ed/watchfiles-1.3.0-cp310-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f0c9865240e065a2247f4b5248528ae3d01e0e8ae071462c250c884dc16e3ee3", size = 632510, upload-time = "2026-09-21T09:08:26.532Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/64/db4a4684275fe9b696ee5118c194705c1423b3d42181c3f8ee70194d5c73/watchfiles-1.3.0-cp310-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:380f513d26cc2e598266b88d66456e07d67ef4be8f0f435a1154d9c44b43d509", size = 661723, upload-time = "2026-09-21T09:08:28.004Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/80/95ddb24f6a2f595c031c24432ab7b8aa7d6d1ada0c6d63a552166e06e8d1/watchfiles-1.3.0-cp310-abi3-win32.whl", hash = "sha256:509d9f74d2bec5c1f4cd868ddcfe0c9601f7ea53066dfcb19d8ede742c4de661", size = 277761, upload-time = "2026-09-21T09:08:29.335Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/a1/1c4a1b3c8030cd03206232612be8a6e8799093490c220b0fcb935d02b75a/watchfiles-1.3.0-cp310-abi3-win_amd64.whl", hash = "sha256:1acabde19b67e673274e89a04d613b7fb1f122e2c4b8ec9a581e59c149264b61", size = 290643, upload-time = "2026-09-21T09:08:30.45Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/e0/d2db577c632d26d08078c4390dd870b8555c4e558df25d22270f0166c71f/watchfiles-1.3.0-cp310-abi3-win_arm64.whl", hash = "sha256:d978cc1dd7ba44f5590d7de74f7a9c8be7262d9e469c2c0efb8bbfc2574321dc", size = 285510, upload-time = "2026-09-21T09:08:31.561Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/55/a122743607939429ab6a7272672f49303aa02bbd34c3f06676765a3861de/watchfiles-1.3.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:e0204f90677b7fb8d3a824279a71a01166bea9f6e53923b8056592d87b33b0b6", size = 395978, upload-time = "2026-09-21T09:08:32.747Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/9d/0ee1b0ea42acfe72afa2484f0222e2386dd09992e9034a81e63cc3cdab0a/watchfiles-1.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7c1125e99f84934a92996ae343501285617373acd66f16b59fd5654b27ad8d80", size = 387997, upload-time = "2026-09-21T09:08:33.953Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/f2/4319c6b683e8bd28465dfc53c750617a2a07f072eb2e3ab0182ccf812229/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a85e243c87109d9f48a2e0d10d6abdee8768cc114848b32281ec83d7f81d86ef", size = 448067, upload-time = "2026-09-21T09:08:35.104Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/71/9f37ac192a51a24de3a2d7fe9aac1e99b2b2668300674ff7a9f85d95beb1/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd538c71766c59c732e2c5cedc39323c99c11897b99f93eac773438e54af206a", size = 452881, upload-time = "2026-09-21T09:08:36.238Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/34/07/1224667c32a4a96876ddd642da69733cbfe6a15897db0865aec5a882cebc/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da213822ec9082b62cadf3008e07a8c690ec7a3c89f6dc5e55a8cf07b56a1b26", size = 484802, upload-time = "2026-09-21T09:08:37.717Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/4d/7f29cd87e1f826df86fc0418d0b8f370ca0311c29c618406dc492aad9951/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4593392e87669836670f87d62fc754059eaeb0e6157af44060a46558cebe5a3", size = 567522, upload-time = "2026-09-21T09:08:38.94Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/ac/3aa1f84abd4dbbfbae99a817dd931f77b439261c911189cad4c30fdfd470/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26b81bc515a0f03f66a69a6fea86eea54c8eca4173c46dd7222569f79dd2f977", size = 461997, upload-time = "2026-09-21T09:08:40.142Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/1c/27c66764d5b0dbb70a36770a32eca217eba739c28b24e8583a035087a3d7/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca39661934749df580d89f3dde266f2af7c6968f4c21c72ce2dc387d299e6aa7", size = 451856, upload-time = "2026-09-21T09:08:41.305Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/b3/a45827084d953c1d0dd3d75abf85dcac26a0f5d115ad2fe0b7e9e931573b/watchfiles-1.3.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:04c5500faa725d69a99b0f63750e80795e455bb951849e9c98a811b4324997c4", size = 459889, upload-time = "2026-09-21T09:08:42.58Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/3d/07740d23ce2bb23419bb24055c435a9abe5fe03efdaea1ba0720b831aab6/watchfiles-1.3.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:ec3cd4bb181a7b6329134204c2277c7144ab5f62cce9c5335d6f901ee7ce8d6a", size = 624482, upload-time = "2026-09-21T09:08:44.064Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/91/46/9540f8123eaec86df989f26d7704cad0448d9fb7271763a4f8f9fe23617d/watchfiles-1.3.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:e5871d4a7f788a9f64fb06f5793389d29d171e5c32d37c3ebfd520b017cc7694", size = 653724, upload-time = "2026-09-21T09:08:45.257Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/9d/b9deb52f1e4528ffe44338c5ebab95c32ed79203082da661de50288e130a/watchfiles-1.3.0-cp314-cp314t-win32.whl", hash = "sha256:81c989d7267bfb7b676cd32c83bec97914e396870983cbf493e29415037bff5e", size = 270863, upload-time = "2026-09-21T09:08:46.518Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/59/8b1881afccbafda06248be4185e28034dd73c80c85e4da24f2cb0c1321eb/watchfiles-1.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d754e049006e81a9be49dc78ba7802081cee426e968cbc63ee19e164fa2d0e39", size = 286879, upload-time = "2026-09-21T09:08:47.874Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/dd/41c12ae9745d5d72f709333287679d71cbba68ad7678fed872207bba94f4/watchfiles-1.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a5b631db08fd3032db57ec6a92d1777cd60f88803287e4e941a4858c53aab1e0", size = 276924, upload-time = "2026-09-21T09:08:48.976Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/eb/289fec71eba51d6c3ef3a28c81f7213f4e860084ffae5abc866933c564f0/watchfiles-1.3.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:45726c6b5a8d67c8087eb2933c4398948546d04206ef40f629dc4881d65d23d1", size = 396932, upload-time = "2026-09-21T09:08:50.217Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/cc/a3c3f557992a20a5bb24607f0da44d39cf6d6ba3f8f6ed7cb31f6c73371a/watchfiles-1.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:6398d250994baff581147ccba846223076fe6492ca93c0d1b3588fcf5355eb5c", size = 394852, upload-time = "2026-09-21T09:08:51.412Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/55/65/d45763a5e1108a65af85a217af2930ebf1081c73fd8169d3f75e586992bb/watchfiles-1.3.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:591de392458bca26f6130fea4e667c06b657e873ad334ea06fad5a79f5086cef", size = 451789, upload-time = "2026-09-21T09:08:53.144Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/96/165c6e540bf765ca1115062017adf861d0816525f02464ba71313fe7cf92/watchfiles-1.3.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ec46a1ff4c1d81405a5fc645abdbff5e4e9388f7c9a6cdb392316a7c29b884f", size = 456919, upload-time = "2026-09-21T09:08:54.372Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "websockets"
|
||||||
|
version = "17.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/18/72/fba934cb3dff7a85d811820efffcd141ddd52b5a2a01637f64551373ff4d/websockets-17.1.tar.gz", hash = "sha256:acfea4c20bf54384883ea33b1240fc1db4f52e190823a4e2b334bc3e8bfca96a", size = 187520, upload-time = "2026-08-26T17:25:33.063Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/ad/66a74d42fb537bd44056483eae6cbb7ebb10b742c300a0bf8cee427556d4/websockets-17.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:88b882764ef65147a7a5ae13168dedbe225a04e2ff4858fe543f2c402f093e9c", size = 216984, upload-time = "2026-08-26T14:55:20.747Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/1b/344ab22cea729e872f759b926441f7b822ab6cd106db527736afc066927f/websockets-17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:98a5b2589a56a4b4f098b0a958099a4356ab904a7844f1da3841efca469af7e9", size = 214667, upload-time = "2026-08-26T14:55:22.298Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/42/bace574b6ae80e1a8d6935b8c5f03fb67236233ec572e976fe826ff719cf/websockets-17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:020e271205f8ab3406d7a59cd00de6dec722315924411c421bd00642f18bad86", size = 214944, upload-time = "2026-08-26T14:55:23.618Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ee/87/08e35ca4a0ffafb500a16ff461bf9561ad2b755362adb5d077d4dba9affc/websockets-17.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:65be6bda2b537fefa4b3a5ccd6ab386533ce39dd8fe62433ec90901fdc81752d", size = 224004, upload-time = "2026-08-26T14:55:24.748Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/9a/00ae2e147eaa086fe8bdddd36f57216ce72b9a9dfc0b17c717005ebdacaf/websockets-17.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0c84bdef916556cbe1d5a43b423398be4dd3cba6522b463e53d848578b920695", size = 224278, upload-time = "2026-08-26T14:55:26.016Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/e2/7aeb4e00defa68826f449392922a382ce7fdf542fe52190558dc1714e284/websockets-17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47a62d6045c6eaa0d8f97bc2fb68b8cf90077a0cbfd4e83d6f2d2145611ee134", size = 225511, upload-time = "2026-08-26T14:55:27.183Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/e4/655be3d93c3edbe1a51606073b5454ea6b1b32d87aa26253a6df952417b7/websockets-17.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34879e19bb0a3c44f9317679435aea5327fac993933a704cbf353bf1234b10c7", size = 228802, upload-time = "2026-08-26T14:55:28.431Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/33/98549a2afa9d68fe1b5a8e0a61cd461a43ea1ab7209bce675eea67c79190/websockets-17.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2d72879819f5145a342d0030c418702496c65a4b913ef81f5ae944dd91dd50f6", size = 226075, upload-time = "2026-08-26T14:55:29.695Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/6b/cbc27e014d6c292b9b2709cfd32781a2b61eb30cd4c9130e7c57e41a204a/websockets-17.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:84f25e099fdfe3b09f953d84698f729a1f7d1e99101b2787d7a28ed77b323750", size = 224846, upload-time = "2026-08-26T14:55:30.964Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/a6/e57925f7a423d90f24559e85bac21a7f0b44c0cf4a5c0babc0759ca54bab/websockets-17.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:469355ab1af100b9380f1afb09985019f4a4b94fa1dd0e9396db4361626d7ab8", size = 222136, upload-time = "2026-08-26T14:55:32.376Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/32/07/b9de0400addb542ba7c819022abc3afa46cd7e518068881bceadac69d995/websockets-17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:00679b7468b4c2b12b0757118174e8eabac56bb2f579a928a104d9554a56e098", size = 225000, upload-time = "2026-08-26T14:55:33.527Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4c/bb/af2828a1d7f2beb792af6ba56d7b02d56262070266b95d2af9ef391fbfb0/websockets-17.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:a9fe648abd1d9b89aebfa30407bfdd08a0271ec5dc7d44a4c6ccd1ce22cf562a", size = 223592, upload-time = "2026-08-26T14:55:34.636Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/51/7379f254730c1dc7d8e4dd8d686868d8f7be55bdc94c6d3a44538840f639/websockets-17.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f47aafd92aa28b941180e6da8a42b0f711851b14b81a5b6bb28dbbb1fa35152c", size = 224360, upload-time = "2026-08-26T14:55:35.777Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/c6/fcd91320dd71dda7046df9bc60f60c70ee15c052dee21e31ed6221dc8b5d/websockets-17.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c89406fa3dcd4aa8662c6406cc5c0de1790e9614d2c3aaf03ca53a8a8ccf3405", size = 225404, upload-time = "2026-08-26T14:55:36.85Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/b2/655a4f939388079f80f1b3f8a1b9d40783e70a376e7423988cc9a590a09f/websockets-17.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b3b451fd2723ad3191a209afe6f3f4bc86c83e9a85bdc255353b91803ee6aa66", size = 222982, upload-time = "2026-08-26T14:55:38.006Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/75/37c84c4371c6aa668910d7841036c4397ea10554c07030603ccd5e44b02a/websockets-17.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:054c28db2dcec0e857e3b705d8c28012613e555b38c765d6a4f75340a4fc06a0", size = 224017, upload-time = "2026-08-26T14:55:39.39Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5e/20/8a9a94323bfcfe03bde3f9d98926bea4855856359702fe3ca0d07051ef5d/websockets-17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f8e822efd54137d8cc8310eb64635ab827a4a6c72ff08691f38aa624776d8ecb", size = 224252, upload-time = "2026-08-26T14:55:40.498Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/dd/aa66e6500188cd40306abeb92c9a738ca6dd7029d8d8532c538055ab5daf/websockets-17.1-cp311-cp311-win32.whl", hash = "sha256:dcb8d5f7edef7a399d322cf28d4c4e6f98dab64d301c8f50581a1080e5198142", size = 217484, upload-time = "2026-08-26T14:55:41.763Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/01/a2/cdf3b551f0b9177023afd3a45d3b431a0d4064951008c4321a8b42ac2288/websockets-17.1-cp311-cp311-win_amd64.whl", hash = "sha256:b1bc819c6db90e8f91a38250a1ab4c058261871aa52d2fe36382eddedf146dee", size = 217779, upload-time = "2026-08-26T14:55:42.942Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/13/51253dbed7d16a4bb87b05110ad3bf12165f410e915f6da1edd4186d8dc1/websockets-17.1-cp311-cp311-win_arm64.whl", hash = "sha256:edadce7a22052056fd4384543019856b34850363c9d387929f677ae01d79709c", size = 217710, upload-time = "2026-08-26T14:55:44.016Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/0d/098f23c4c858e5de9459ffc554fa07d5493fbcfca7f040b5800cf1cecc35/websockets-17.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:76dd004f59115087c7b700474cb18f01325e37250032e19396c08ae41448e4b3", size = 217015, upload-time = "2026-08-26T14:55:45.194Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/13/86/bc1317b1a4d8c4688e2a7e564b5e004dab44c2534d7ca05de6ae9a863fca/websockets-17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:581fa678ef46f4277cc8491312468e582f8ad609dbab907ba6096a08c6a0ff98", size = 214692, upload-time = "2026-08-26T14:55:46.366Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/e7/df821761772beaa48c211ee0e234930b35c1473778470773823f56d3911b/websockets-17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87f0d5e77548b0c40c8464cdb6108792e7e53f487c6400028a4ec28a8afbe5ab", size = 214959, upload-time = "2026-08-26T14:55:47.885Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/92/c3fb72f11764812fc648bf3838d224972427b348e8b3989d9e0a9df87da3/websockets-17.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:882af300d2c6a092b93767d5de03c7bb56dfb06314140c8e872d3f48e09f7b74", size = 224278, upload-time = "2026-08-26T14:55:49.241Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/05/9f82d090c8d2d861604147ef6dfb938a90b039f9358d5193f1df62558593/websockets-17.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0c863507ada5805517ca6dff1c524dcd42942efe6304dacf06700878398d21a6", size = 224557, upload-time = "2026-08-26T14:55:50.348Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/50/5cbf677b865290fe36819ff00615826e7edc1df38786f770123ff39a933d/websockets-17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d41ef69d5416fbc1d98cf96c37be6192d10fd101c3e0f8b3ddc36e09432b3c08", size = 225791, upload-time = "2026-08-26T14:55:51.75Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/1c/eb8a032285243381b09a221ae384c972d5000453ad136add4d1595cec798/websockets-17.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5aefe78e6a3077fe22b5e64b04666a85a3eb8b934d40e8595a693adcbceb6f11", size = 228574, upload-time = "2026-08-26T14:55:52.922Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/69/85/413736251cb3ac04ce84cbd90e893d9a36a9698d4820b323aff3aa187e50/websockets-17.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f64e001bb7fa89b9f32cfa600bf8e9ac8ca26759d9b92ae01453ee303d9cd7b4", size = 226428, upload-time = "2026-08-26T14:55:54.263Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/2b/a08bcc7fa1ca81a10f84ba32b6e6edd73a913f4b0c2640eed1fd626efacd/websockets-17.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:677014a073bcb1fbaa7e21144786864f16c08f856d66834f611eceb9006cbab8", size = 225184, upload-time = "2026-08-26T14:55:55.943Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/8a/3bd2d0cf6b148c8c866d5d9fdcde30c04bfd81fdfac86813e69377eb4448/websockets-17.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0de501b7f2db11e83739ac20e2d33d46da4604b829f506c24be80e7def069391", size = 222430, upload-time = "2026-08-26T14:55:57.103Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/c9/8e891ae342668735eabbbc669895e15195e4b45f24a4beeb58af76f414c7/websockets-17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f62114a54117e4948a1e414e89521f7fe1e3c2f83f2a571a06a4fc6718b0900a", size = 225227, upload-time = "2026-08-26T14:55:58.375Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/6f/c816f332dca11425e9bda7c07f7573eb5c5f8a735849d02b0d81e8ee20fa/websockets-17.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:eec113a5b41d124ef42ff56b0d74a6da3fd986400038eab9e58ee42a4024e837", size = 223831, upload-time = "2026-08-26T14:55:59.664Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/53/67/5e91d5308ce24fc1ec74f56536c12f4888bad45ff5ea50f3180f8c518c57/websockets-17.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5f051f8030a51815dc00e24bd2e5f1435af095c1cc111d747ac6e2a3620d7641", size = 224600, upload-time = "2026-08-26T14:56:00.873Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/96/faa298ecf2570d35b0eb37caddf4992178d907e108ed74bfffb6bc092c29/websockets-17.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:655a8e28010f09fd6fa317e857afab3af7647f33e41dee88fa421e92086d1090", size = 225707, upload-time = "2026-08-26T14:56:02.001Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/12/5710d2482ca5061c1eec5eb46f6313837c760d4115b1795c85b6c08be4e3/websockets-17.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dc2b79afc074d2f3e64b26539350f697fe1b85ea1c49ea24eb588f247b053ce1", size = 223263, upload-time = "2026-08-26T14:56:03.092Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/47/0c30f4eebfd1d93fae779d268f678d48847fb98516f5200849574eee8820/websockets-17.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e4bd7eacb87d8cf3ed70d6392c770a0d92441f05d7d2a3efafb5bc171d5e3067", size = 224244, upload-time = "2026-08-26T14:56:04.321Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/33/46c256195a1255079ae23d1b1267b2e1843dc5f46a67f973cdf2a3523dff/websockets-17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ccbf3f4a9890d50b3a08ee04029fde30a03bfdeffaa19977628bf17251764e60", size = 224520, upload-time = "2026-08-26T14:56:05.521Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/06/9a/aef0792731df4352e5f417369b532b3325fe434765ca90c193f594ae1e67/websockets-17.1-cp312-cp312-win32.whl", hash = "sha256:7e724f843fa6a0614aece65a7c73e51d0f4412ca41dccac13c3caf98e69536bb", size = 217485, upload-time = "2026-08-26T14:56:06.715Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/23/493ecfdaf32898e5ea24dc900e33e5e317f9662d5d9ab2d44b2e111b4e1c/websockets-17.1-cp312-cp312-win_amd64.whl", hash = "sha256:617243e19a0992095956f406ee9cd3bc4ba92862d83cb1d83bb59ce574412bec", size = 217786, upload-time = "2026-08-26T14:56:08.055Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/3d/91954e2f7876f74ce1213e9b92c65a63b559cc4b942a931ebeb351cd9932/websockets-17.1-cp312-cp312-win_arm64.whl", hash = "sha256:9f4a08ff7cb68c27b18e09223cc6304e01d0f82d5a240d251266dfd2e6e44729", size = 217711, upload-time = "2026-08-26T14:56:09.267Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1d/31/5f6450a7879f4f063ef08897cc385ea3ce3f1fe17f08b11e3fd959abdf27/websockets-17.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2a0162a6372110a5601cb5c9fd826635cedf69f3e110c545dd19774e040b970e", size = 217006, upload-time = "2026-08-26T14:56:10.509Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/2a/c1b006fc861695d2aa4e35327b842015ce1d98cf8f99241829b3d6460bfc/websockets-17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:829dba1bc049779de9b332088c1a6a9858e96bd67e50b6b644a95e02b67836bc", size = 214690, upload-time = "2026-08-26T14:56:11.681Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/69/66e5b7d01445e0eeb1d4ab419c30315f2c90cf7a8a8cd4ecc47f894dba54/websockets-17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd8f47dbf2e8adb15c847215f83436de3fdb120b51fdae0fbbdf69fd97a3ad80", size = 214947, upload-time = "2026-08-26T14:56:12.923Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/ce/033cafe2d2538562efa876b9149a2c7a0f7787870a4b1bb6e28adc9ceb6b/websockets-17.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9f4c0377a83e163a303514fdfab501dbe379bdc13e5b9312a91d112658b29dce", size = 224329, upload-time = "2026-08-26T14:56:14.212Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/34/c7/e1c2e8a67f6cc0aa43abe0046fb3b7a020980649e6a843751dc7ce9eb170/websockets-17.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c3241d684a76eaaef8b2dc789afde4343cd3aad55ea81e4e8ab3605b529bae51", size = 224611, upload-time = "2026-08-26T14:56:15.702Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/de/07c6d48eb3d2069709410c851e7de10ab83d752c4bd09862899627c2729b/websockets-17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5f5c7a893507d0e83a80b88aefd6522f7e882cd53f9722c6f23f5a020c9557c", size = 225848, upload-time = "2026-08-26T14:56:16.962Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/dd/3c68572d20509648cc2fb6f50ccf3deeb4b87270f2c8966e99476e278ea3/websockets-17.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:00bf34b64501e3477e81fc281532ff3cbf4da26633c10b63979d5085d46602d3", size = 227290, upload-time = "2026-08-26T14:56:18.204Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/4a/8f6651c8a22093539c9215af0c5bbf217b87b382c99d2112039b92d593c2/websockets-17.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ce0305b702b20d1e1d60a9aaace6bc89970e1753565543f310d549eab22c2435", size = 226476, upload-time = "2026-08-26T14:56:19.459Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/be/f6fc33cea86b1127fd1297b18c107e81580ab55a73a39f9a934441ef321f/websockets-17.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29176d8b429cfa0fa443c473878d37a5c06cfd0cb36b71ba4314accc71e05906", size = 225233, upload-time = "2026-08-26T14:56:20.939Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/83/65edaf05f7c9b1dea82f4d252fdc37706a84571646f06119a27b0a16fe19/websockets-17.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3709a1ab30b4b922027d22f68d2b61a0656a91680ac894a537624e6be7dd7f7c", size = 222488, upload-time = "2026-08-26T14:56:22.208Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/42/d1169c2f7f1f0032b0d4b0c00f0711a070cd7c735de37bfeb876bc0f9606/websockets-17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:43bd0c1ceb924d67f5c1a5254d8361dd9d94246e6331a726064dfa2917880780", size = 225295, upload-time = "2026-08-26T14:56:23.445Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/f4/64e2a386c3899b917c2933225c9b47887874229d159797f3bf1a11c20d51/websockets-17.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:1fce0f43e0d41422e0b2cad6561e1970df22f212f4c7e884967df7cf591b031c", size = 223891, upload-time = "2026-08-26T14:56:24.647Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/26/b3/dfb5c482f7e310a3432fdbb045ddfe6d34114680e89a233d4ff900a32961/websockets-17.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4031152769179ab8dcdeafc7b0e58052a49117560a28671700b47b2c7b717aad", size = 224661, upload-time = "2026-08-26T14:56:26.027Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/cf/94865130a336029f46412adc127c4fbe380f46172b90ce251369e35c4302/websockets-17.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a06f3b5085176763182449559e20391d7ce616a8972a9f7a33deda87ea6d4f3c", size = 225766, upload-time = "2026-08-26T14:56:27.455Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/34/eb8c658f86dfe562ed49a887a27424bfe9e618c26ea6f865b093d075d3a6/websockets-17.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:77b37cceca17291897c3c73bd30a7c7c7909593554b5da574ec852af83c1742a", size = 223323, upload-time = "2026-08-26T14:56:28.807Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/7e/2629609652ece5ca0c7ac235927dd4511b08131e3a5d53439b798fddf002/websockets-17.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d8e83333385cac6030a5167fd18bf96cc6c58b914c308e683f05b0cf94bc8dd0", size = 224276, upload-time = "2026-08-26T14:56:29.991Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/6b/8525737fe840b38e5f40956c198fb586a4fac1e07144d41a5b949b989cf8/websockets-17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:073c5c3f7e127041fa9d34a9e29ceefee8c3cafbd267ed2927318f425144380d", size = 224558, upload-time = "2026-08-26T14:56:31.184Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/ab/3a958c6cbcf74b118f601c20a80ac8bd5e8dfec0bcf7345116feaeefb121/websockets-17.1-cp313-cp313-win32.whl", hash = "sha256:2afb58c7ba48b329d56769f8dfd89f394efe587b65ef806bae810a484d6d3608", size = 217475, upload-time = "2026-08-26T14:56:32.431Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/36/fb521f0f2994c25509651f169efe5582dddd8713d57a0757ba87859372ef/websockets-17.1-cp313-cp313-win_amd64.whl", hash = "sha256:0340bbef6bfbe16da888b3983d666a4db4954ac3253c38f13bc7aba0c7db5a2f", size = 217784, upload-time = "2026-08-26T14:56:33.608Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/92/9b8419584681a12a7534b746dfb2737c466efe2455483e2fbf8b941a04ec/websockets-17.1-cp313-cp313-win_arm64.whl", hash = "sha256:7a72efa3bf4fa3a6669a54420a472ad056da3973d827f10e3a536da463f926c2", size = 217715, upload-time = "2026-08-26T14:56:34.865Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/0d/500cf5daea09d4669dff3a7d67159094a0bd6c4ef130381404f6edd3eb5f/websockets-17.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0c9982938980e086da59f70d05f9418cd143401a601a0faac10fa48f7bb1cd3e", size = 217048, upload-time = "2026-08-26T14:56:36.03Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/12/5b12c6168aa269cffbfd24d177cd492b130120403a418c7e89462e27b4ac/websockets-17.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:57b39dc8541cf7ed3f639da82bf7451060483967f9e733da1f8173e4095f0642", size = 214737, upload-time = "2026-08-26T14:56:37.43Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/36/e453e5106e4e2416f008ac222837c2f1637a063b08008afcd1088889b631/websockets-17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:96abdecbaae746851b87c3a36cb4a661df93ca3d92f114270f79228bf1d00de6", size = 214955, upload-time = "2026-08-26T14:56:38.71Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/30/0204bb86176db02cdfc678ce65ed808a66fab87d250ce61a8790800a60b0/websockets-17.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d9fc873e239c5abeb150bc24dbd1a7af23a9254526383ce0a077f5e20adbeb19", size = 224331, upload-time = "2026-08-26T14:56:39.924Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/c8/d8372256e00c4e3cab1115c45075d1eeedb642a3f2b42bd70c4deae03f06/websockets-17.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f42912fa9eb4cb7c7ec9fde9b3332ba339eb8a8811981043d4029599f3d950b", size = 224685, upload-time = "2026-08-26T14:56:41.169Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/7d/650355b8f67f908ff99603351d4458d1a0b787d627950a47c38db7e25308/websockets-17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f98bf378d7a5be047a044a1a27c987a8f355e10e3b5754617dbe756248cbc5ce", size = 225927, upload-time = "2026-08-26T14:56:42.359Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/34/6c/a9ffa5b903579eed76017870f055d75ecc73988d9d0c9b65a92ba0bf2a27/websockets-17.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d334d11398086bb5559606cb42d51c013ea7c061c7db701521392373d3c087f5", size = 227300, upload-time = "2026-08-26T14:56:43.538Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9b/5d/4551c2269066af7481ee44605a0813770961615b5b5da3e87a8f5cb859ea/websockets-17.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c27336b1a0ac56569493e858497870347854372395f50483725f8cdacc5a45c", size = 226533, upload-time = "2026-08-26T14:56:44.669Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/43/237a99233e5c445759a613831b3a92e91905afc064dc3bd0ad33c35fd1e2/websockets-17.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67258b00302a5aaf0b267771c7014b13429abd7ea17eebc4c55bd935ff101555", size = 225280, upload-time = "2026-08-26T14:56:45.83Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/b5/e9407a91613d1d1cd932414143a1012096b26674a782fc55a0bd23217ee4/websockets-17.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:455ffeea0879d313205df1e745e5883e1feb7f31ecd26be882f5f0babd3db04f", size = 222540, upload-time = "2026-08-26T14:56:47.053Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/d2/db76628db0577b783205d9779f64d8e373416b04c62d1546be4b75dc8540/websockets-17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f7233eaf441a345a5943a929fd4b5ea3278f11aed35a9ed0f3106b8cb3ca846a", size = 225354, upload-time = "2026-08-26T14:56:48.32Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/4c/2174181c067b89a74ae18e2650c2ac29959f4b796afe876ab3f4d30d642c/websockets-17.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c65da239a5ad553619804c1f9d65c1a0b3005381c6158ee14da2c7444cbd0c78", size = 223867, upload-time = "2026-08-26T14:56:49.579Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/75/274decb9a8253561b5be3261e02a6676fc8ecdf31e95b722e53d5bfb8fd2/websockets-17.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9fa1ffa08c81a4f809cdab6129f8e55bee4650b9d6d3461019dda73aacd146b6", size = 224652, upload-time = "2026-08-26T14:56:50.885Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/e6/49824f1fb4db7656d2f7492b1d8be16147b759d909490e32f4776843ee64/websockets-17.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:406b8107943a43ef4649b1e0cb0cdc052bbf08fe1c8905a623c4af9586e5cebb", size = 225822, upload-time = "2026-08-26T14:56:52.356Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/6a/5dc43838c0b02a95f42c47a0de33c5ddd7767a9feeb4d0d8777ac1cfefe4/websockets-17.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:4e8ffcb486c8490a34a4cef5e4409d8da5a1cb1681e5bf7d786ce5e84aa8540d", size = 223379, upload-time = "2026-08-26T14:56:53.699Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/62/585637cf06d6b321232f79c55dc14d65518d12cf87c94c44f5864068810e/websockets-17.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fb88076df585b69c5761c387c0081aa87d7b9eb1b205a6535ca4777e25650d81", size = 224330, upload-time = "2026-08-26T14:56:55.184Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/68/c3b234a6a1366b6ab5bbfaa4434a1b946e1dc4e8ddd6824bfd93a8835b7f/websockets-17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5d4724255fb8398acd9e583b97eb2279cec20e0bd0f9a94bf75f6056ef9f13da", size = 224622, upload-time = "2026-08-26T14:56:56.393Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/d4/84cf3d1376f5d8207f55f43c1c818babd6b89447f5dcd01f18a6d5526796/websockets-17.1-cp314-cp314-win32.whl", hash = "sha256:be3f0129c5654517b2abf07dcb75bb1d9479759a4ccfb569e8293579e9fc029a", size = 217036, upload-time = "2026-08-26T14:56:57.652Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/0f/9e7ac63c5d7cb642952200814f584318e65146df008b7d375d5d9c6b2c97/websockets-17.1-cp314-cp314-win_amd64.whl", hash = "sha256:2a4dc6ef83f4559e0d05f313a375cb38f63c986096a9da99fe94fdd779d313e5", size = 217382, upload-time = "2026-08-26T14:56:59.065Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/bb/1ae6b91f7f3ac05f5c9f14a72dc2181c115ff370bcd8a7f10f02c174adfd/websockets-17.1-cp314-cp314-win_arm64.whl", hash = "sha256:46c0331c9eaaf73a559f3a9e388466be0df96eb83d40f06f1ca6ab6613b35c82", size = 217268, upload-time = "2026-08-26T14:57:00.654Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/f0/f65644d0e0b2b90918a8c41503841cc4072a58f2bf76c09bc36e751fc0dd/websockets-17.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d411ea5ca18ac1b12c0c94be88b60c18ca641ac43bcdfdf1c9f79d46cdbe1603", size = 217379, upload-time = "2026-08-26T14:57:02.181Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/35/4c46d1f620ac1a30f92b6eae78ee40a772a93f568647ca7ccdc5ea283cf8/websockets-17.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:07fa3e7c30e2c577928d359b56bf872a3e0cbcc15553eaa0907c1ee86344b56f", size = 214911, upload-time = "2026-08-26T14:57:03.478Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/6e/4587e8406d7c1188e97b9cf466c081e93399380d447f885bfce81626cd37/websockets-17.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6de9acef07e3a78e9567fcd26c29011a4da8f050b13004bbf880a0fd82a6eea5", size = 215115, upload-time = "2026-08-26T14:57:04.692Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/06/1381c8fff525041025909eb80ace32489194a00ba22a0a8d428030afcc84/websockets-17.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ea0ed9373b880115911d9d39634bccc95b8ce590c9c42e8589f5cacc3ef3cee2", size = 224696, upload-time = "2026-08-26T14:57:05.899Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/9d/9034e867dc85340be058619751742b895f722326e83100d110063461ca07/websockets-17.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50903d335bfda026c2fa11dd9aed09d8cbee0c451e3a85122a9acb041b7dc69b", size = 224975, upload-time = "2026-08-26T14:57:07.262Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/eb/ed03aa3cae748ebf6397e5d44028f433f746bad09dc568ff754fda3a3c9b/websockets-17.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a74531ce81af587f906ab42f194032388fcff8fc7938402e5917c9147a39441", size = 226151, upload-time = "2026-08-26T14:57:08.524Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/c9/cc1964a096d16f3b73cb1ee5f14f277f5a3bcac07c6e8f9a1dcded99f4c8/websockets-17.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8fbf28e639544503b7d1c96452a5e5e043e4108d89b1f3fa02910603622d19db", size = 228292, upload-time = "2026-08-26T14:57:09.846Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/26/46da6dd0363c2db2e4876fd59a40fd40c1943a82d7018d0a33afbce47d52/websockets-17.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f612dc57f00c07cf4aa2673f7cbceabd654ad2457b7e639f061b794d6e11f9fd", size = 226722, upload-time = "2026-08-26T14:57:11.118Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/98/ecd8f5e1c5d0e54c08ebc5c66852271112166db68107cb0e17ca1bf25009/websockets-17.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1c7ac77401227212dc6e849182feee50d57cf456ec6329ffd6979c94bb136c5c", size = 225451, upload-time = "2026-08-26T14:57:12.601Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/65/4d/da8d2760db53e17aae763738b6ba834b1fcf16813d3632f3edb6951e1ec8/websockets-17.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32a2a68d989d6e5b74a9d5095415c51189ebae29fceb7cf2b64a1c0318a81256", size = 223003, upload-time = "2026-08-26T14:57:13.875Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/40/ea401c141a79c5b1d0021a0dab9d0df2051c108f1620fbb39a6e7c714c3b/websockets-17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:aec00f018d34c67500ff0438dc314b40277be4a1b983cbacbf53ccf7db63e257", size = 225704, upload-time = "2026-08-26T14:57:15.091Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/8e/07ab3f44215d89840d5385fdcaaab1fed8caeffa67c6899e15062957c12c/websockets-17.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:0014eaff8ad5b3b43feda2279f9d34bf2eaae040720b9fbbb55944b10f40b14d", size = 224192, upload-time = "2026-08-26T14:57:16.3Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/93/ccf1af0a23e5748d4e22292a377d78d15cf294d7e707bbb11a8990ae6bd5/websockets-17.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:db9d7ee47f3ba531e278be539af39e2c7c7d28fb94897b6cd1120d63b0ef5922", size = 225082, upload-time = "2026-08-26T14:57:17.531Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/db/e32200f99ce282e728d2929f2c429db353cf3282db7d0eba99eb32c9fec1/websockets-17.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:ff3e2ba7a9f0a110b0555452e9b5a03a34e11662544e01beea15f144b48ba7b7", size = 226101, upload-time = "2026-08-26T14:57:18.802Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/28/3d/e7a6e9777b29433620167c98f3caaff0d6b08b1239a273ef7f7fd1393349/websockets-17.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6da17fc94bd270f5987b10bee113461ac36a36a98b0481ddcc98056e5a90001a", size = 223794, upload-time = "2026-08-26T14:57:20.313Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/05/ac569090726dedd6656f3ee28b0c02dfb1ba76e898dceaccc2987a237cef/websockets-17.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:e8dc3fa6d6b7ead3f9de57895f41b116a28787548e066365d9d90f7356bcaad2", size = 224567, upload-time = "2026-08-26T14:57:21.634Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/50/4ef62941111db6b31193f4fabbb65f845a5177579040cb8fe0d774d25034/websockets-17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b65d5fe48219dc2d5e158de9e6514e75600f379cc7e37108d35f31764c155566", size = 224993, upload-time = "2026-08-26T14:57:22.86Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/28/42/2b95ada4ea19bf3a2072b68669ce4f4afb212690b727d31640576287fd68/websockets-17.1-cp314-cp314t-win32.whl", hash = "sha256:2cce251f3e2469b99b6802b55435bcdd07123b41870f54c87b336183af9d7e68", size = 217168, upload-time = "2026-08-26T14:57:24.466Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/32/0a/67d5ee08dd8060a37d612fd40a625b5376ad19ae48fe1c8ad428c278b817/websockets-17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8f6c38cdcaf98a911d7acc25577f2f9e710f3a2fc2bde1563556784320196b51", size = 217508, upload-time = "2026-08-26T14:57:25.983Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/a3/822005d0c674451d2411027b878cdc128a2b7ea5a30d337d9e279da22eba/websockets-17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:d1e2f5fa2b6d01f0d85b4f223fea7ed1d504be282a02a81bd2be4817ef7a2f03", size = 217425, upload-time = "2026-08-26T14:57:27.324Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/d5/99a6c6a1eb5d5ae9f45f59a3c97f4e3b21f310eb404a547fb3e7d2fc054c/websockets-17.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:88381602e379165b66244b2ebc29f9b23ea0851fbe63ae157f91ca324f072d6f", size = 216970, upload-time = "2026-08-26T14:57:28.575Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/0e/1e7f6e833728193958d3ed3d67b5d57c3c7cfa948abf94d4bc553257c954/websockets-17.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:88bc5138e53903a85c354e59df7ba73ce306f7b09724cef74dba121e60a88ce2", size = 214699, upload-time = "2026-08-26T14:57:29.862Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/00/95d39549f86e34425a0412bcbe61708dd1fc46af654e2134a6c4389102ad/websockets-17.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:3546ef55b3a074494106508bc6505c73825970d2d9505f7bf53882b3e88b0d1e", size = 214927, upload-time = "2026-08-26T14:57:31.148Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4c/ff/b442415fc4f7f9943b0fc8e8eebaa13923ca73361e167c439ba634eecbd9/websockets-17.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9ae55d24241fc055f22aea3ac924559069848bd0ad4ea065fdd72d2194685fe8", size = 224373, upload-time = "2026-08-26T14:57:32.833Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/dd/b83537aae4cf61615b9d8b2dbb235c0030ba85457a6d934798273814600f/websockets-17.1-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d7b349265fad6244013eecd99df8d83c12bf3013943e431f4fadd5bffc37db42", size = 224801, upload-time = "2026-08-26T14:57:34.041Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/83/5ab0abed58454909e8dbab45086ac68ee4556d7a8ada26735addc909b903/websockets-17.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc5789e5ea182b77a38881383ada5347202a6c66f4857d054e075290e80b604b", size = 225967, upload-time = "2026-08-26T14:57:35.292Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/26/e2412f2b998a8c1dfc00c0709ff6ee0c634dd0b0b4f92bdfe9667876b71c/websockets-17.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ce13c7d233239e739600a57d4a73c1192ad8259e655a4d55aa1a454242bc809d", size = 227664, upload-time = "2026-08-26T14:57:36.493Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/25/0dd4495df3c0e02f6db705312ba85ab9b2dd42257dc23eb0da10066e4844/websockets-17.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1036189bd34b0bc1b10a4679321e2c7968af317efe6e8e4c1c5141c4254fb5bb", size = 226447, upload-time = "2026-08-26T14:57:37.781Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/67/6df3f63ffc48f08126ed0cd2fd2a41092967c3e364f8ec100deae90b6d77/websockets-17.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e78fd4b7b2c5086a38671c9c882c1e643385eccea360b5b1fda4a105e590087e", size = 225343, upload-time = "2026-08-26T14:57:39.133Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/8d/a8479bbb09ff054907d141123d8f52fb6ae5ac39c6dbe39e6a02a8408309/websockets-17.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:46e7a10bf04318c7b0c0273791925ae5e1cbe4a11e34aa934d2ef27862058a80", size = 222748, upload-time = "2026-08-26T14:57:40.478Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/fb/4c3d2a3269cde3f3087916de9c3d9fc5d7196b46846d8c3a9ae59ad0a884/websockets-17.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:33e45c7ea38428e740a7f233555d71df0b875cef7fc080acebc9654475e35335", size = 225453, upload-time = "2026-08-26T14:57:41.859Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7f/1c/6467b401d19408f34e1c7389c222c2c7e1dfdf08c551190269b5eabc726c/websockets-17.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:6e63c01803be425ff062b7f7fc201a74def1d49fc94a2410dd17375df75936e9", size = 224112, upload-time = "2026-08-26T14:57:43.136Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/5f/744e032ac80e11039a7447657ebabb46e9b5c2dbcec83be571335212932f/websockets-17.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:722ec21717eec6477bce582147a28acdfe034e604239466a6a95daedb863e774", size = 224646, upload-time = "2026-08-26T14:57:44.871Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/47/bcb9128d9afc4d0934d9192e2a24897ca2f7a63df2654904915349c6c46d/websockets-17.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:e74e41f0ad12ff1e8983e349daef79d37cc8280c743ce9d134d6c74c18dab5d6", size = 225797, upload-time = "2026-08-26T14:57:46.338Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/e0/b058047b7cf565e1105b10ef6b6b24a6ebe3575678c7dc75a645334705a7/websockets-17.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:12fe8984a32dbfd084e0603f1a8d740c0180cb85b3174585c54a80d2455a8394", size = 223605, upload-time = "2026-08-26T14:57:48.175Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/69/fc1555bff884de363f1bf9eebf2836dbeb29fa7e4f957debb7bbcf43abba/websockets-17.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:01dcb47deebc40b38fd4a493b9b9f4d0a704b7bec6f35e4d34085b329abce71a", size = 224508, upload-time = "2026-08-26T14:57:49.407Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e7/f9/648d4e68621688b19093b06f7b497d520952e68cdea1c1b54371fe9491de/websockets-17.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f4c45ee2512d3757b5e6c67c5a34e435143f2ecb7df3324f9fd888688c45c0f4", size = 224767, upload-time = "2026-08-26T14:57:50.799Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/93/f8342b55864f71df13eb8e9ef7dce691b87a87f04f75bb8a1385b3336e7c/websockets-17.1-cp315-cp315-win32.whl", hash = "sha256:0f4f50dfe2cc810fc4e2de979b35e83bf8bb4bccdc6fe472d93762ea7b1d5927", size = 217003, upload-time = "2026-08-26T14:57:52.122Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/f0/7b5fdb774c245e0b6217009e2a24d2105c1a64923949f33be41aa7959302/websockets-17.1-cp315-cp315-win_amd64.whl", hash = "sha256:4af784f3e436f65b355c117c6497320f2b5cf6a559295cb1c4c7338e335d45cc", size = 217300, upload-time = "2026-08-26T14:57:53.492Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/33/1fe6ed1b5087516115ca451b2c240314b010647071f8fc3bd78a21e4dddb/websockets-17.1-cp315-cp315-win_arm64.whl", hash = "sha256:d58159af7835fde09c462394293c0d7aaf8fb4557d8f8e5699f5e722ccae013d", size = 217214, upload-time = "2026-08-26T14:57:54.88Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/ca/ed02e75996a266d76c5fcb5dd9b930db4cf2b388ca5fa3d2a72086f81568/websockets-17.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:1a5cf4e7bbe3ca499e6a289206cb4fcb7444b09919e129bd517f57d5fa192c13", size = 217282, upload-time = "2026-08-26T14:57:56.108Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/7d/d536f5bc89ea5b52fd1c1727c59fabafee6bc41f5ce92c3bd2f83047908c/websockets-17.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:416b4bc8789a1865a3ff643ec4ee073a5f52402d0dbeafd27b1798d5dd6b6a51", size = 214863, upload-time = "2026-08-26T14:57:57.355Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/37/944cf17bad668e9be1247e6314f88a48b9faf7c250e383410db8b38af0b9/websockets-17.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:259f45358c76d3b18489e3e80636cdbe807e05ecf1b10fdf1a779106d23d0c8e", size = 215073, upload-time = "2026-08-26T14:57:58.719Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/bf/3267966cc1bbc2b8fa62fd329651b0af502df1f5d1c0eed027ff339d6aa8/websockets-17.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d9d01e8ede41fea4f5a847dad9d628355f74905f437a5b6856d67aa66d193800", size = 225229, upload-time = "2026-08-26T14:58:00.235Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7f/d8/85ea722f483510abb39fc71aafb4465d17cf9051a275ab036874ff3c300c/websockets-17.1-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7b35181a14cbfcae163b4de545d22abfd07d06c2c41ca69cfcd99251d6888ab", size = 225500, upload-time = "2026-08-26T14:58:01.994Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/ce/64c7d00005bd0d15ecb5c5fcb7fb2597b6b92ddd16c4fa6bbc3d2835ad63/websockets-17.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a8e768a048c2220697477ce2e67e4345dc9f693d0ee6af53945b5e30227c6a7", size = 226829, upload-time = "2026-08-26T14:58:03.327Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/dc/096c67940fb957e667ca3c542818150434eb0388c6fdc90b3a502f3c3e96/websockets-17.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:880069d21cc33a558dcf180924a546d1ecf8ada5be3e4e70acee87019d706a24", size = 228457, upload-time = "2026-08-26T14:58:04.78Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/fe/f2331b6b7ccc67589891da354fa46a5cb79e95f83b9fd0e734d77f1f2140/websockets-17.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cec1bb8f22abccc8d20f8ca63df9be41600c26c190f4b97ee86c675fd4a863a6", size = 227265, upload-time = "2026-08-26T14:58:06.102Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/a5/fb1642302f8ec77ca922203074f155a9831a5128ad75e725059a476d1227/websockets-17.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f3a1d577e081667dda7f8e5b4796e6e32f9713c93e2a3d930669519840a3c623", size = 226143, upload-time = "2026-08-26T14:58:07.464Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/41/7133fcfb63f5562750b269d6a845c689dde6a2c6407286da395beea19ddd/websockets-17.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc053f9e95a76213c5eb7ed95779f7daf0d2bf0e4e03073629ebfa43a033f151", size = 223501, upload-time = "2026-08-26T14:58:08.766Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/b1/82b36bfabc79ff2d383a1fc043cee6a13f794ef4f6bf1b4810ad6988cf6f/websockets-17.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:bb0efe019480a1c93e168ce96479273aaebd672fc8c350d5eed1e507ababb1b8", size = 226330, upload-time = "2026-08-26T14:58:09.987Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/7d/5b511b9bf6e9ad331e6ff902fcbcc71c3794d10ef3b5efe80ccb8f0a7861/websockets-17.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:615746b12b26a3fd4077bc6fbeb277a1c192a45dd57b531d07ad9ed5c52a9a7a", size = 224980, upload-time = "2026-08-26T14:58:11.303Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/50/aed08f25301f8eef23be903ff9319fcf35630ca2bdec9d226f7d804dd5b3/websockets-17.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:1a20136d61f9ca3a31493732762661fafc2c20e8861930214e21afc6a8a692a2", size = 225478, upload-time = "2026-08-26T14:58:12.543Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/47/0d63d4168536b4682c9d19b7399443b1176f25dbb68878374fa716670230/websockets-17.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:2786cbd273ab69c22612db8a41229ddf2c158060b17b5928884bf388d07887f3", size = 226588, upload-time = "2026-08-26T14:58:14.457Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/dd/844bd0b6386fc81ed6a55f4b6dd26f01c6987eda205afa10175ea12b2164/websockets-17.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:b1c323fc3be1dc3f87f6c59458cb7d9e13dcbbf971d6c3f3e2bbaf58d3bfcdfe", size = 224336, upload-time = "2026-08-26T14:58:15.778Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/18/03709c84bc88ec4dcea68d4be4ccd07d611073dec111203a5bf45af8809d/websockets-17.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:12c8e2b25df59755954a04dfa09c990b96691025aaf7eafd19ed6da24b09c18d", size = 225197, upload-time = "2026-08-26T14:58:17.141Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/cf/0d1c694b6466c89e875b85b32b51312c472cf6708eee91914866f5087dde/websockets-17.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f58f58b4b29bbea2a3635e2c56eff4a3adab011fe383802a9e542e31b97085fc", size = 225493, upload-time = "2026-08-26T14:58:18.521Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/f5/99857c3dd9676749f33e3668665a34ad6099505fb8d75eb084f49f7807a9/websockets-17.1-cp315-cp315t-win32.whl", hash = "sha256:f78a3ffb1994304db2c0c4588e4d1a518079b557054fa3bb985a6f5e50ff49a3", size = 217130, upload-time = "2026-08-26T14:58:20.037Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/84/77599922ab441bfe61508f97dab2c71f8e114d31793993ea54011db16199/websockets-17.1-cp315-cp315t-win_amd64.whl", hash = "sha256:ad68c28a27246fed109a4409393d677b7e1388345cbbd2f5aee5c182d8506110", size = 217448, upload-time = "2026-08-26T14:58:21.382Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/3c/8b9a225b523f06a9389be81f1b0ab07c49bec6014742e6aa359c1f920f1f/websockets-17.1-cp315-cp315t-win_arm64.whl", hash = "sha256:e552e0037230ac16e5f568de7012041344d1b18c9feed30ec2891b8eba55af81", size = 217372, upload-time = "2026-08-26T14:58:22.807Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e7/e4/af4abbcf07eac6a725ec6f865611526b2b0c23d482723de551bec667880d/websockets-17.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:10ecb38ffc05e1841b619d99c725307a223ef9ad58e7b1ed33311d472dc43918", size = 214602, upload-time = "2026-08-26T14:58:25.211Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4d/fe/819fba7ba35f92b639333da7355041c07dd50048f9c76fba0b8e292a6483/websockets-17.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17aa424ab61620aad21b36b2240efc87b500cc496e7d0e999a5c2ae99395e886", size = 214874, upload-time = "2026-08-26T14:58:26.689Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/a7/d370ab794f47fbeea648d17ad08caf0bb50131d6c04b7ad83e6af63c405a/websockets-17.1-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:764cf7bfa149365f32b7a0fd9fed32debdac29dd06295d5635cde1745b446cd8", size = 215821, upload-time = "2026-08-26T17:25:23.616Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9b/6b/251b00fe634e2a9c2cb5d6390e0e97cec55e3d18dd09b4b976620eed5d7b/websockets-17.1-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8d1b108bd8f5f6a8b90801f6db3b3858d5deca889acfdb8ac497bbb24e4b0edf", size = 215714, upload-time = "2026-08-26T17:25:26.295Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/b1/37fe0c96c206b4208a072c3a74add6a72af4b8228be3f5435163c5a6d099/websockets-17.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a62d8c424383c9dc769ff3672018df822603117e32686e567d452ed035b6fb2e", size = 216608, upload-time = "2026-08-26T17:25:28.134Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/7e/75a0a491b512412e08333b9f8412757af6186fe1c598186261002de1a793/websockets-17.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8196d217eeca52b9235ee1f8a684a09885a5f953d5a31e80ef915bf2c5c94f9d", size = 217870, upload-time = "2026-08-26T17:25:29.745Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/63/23572870e01836a98346075b9e17a8bc24a6ddd9800a3204ceee58677f3c/websockets-17.1-py3-none-any.whl", hash = "sha256:f221081107b8c48184d99f7019604486376e7ef826037e70aad6b02540732c23", size = 211134, upload-time = "2026-08-26T17:25:31.397Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "werewolf-backend"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "fastapi" },
|
||||||
|
{ name = "openai" },
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "uvicorn", extra = ["standard"] },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "fastapi", specifier = ">=0.115" },
|
||||||
|
{ name = "openai", specifier = ">=1.40" },
|
||||||
|
{ name = "pydantic", specifier = ">=2.7" },
|
||||||
|
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.30" },
|
||||||
|
]
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
"""无头联机测试:2 个程序化真人客户端 + 10 个 AI 打一整局。
|
||||||
|
|
||||||
|
验证:REST 建房、WS 加入、座位分配、房主控制(补AI/开局)、
|
||||||
|
个性化快照、人类行动提交(发言/投票/夜间技能)、断线重连、胜负结算。
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import random
|
||||||
|
import sys
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
import websockets
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
|
||||||
|
BASE = "http://127.0.0.1:8000"
|
||||||
|
random.seed(7)
|
||||||
|
|
||||||
|
|
||||||
|
class Client:
|
||||||
|
def __init__(self, name: str) -> None:
|
||||||
|
self.name = name
|
||||||
|
self.snap: dict | None = None
|
||||||
|
self.token: str | None = None
|
||||||
|
self.ws = None
|
||||||
|
self.task = None
|
||||||
|
|
||||||
|
async def connect(self, code: str, token: str | None = None) -> None:
|
||||||
|
url = f"ws://127.0.0.1:8000/ws/{code}?name={self.name}"
|
||||||
|
if token:
|
||||||
|
url += f"&token={token}"
|
||||||
|
self.ws = await websockets.connect(url)
|
||||||
|
self.task = asyncio.create_task(self._recv())
|
||||||
|
|
||||||
|
async def _recv(self) -> None:
|
||||||
|
try:
|
||||||
|
async for raw in self.ws:
|
||||||
|
data = json.loads(raw)
|
||||||
|
if data["type"] == "snapshot":
|
||||||
|
self.snap = data["data"]
|
||||||
|
if self.snap.get("token"):
|
||||||
|
self.token = self.snap["token"]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def send(self, obj: dict) -> None:
|
||||||
|
await self.ws.send(json.dumps(obj, ensure_ascii=False))
|
||||||
|
|
||||||
|
def seat(self):
|
||||||
|
me = (self.snap or {}).get("me")
|
||||||
|
return me["seat"] if me else None
|
||||||
|
|
||||||
|
def pending(self):
|
||||||
|
return (self.snap or {}).get("pending")
|
||||||
|
|
||||||
|
def phase(self):
|
||||||
|
return (self.snap or {}).get("phase")
|
||||||
|
|
||||||
|
|
||||||
|
async def act(c: Client) -> bool:
|
||||||
|
"""若该客户端有待办则提交一个动作,返回是否提交过。"""
|
||||||
|
p = c.pending()
|
||||||
|
if not p:
|
||||||
|
return False
|
||||||
|
key = p["key"]
|
||||||
|
if p.get("input_text"):
|
||||||
|
text = "大家听我分析,我目前是好人视角,先跟票看看。" if p.get("purpose") != "election" \
|
||||||
|
else "我想拿警徽带队,请大家支持。"
|
||||||
|
await c.send({"type": "speech", "text": text})
|
||||||
|
return True
|
||||||
|
if key == "elect":
|
||||||
|
await c.send({"type": "action", "action": "elect", "target": random.choice([1, None])})
|
||||||
|
return True
|
||||||
|
options = p.get("options") or []
|
||||||
|
if p.get("allow_skip") and random.random() < 0.15:
|
||||||
|
await c.send({"type": "action", "action": key, "target": None})
|
||||||
|
return True
|
||||||
|
if not options:
|
||||||
|
await c.send({"type": "action", "action": key, "target": None})
|
||||||
|
return True
|
||||||
|
me = c.seat()
|
||||||
|
pool = [o for o in options if o != me] or options
|
||||||
|
await c.send({"type": "action", "action": key, "target": random.choice(pool)})
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"{BASE}/api/rooms",
|
||||||
|
data=json.dumps({"board": "classic12", "host_name": "甲"}).encode(),
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
code = json.loads(urllib.request.urlopen(req).read())["code"]
|
||||||
|
print("房间:", code)
|
||||||
|
|
||||||
|
a, b = Client("甲"), Client("乙")
|
||||||
|
await a.connect(code)
|
||||||
|
await b.connect(code)
|
||||||
|
await asyncio.sleep(0.6)
|
||||||
|
assert a.seat() == 1, f"甲座位错误: {a.seat()}"
|
||||||
|
assert b.seat() == 2, f"乙座位错误: {b.seat()}"
|
||||||
|
print("座位分配 OK: 甲=1号(房主), 乙=2号")
|
||||||
|
|
||||||
|
await a.send({"type": "control", "op": "fill_ai"})
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
n = len(a.snap["players"])
|
||||||
|
assert n == 12, f"补AI后玩家数 {n}"
|
||||||
|
print("一键补满 AI OK: 12 名玩家")
|
||||||
|
|
||||||
|
# 乙断线重连(大厅阶段)
|
||||||
|
old_token = b.token
|
||||||
|
await b.ws.close()
|
||||||
|
await asyncio.sleep(0.4)
|
||||||
|
await b.connect(code, token=old_token)
|
||||||
|
await asyncio.sleep(0.6)
|
||||||
|
assert b.seat() == 2, f"重连后座位错误: {b.seat()}"
|
||||||
|
print("断线重连 OK: 乙重新回到 2 号")
|
||||||
|
|
||||||
|
await a.send({"type": "control", "op": "start"})
|
||||||
|
await asyncio.sleep(1.0)
|
||||||
|
assert a.phase() != "lobby", "开局失败"
|
||||||
|
print("开局 OK, 阶段:", a.snap["phase_text"])
|
||||||
|
print("甲身份:", a.snap["me"].get("role_name"), "| 乙身份:", b.snap["me"].get("role_name"))
|
||||||
|
|
||||||
|
# 自动打完整局
|
||||||
|
deadline = asyncio.get_event_loop().time() + 300
|
||||||
|
acted_log = []
|
||||||
|
while asyncio.get_event_loop().time() < deadline:
|
||||||
|
for c in (a, b):
|
||||||
|
if await act(c):
|
||||||
|
acted_log.append((c.name, c.phase()))
|
||||||
|
if a.phase() == "game_over" and b.phase() == "game_over":
|
||||||
|
break
|
||||||
|
await asyncio.sleep(0.4)
|
||||||
|
|
||||||
|
print("\n=== 对局结束 ===")
|
||||||
|
sa, sb = a.snap, b.snap
|
||||||
|
assert sa["phase"] == "game_over", f"超时未结束: {sa['phase']} {sa['phase_text']}"
|
||||||
|
print("胜者:", sa["winner"], "|", sa["winner_text"])
|
||||||
|
print("甲(1号)身份:", next(p["role_name"] for p in sa["players"] if p["seat"] == 1),
|
||||||
|
"存活:", next(p["alive"] for p in sa["players"] if p["seat"] == 1))
|
||||||
|
print("乙(2号)身份:", next(p["role_name"] for p in sb["players"] if p["seat"] == 2),
|
||||||
|
"存活:", next(p["alive"] for p in sb["players"] if p["seat"] == 2))
|
||||||
|
print("结算页显示全部身份:", all(p["role_name"] for p in sa["players"]))
|
||||||
|
|
||||||
|
# 对局中乙断线重连测试
|
||||||
|
token_b = b.token
|
||||||
|
await b.ws.close()
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
await b.connect(code, token=token_b)
|
||||||
|
await asyncio.sleep(0.8)
|
||||||
|
assert b.seat() == 2 and b.phase() == "game_over"
|
||||||
|
print("对局结束后重连 OK")
|
||||||
|
|
||||||
|
# 房主 reset 回大厅再来一局
|
||||||
|
await a.send({"type": "control", "op": "reset"})
|
||||||
|
await asyncio.sleep(0.6)
|
||||||
|
assert a.phase() == "lobby", f"reset 失败: {a.phase()}"
|
||||||
|
print("再来一局(reset) OK")
|
||||||
|
await a.send({"type": "control", "op": "start"})
|
||||||
|
await asyncio.sleep(1.0)
|
||||||
|
assert a.phase() != "lobby", "第二局开局失败"
|
||||||
|
print("第二局开局 OK:", a.snap["phase_text"])
|
||||||
|
|
||||||
|
print("\n✅ 全部联机测试通过")
|
||||||
|
await a.ws.close()
|
||||||
|
await b.ws.close()
|
||||||
|
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
*.local
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>狼人杀 · Werewolf</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "werewolf-frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-router-dom": "^6.26.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.3.11",
|
||||||
|
"@types/react-dom": "^18.3.1",
|
||||||
|
"@vitejs/plugin-react": "^4.3.2",
|
||||||
|
"sass": "^1.79.5",
|
||||||
|
"typescript": "^5.6.3",
|
||||||
|
"vite": "^5.4.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+1339
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,3 @@
|
|||||||
|
allowBuilds:
|
||||||
|
'@parcel/watcher': set this to true or false
|
||||||
|
esbuild: set this to true or false
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { BrowserRouter, Route, Routes } from 'react-router-dom'
|
||||||
|
import Lobby from './pages/Lobby'
|
||||||
|
import Room from './pages/Room'
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Lobby />} />
|
||||||
|
<Route path="/room/:code" element={<Room />} />
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
// 带自动重连的 WebSocket 客户端封装
|
||||||
|
export class WsClient {
|
||||||
|
private ws: WebSocket | null = null
|
||||||
|
private url: string
|
||||||
|
private closedByUser = false
|
||||||
|
private retryTimer: number | null = null
|
||||||
|
|
||||||
|
onMessage: (data: any) => void = () => {}
|
||||||
|
onClose: (code: number) => void = () => {}
|
||||||
|
onStateChange: (connected: boolean) => void = () => {}
|
||||||
|
|
||||||
|
constructor(url: string) {
|
||||||
|
this.url = url
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.closedByUser = false
|
||||||
|
const ws = new WebSocket(this.url)
|
||||||
|
this.ws = ws
|
||||||
|
ws.onopen = () => this.onStateChange(true)
|
||||||
|
ws.onmessage = (ev) => {
|
||||||
|
try {
|
||||||
|
this.onMessage(JSON.parse(ev.data))
|
||||||
|
} catch {
|
||||||
|
/* 忽略非 JSON 消息 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ws.onclose = (ev) => {
|
||||||
|
this.onStateChange(false)
|
||||||
|
this.ws = null
|
||||||
|
this.onClose(ev.code)
|
||||||
|
if (!this.closedByUser && ev.code !== 4404 && ev.code !== 4403) {
|
||||||
|
this.retryTimer = window.setTimeout(() => this.connect(), 2000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ws.onerror = () => ws.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
send(data: unknown): boolean {
|
||||||
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||||
|
this.ws.send(JSON.stringify(data))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.closedByUser = true
|
||||||
|
if (this.retryTimer) window.clearTimeout(this.retryTimer)
|
||||||
|
if (this.ws) this.ws.close()
|
||||||
|
this.ws = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wsUrl(code: string, name: string, token: string | null): string {
|
||||||
|
const proto = location.protocol === 'https:' ? 'wss' : 'ws'
|
||||||
|
const params = new URLSearchParams({ name })
|
||||||
|
if (token) params.set('token', token)
|
||||||
|
return `${proto}://${location.host}/ws/${code}?${params.toString()}`
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
@use '../styles/variables' as *;
|
||||||
|
|
||||||
|
.action-panel {
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: $panel;
|
||||||
|
border: 1px solid $border;
|
||||||
|
border-radius: $radius;
|
||||||
|
padding: 12px 16px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-bar {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 3px;
|
||||||
|
background: var(--border);
|
||||||
|
|
||||||
|
.fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, var(--gold), var(--accent));
|
||||||
|
transition: width 0.5s linear;
|
||||||
|
|
||||||
|
&.urgent {
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.waiting {
|
||||||
|
text-align: center;
|
||||||
|
color: $text-dim;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt {
|
||||||
|
font-size: 14px;
|
||||||
|
color: $text;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-row,
|
||||||
|
.speech-box,
|
||||||
|
.pick-box {
|
||||||
|
animation: fade-up 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btns {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 发言 ---------- */
|
||||||
|
.speech-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
flex: 1;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiny-hint {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: $text-faint;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 选择目标 ---------- */
|
||||||
|
.seat-btns {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
max-height: 120px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seat-btn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1px;
|
||||||
|
min-width: 72px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
b {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 11px;
|
||||||
|
color: $text-dim;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.skip {
|
||||||
|
border-style: dashed;
|
||||||
|
color: $text-dim;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
border-color: var(--gold);
|
||||||
|
color: var(--gold);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 大厅 ---------- */
|
||||||
|
.ap-lobby-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.inline {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: $text-dim;
|
||||||
|
|
||||||
|
select {
|
||||||
|
padding: 7px 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.seat-list {
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seat-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: $text-dim;
|
||||||
|
border: 1px solid $border;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
|
||||||
|
.x {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: $text-faint;
|
||||||
|
padding: 0 2px;
|
||||||
|
font-size: 11px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $accent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import type { PendingView, Snapshot } from '../types'
|
||||||
|
import './ActionPanel.scss'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
snap: Snapshot
|
||||||
|
pending: PendingView | null
|
||||||
|
isMyTurn: boolean
|
||||||
|
onAction: (key: string, target: number | null) => void
|
||||||
|
onSpeech: (text: string) => void
|
||||||
|
onControl: (op: string, extra?: Record<string, unknown>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const SKIP_LABELS: Record<string, string> = {
|
||||||
|
vote: '弃票',
|
||||||
|
kill: '空刀',
|
||||||
|
protect: '空守',
|
||||||
|
witch_save: '不使用解药',
|
||||||
|
witch_poison: '不使用毒药',
|
||||||
|
shoot: '不开枪',
|
||||||
|
badge: '撕毁警徽',
|
||||||
|
}
|
||||||
|
|
||||||
|
function totalSeconds(snap: Snapshot, key: string): number {
|
||||||
|
const s = snap.settings
|
||||||
|
if (key === 'speech') return s.speech_seconds
|
||||||
|
if (['vote', 'sheriff_vote', 'elect', 'shoot', 'badge'].includes(key)) return s.vote_seconds
|
||||||
|
return s.night_seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ActionPanel(props: Props) {
|
||||||
|
const { snap } = props
|
||||||
|
if (snap.phase === 'game_over') return null
|
||||||
|
|
||||||
|
if (snap.phase === 'lobby') return <LobbyControls {...props} />
|
||||||
|
return <GameControls {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================ 大厅控制 ============================ */
|
||||||
|
function LobbyControls({ snap, onControl }: Props) {
|
||||||
|
const isHost = snap.me?.is_host
|
||||||
|
const occupied = snap.players.filter((p) => !p.empty)
|
||||||
|
const canStart = occupied.length === snap.seat_count
|
||||||
|
|
||||||
|
if (!isHost) {
|
||||||
|
return (
|
||||||
|
<footer className="action-panel">
|
||||||
|
<div className="waiting">
|
||||||
|
<span className="spinner" /> 等待房主操作…({occupied.length}/{snap.seat_count} 人已入座)
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer className="action-panel ap-lobby">
|
||||||
|
<div className="ap-lobby-row">
|
||||||
|
<button onClick={() => onControl('add_ai')}>+ 添加 AI</button>
|
||||||
|
<button onClick={() => onControl('fill_ai')}>⚡ 一键补满 AI</button>
|
||||||
|
<label className="inline">
|
||||||
|
胜负规则
|
||||||
|
<select
|
||||||
|
value={snap.settings.win_rule}
|
||||||
|
onChange={(e) =>
|
||||||
|
onControl('update_settings', { settings: { win_rule: e.target.value } })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="bian">屠边(杀光神职或村民)</option>
|
||||||
|
<option value="cheng">屠城(狼数 ≥ 好人数)</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<div className="spacer" />
|
||||||
|
<button
|
||||||
|
className="primary"
|
||||||
|
disabled={!canStart}
|
||||||
|
title={canStart ? '' : '座位未满'}
|
||||||
|
onClick={() => onControl('start')}
|
||||||
|
>
|
||||||
|
🎮 开始游戏
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="seat-list">
|
||||||
|
{occupied.map((p) => (
|
||||||
|
<span key={p.seat} className="seat-pill">
|
||||||
|
{p.seat}号 {p.name}
|
||||||
|
{!p.is_ai && p.seat !== snap.host_seat && (
|
||||||
|
<button
|
||||||
|
className="x"
|
||||||
|
title="移除"
|
||||||
|
onClick={() => onControl('remove', { seat: p.seat })}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{p.is_ai && (
|
||||||
|
<button className="x" title="移除" onClick={() => onControl('remove', { seat: p.seat })}>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================ 对局控制 ============================ */
|
||||||
|
function GameControls({ snap, pending, isMyTurn, onAction, onSpeech }: Props) {
|
||||||
|
const [text, setText] = useState('')
|
||||||
|
const me = snap.me
|
||||||
|
|
||||||
|
let body: JSX.Element
|
||||||
|
|
||||||
|
if (!pending) {
|
||||||
|
const hint = snap.spectator
|
||||||
|
? ' 👁️ 观战中,紧张的对局正在进行…'
|
||||||
|
: me && !me.alive
|
||||||
|
? ' 💀 你已出局,正在观战。'
|
||||||
|
: ' ⏳ 等待其他玩家行动…'
|
||||||
|
body = <div className="waiting">{hint}</div>
|
||||||
|
} else if (pending.input_text) {
|
||||||
|
const isElection = pending.purpose === 'election'
|
||||||
|
body = (
|
||||||
|
<div className="speech-box">
|
||||||
|
<div className="prompt">
|
||||||
|
🎤 {pending.text}
|
||||||
|
{isElection ? '(竞选发言)' : ''}
|
||||||
|
</div>
|
||||||
|
<div className="speech-row">
|
||||||
|
<textarea
|
||||||
|
value={text}
|
||||||
|
maxLength={300}
|
||||||
|
rows={2}
|
||||||
|
placeholder="输入你的发言…(像真人一样说话)"
|
||||||
|
onChange={(e) => setText(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
||||||
|
if (text.trim()) {
|
||||||
|
onSpeech(text)
|
||||||
|
setText('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="primary"
|
||||||
|
disabled={!text.trim()}
|
||||||
|
onClick={() => {
|
||||||
|
onSpeech(text)
|
||||||
|
setText('')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
发言
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="tiny-hint">Ctrl+Enter 发送 · {text.length}/300</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
} else if (pending.key === 'elect') {
|
||||||
|
body = (
|
||||||
|
<div className="prompt-row">
|
||||||
|
<div className="prompt">{pending.text}</div>
|
||||||
|
<div className="btns">
|
||||||
|
<button className="gold" onClick={() => onAction('elect', 1)}>
|
||||||
|
🙋 我要上警
|
||||||
|
</button>
|
||||||
|
<button onClick={() => onAction('elect', null)}>不参与</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
const skipLabel = pending.allow_skip ? SKIP_LABELS[pending.key] : null
|
||||||
|
const optionLabel = (seat: number): string => {
|
||||||
|
if (pending.key === 'witch_save') return `救 ${seat}号`
|
||||||
|
if (pending.key === 'witch_poison') return `毒 ${seat}号`
|
||||||
|
if (pending.key === 'kill') return `刀 ${seat}号`
|
||||||
|
return `${seat}号`
|
||||||
|
}
|
||||||
|
body = (
|
||||||
|
<div className="pick-box">
|
||||||
|
<div className="prompt">{pending.text}</div>
|
||||||
|
<div className="seat-btns">
|
||||||
|
{pending.options.map((seat) => {
|
||||||
|
const p = snap.players.find((x) => x.seat === seat)
|
||||||
|
return (
|
||||||
|
<button key={seat} className="seat-btn" onClick={() => onAction(pending.key, seat)}>
|
||||||
|
<b>{optionLabel(seat)}</b>
|
||||||
|
<span>{p?.name}</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{skipLabel && (
|
||||||
|
<button className="seat-btn skip" onClick={() => onAction(pending.key, null)}>
|
||||||
|
<b>{skipLabel}</b>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pct = pending
|
||||||
|
? Math.max(0, Math.min(100, (pending.seconds_left / totalSeconds(snap, pending.key)) * 100))
|
||||||
|
: 0
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer className="action-panel">
|
||||||
|
{isMyTurn && (
|
||||||
|
<div className="timer-bar">
|
||||||
|
<div
|
||||||
|
className={`fill ${pending && pending.seconds_left <= 10 ? 'urgent' : ''}`}
|
||||||
|
style={{ width: `${pct}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{body}
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
@use '../styles/variables' as *;
|
||||||
|
|
||||||
|
.game-log {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-title {
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: $text-dim;
|
||||||
|
border-bottom: 1px solid $border;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 10px 14px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: $text-dim;
|
||||||
|
word-break: break-all;
|
||||||
|
|
||||||
|
.lock {
|
||||||
|
margin-right: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tag-phase {
|
||||||
|
text-align: center;
|
||||||
|
color: $gold;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tag-speech {
|
||||||
|
color: $text;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
background: $hover;
|
||||||
|
border-left: 2px solid var(--gold);
|
||||||
|
display: block;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 0 8px 8px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tag-night {
|
||||||
|
color: $night;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tag-vote {
|
||||||
|
color: $purple;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.private {
|
||||||
|
color: $purple;
|
||||||
|
background: $purple-soft;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tag-identity {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
background: $gold-soft;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
color: $text;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
import type { LogEntry } from '../types'
|
||||||
|
import './GameLog.scss'
|
||||||
|
|
||||||
|
export default function GameLog({ log }: { log: LogEntry[] }) {
|
||||||
|
const boxRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = boxRef.current
|
||||||
|
if (el) el.scrollTop = el.scrollHeight
|
||||||
|
}, [log])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="game-log panel">
|
||||||
|
<div className="log-title">📜 对局记录</div>
|
||||||
|
<div className="log-body" ref={boxRef}>
|
||||||
|
{log.map((e, i) => (
|
||||||
|
<div key={i} className={`entry tag-${e.tag} ${e.kind === 'private' ? 'private' : ''}`}>
|
||||||
|
{e.kind === 'private' && <span className="lock">🔒</span>}
|
||||||
|
<span className="text">{e.text}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
@use '../styles/variables' as *;
|
||||||
|
|
||||||
|
.game-over {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: $overlay;
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 100;
|
||||||
|
animation: fade-up 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
width: min(680px, 92vw);
|
||||||
|
max-height: 88vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: $dialog-bg;
|
||||||
|
border: 1px solid $border;
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 28px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: $shadow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verdict {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
|
||||||
|
&.wolf {
|
||||||
|
color: $accent;
|
||||||
|
text-shadow: 0 0 24px $glow-accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.good {
|
||||||
|
color: $green;
|
||||||
|
text-shadow: 0 0 24px var(--green-soft);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub {
|
||||||
|
margin-top: 8px;
|
||||||
|
color: $text-dim;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reveal-grid {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reveal {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
.r-emoji {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.r-name {
|
||||||
|
color: var(--text);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.r-role {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 11px;
|
||||||
|
color: $text-dim;
|
||||||
|
|
||||||
|
&.werewolf {
|
||||||
|
color: $accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.seer,
|
||||||
|
&.witch,
|
||||||
|
&.hunter,
|
||||||
|
&.guard {
|
||||||
|
color: $gold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dead {
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btns {
|
||||||
|
margin-top: 22px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import type { Snapshot } from '../types'
|
||||||
|
import { ROLE_EMOJI } from '../types'
|
||||||
|
import './GameOver.scss'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
snap: Snapshot
|
||||||
|
isHost?: boolean
|
||||||
|
onReset: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function GameOver({ snap, isHost, onReset }: Props) {
|
||||||
|
const wolfWin = snap.winner === 'wolf'
|
||||||
|
return (
|
||||||
|
<div className="game-over">
|
||||||
|
<div className="dialog">
|
||||||
|
<div className={`verdict ${wolfWin ? 'wolf' : 'good'}`}>
|
||||||
|
{wolfWin ? '🐺 狼人阵营胜利' : '🎉 好人阵营胜利'}
|
||||||
|
</div>
|
||||||
|
{snap.winner_text && <p className="sub">{snap.winner_text}</p>}
|
||||||
|
|
||||||
|
<div className="reveal-grid">
|
||||||
|
{snap.players.map((p) => (
|
||||||
|
<div key={p.seat} className={`reveal ${p.alive ? '' : 'dead'}`}>
|
||||||
|
<span className="r-emoji">
|
||||||
|
{p.alive ? ROLE_EMOJI[p.role_key || ''] || '🧑' : '💀'}
|
||||||
|
</span>
|
||||||
|
<span className="r-name">
|
||||||
|
{p.seat}号 {p.name}
|
||||||
|
</span>
|
||||||
|
<span className={`r-role ${p.role_key}`}>{p.role_name}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="btns">
|
||||||
|
{isHost && (
|
||||||
|
<button className="primary" onClick={onReset}>
|
||||||
|
🔄 回到房间(再来一局)
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<a href="/">
|
||||||
|
<button className="ghost">返回大厅</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
@use '../styles/variables' as *;
|
||||||
|
|
||||||
|
.info-panel {
|
||||||
|
padding: 14px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spectate {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 15px;
|
||||||
|
color: $night;
|
||||||
|
padding: 6px 0;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: $text-faint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-card {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
.role-emoji {
|
||||||
|
font-size: 34px;
|
||||||
|
filter: drop-shadow(0 0 10px $glow-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-name {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text;
|
||||||
|
|
||||||
|
.camp {
|
||||||
|
margin-left: 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: $text-dim;
|
||||||
|
border: 1px solid $border;
|
||||||
|
padding: 1px 7px;
|
||||||
|
border-radius: 999px;
|
||||||
|
vertical-align: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-desc {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: $text-dim;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.waiting {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sec {
|
||||||
|
margin-top: 12px;
|
||||||
|
|
||||||
|
.sec-title {
|
||||||
|
font-size: 12px;
|
||||||
|
color: $text-dim;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chips {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid $border;
|
||||||
|
color: $text-dim;
|
||||||
|
|
||||||
|
&.host-chip {
|
||||||
|
color: $gold;
|
||||||
|
border-color: $gold;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.sheriff-chip {
|
||||||
|
color: $gold;
|
||||||
|
border-color: $gold;
|
||||||
|
background: $gold-soft;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.seat-chip {
|
||||||
|
color: $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dead-chip {
|
||||||
|
color: $accent;
|
||||||
|
border-color: $accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mate-chip {
|
||||||
|
color: $accent;
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: $accent-soft;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.good {
|
||||||
|
color: $green;
|
||||||
|
border-color: var(--green);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bad {
|
||||||
|
color: $accent;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.used {
|
||||||
|
opacity: 0.45;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import type { Snapshot } from '../types'
|
||||||
|
import { ROLE_EMOJI } from '../types'
|
||||||
|
import './InfoPanel.scss'
|
||||||
|
|
||||||
|
const CAMP_NAME: Record<string, string> = {
|
||||||
|
wolf: '狼人阵营',
|
||||||
|
god: '神职',
|
||||||
|
civilian: '村民',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function InfoPanel({ snap }: { snap: Snapshot }) {
|
||||||
|
const me = snap.me
|
||||||
|
return (
|
||||||
|
<div className="info-panel panel">
|
||||||
|
{snap.spectator || !me ? (
|
||||||
|
<div className="spectate">
|
||||||
|
<span>👁️</span> 观战模式
|
||||||
|
{snap.spectator_note && <p className="note">{snap.spectator_note}</p>}
|
||||||
|
</div>
|
||||||
|
) : me.role_key ? (
|
||||||
|
<>
|
||||||
|
<div className="role-card">
|
||||||
|
<span className="role-emoji">{me.role_emoji || ROLE_EMOJI[me.role_key]}</span>
|
||||||
|
<div>
|
||||||
|
<div className="role-name">
|
||||||
|
{me.role_name}
|
||||||
|
<span className="camp">{CAMP_NAME[me.role_camp || ''] || ''}</span>
|
||||||
|
</div>
|
||||||
|
<div className="role-desc">{me.role_desc}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="meta-row">
|
||||||
|
<span className="chip seat-chip">我是 {me.seat} 号</span>
|
||||||
|
{me.is_host && <span className="chip host-chip">房主</span>}
|
||||||
|
{snap.sheriff_seat === me.seat && <span className="chip sheriff-chip">⭐ 警长</span>}
|
||||||
|
{!me.alive && <span className="chip dead-chip">💀 已出局</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{me.teammates && me.teammates.length > 0 && (
|
||||||
|
<div className="sec">
|
||||||
|
<div className="sec-title">🐺 狼队友</div>
|
||||||
|
<div className="chips">
|
||||||
|
{me.teammates.map((s) => (
|
||||||
|
<span key={s} className="chip mate-chip">
|
||||||
|
{s}号
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{me.seer_results && me.seer_results.length > 0 && (
|
||||||
|
<div className="sec">
|
||||||
|
<div className="sec-title">🔮 查验记录</div>
|
||||||
|
<div className="chips">
|
||||||
|
{me.seer_results.map((r) => (
|
||||||
|
<span key={r.seat} className={`chip ${r.result === '狼人' ? 'bad' : 'good'}`}>
|
||||||
|
{r.seat}号 · {r.result}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{me.potions && (
|
||||||
|
<div className="sec">
|
||||||
|
<div className="sec-title">🧪 药水</div>
|
||||||
|
<div className="chips">
|
||||||
|
<span className={`chip ${me.potions.antidote ? 'good' : 'used'}`}>
|
||||||
|
解药 {me.potions.antidote ? '✓' : '已用'}
|
||||||
|
</span>
|
||||||
|
<span className={`chip ${me.potions.poison ? 'bad' : 'used'}`}>
|
||||||
|
毒药 {me.potions.poison ? '✓' : '已用'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="role-card waiting">
|
||||||
|
<span className="role-emoji">🎭</span>
|
||||||
|
<div>
|
||||||
|
<div className="role-name">身份待分发</div>
|
||||||
|
<div className="role-desc">等待房主开始游戏</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
@use '../styles/variables' as *;
|
||||||
|
|
||||||
|
.phase-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 18px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid $border;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
color: $text;
|
||||||
|
background: $panel;
|
||||||
|
|
||||||
|
&.night {
|
||||||
|
background: $night-soft;
|
||||||
|
border-color: var(--night);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.day {
|
||||||
|
background: $gold-soft;
|
||||||
|
border-color: var(--gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-chip {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: $text-dim;
|
||||||
|
border: 1px solid $border;
|
||||||
|
padding: 2px 9px;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
font-size: 14px;
|
||||||
|
color: $gold;
|
||||||
|
min-width: 36px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.urgent {
|
||||||
|
border-color: $accent;
|
||||||
|
animation: pulse-ring 1s ease-out infinite;
|
||||||
|
|
||||||
|
.countdown {
|
||||||
|
color: $accent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import type { Snapshot } from '../types'
|
||||||
|
import './PhaseBanner.scss'
|
||||||
|
|
||||||
|
export default function PhaseBanner({ snap }: { snap: Snapshot }) {
|
||||||
|
const { phase, phase_text, day, pending } = snap
|
||||||
|
const isNight = phase === 'night'
|
||||||
|
const urgent = !!pending && pending.seconds_left <= 10 && pending.seconds_left > 0
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`phase-banner ${isNight ? 'night' : 'day'} ${urgent ? 'urgent' : ''}`}>
|
||||||
|
{day > 0 && phase !== 'game_over' && (
|
||||||
|
<span className="day-chip">{day > 0 ? `第${day}天` : ''}</span>
|
||||||
|
)}
|
||||||
|
<span className="phase-name">{phase_text}</span>
|
||||||
|
{pending && !pending.input_text && (
|
||||||
|
<span className="countdown">{pending.seconds_left}s</span>
|
||||||
|
)}
|
||||||
|
{pending && pending.input_text && (
|
||||||
|
<span className="countdown">{pending.seconds_left}s</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
@use '../styles/variables' as *;
|
||||||
|
|
||||||
|
.seat-circle {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-info {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
text-align: center;
|
||||||
|
width: 220px;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
.phase-icon {
|
||||||
|
font-size: 42px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
|
||||||
|
&.moon {
|
||||||
|
filter: drop-shadow(0 0 22px $glow-night);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.sun {
|
||||||
|
filter: drop-shadow(0 0 22px $glow-gold);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-text {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
color: $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheriff-line {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: $gold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.seat {
|
||||||
|
position: absolute;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 108px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: default;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 62px;
|
||||||
|
height: 62px;
|
||||||
|
margin: 0 auto;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: $avatar-grad;
|
||||||
|
border: 2px solid $border-strong;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 30px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seat-name {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: $text-dim;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-tag {
|
||||||
|
margin-top: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: $text-faint;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&.werewolf {
|
||||||
|
color: $accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.seer,
|
||||||
|
&.witch,
|
||||||
|
&.hunter,
|
||||||
|
&.guard {
|
||||||
|
color: $gold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.me .avatar {
|
||||||
|
border-color: $gold;
|
||||||
|
box-shadow: 0 0 12px $glow-gold;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.me .seat-name {
|
||||||
|
color: $gold;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.speaking .avatar {
|
||||||
|
border-color: $accent;
|
||||||
|
animation: pulse-ring 1.2s ease-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dead .avatar {
|
||||||
|
filter: grayscale(1);
|
||||||
|
opacity: 0.45;
|
||||||
|
border-style: dashed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dead .seat-name {
|
||||||
|
text-decoration: line-through;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.offline .avatar {
|
||||||
|
border-style: dashed;
|
||||||
|
border-color: $accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.empty .avatar {
|
||||||
|
opacity: 0.35;
|
||||||
|
border-style: dotted;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pickable {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
border-color: $green;
|
||||||
|
box-shadow: 0 0 0 4px $green-soft;
|
||||||
|
transform: scale(1.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .avatar {
|
||||||
|
border-color: $gold;
|
||||||
|
box-shadow: 0 0 0 6px $glow-gold;
|
||||||
|
transform: scale(1.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.seat-name {
|
||||||
|
color: $green;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -6px;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 1px 7px;
|
||||||
|
border-radius: 999px;
|
||||||
|
white-space: nowrap;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheriff-badge {
|
||||||
|
left: 8px;
|
||||||
|
background: $gold-soft;
|
||||||
|
border: 1px solid $gold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cand-badge {
|
||||||
|
right: -4px;
|
||||||
|
background: $night-soft;
|
||||||
|
border: 1px solid $night;
|
||||||
|
color: $night;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-badge {
|
||||||
|
right: -4px;
|
||||||
|
bottom: 26px;
|
||||||
|
background: $accent-soft;
|
||||||
|
border: 1px solid $accent;
|
||||||
|
color: $accent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 980px) {
|
||||||
|
.seat {
|
||||||
|
width: 84px;
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-info {
|
||||||
|
width: 150px;
|
||||||
|
|
||||||
|
.phase-icon {
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-text {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import type { PlayerView, Snapshot } from '../types'
|
||||||
|
import { ROLE_EMOJI } from '../types'
|
||||||
|
import './SeatCircle.scss'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
snap: Snapshot
|
||||||
|
pickable: Set<number>
|
||||||
|
onPick: (seat: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SeatCircle({ snap, pickable, onPick }: Props) {
|
||||||
|
const radius = 40
|
||||||
|
const players = snap.players
|
||||||
|
|
||||||
|
const voteCount = (seat: number): number => {
|
||||||
|
if (snap.phase !== 'vote' && snap.phase !== 'sheriff_elect') return 0
|
||||||
|
return snap.last_votes.filter((v) => v.target === seat).length
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="seat-circle">
|
||||||
|
<div className="center-info">
|
||||||
|
<div className={`phase-icon ${snap.phase === 'night' ? 'moon' : 'sun'}`}>
|
||||||
|
{snap.phase === 'night' ? '🌙' : snap.phase === 'game_over' ? '🏁' : '☀️'}
|
||||||
|
</div>
|
||||||
|
<div className="center-text">{snap.phase_text}</div>
|
||||||
|
{snap.sheriff_seat != null && (
|
||||||
|
<div className="sheriff-line">⭐ 警长:{snap.sheriff_seat}号</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{players.map((p) => (
|
||||||
|
<SeatCard
|
||||||
|
key={p.seat}
|
||||||
|
p={p}
|
||||||
|
angle={(p.seat / players.length) * 2 * Math.PI - Math.PI / 2}
|
||||||
|
radiusX={radius}
|
||||||
|
radiusY={radius}
|
||||||
|
snap={snap}
|
||||||
|
pickable={pickable.has(p.seat)}
|
||||||
|
votes={voteCount(p.seat)}
|
||||||
|
onPick={onPick}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SeatProps {
|
||||||
|
p: PlayerView
|
||||||
|
angle: number
|
||||||
|
radiusX: number
|
||||||
|
radiusY: number
|
||||||
|
snap: Snapshot
|
||||||
|
pickable: boolean
|
||||||
|
votes: number
|
||||||
|
onPick: (seat: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function SeatCard({ p, angle, radiusX, radiusY, snap, pickable, votes, onPick }: SeatProps) {
|
||||||
|
// 用椭圆半径铺满舞台,组件内再换算成 px 由 CSS 缩放
|
||||||
|
const x = 50 + radiusX * Math.cos(angle)
|
||||||
|
const y = 50 + radiusY * Math.sin(angle)
|
||||||
|
const isMe = p.is_me
|
||||||
|
const dead = !p.empty && !p.alive
|
||||||
|
const myRoleVisible = !!p.role_name
|
||||||
|
|
||||||
|
const emoji = p.empty
|
||||||
|
? '➕'
|
||||||
|
: dead
|
||||||
|
? '💀'
|
||||||
|
: p.is_ai
|
||||||
|
? '🤖'
|
||||||
|
: '🧑'
|
||||||
|
|
||||||
|
const mate = snap.me?.teammates?.includes(p.seat)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={[
|
||||||
|
'seat',
|
||||||
|
p.empty ? 'empty' : '',
|
||||||
|
dead ? 'dead' : '',
|
||||||
|
isMe ? 'me' : '',
|
||||||
|
p.speaking ? 'speaking' : '',
|
||||||
|
p.sheriff ? 'sheriff' : '',
|
||||||
|
pickable ? 'pickable' : '',
|
||||||
|
p.connected ? '' : 'offline',
|
||||||
|
].join(' ')}
|
||||||
|
style={{ left: `${x}%`, top: `${y}%` }}
|
||||||
|
onClick={pickable ? () => onPick(p.seat) : undefined}
|
||||||
|
title={pickable ? `选择 ${p.seat} 号` : undefined}
|
||||||
|
>
|
||||||
|
{p.sheriff && <span className="badge sheriff-badge">⭐</span>}
|
||||||
|
{p.candidate && <span className="badge cand-badge">上警</span>}
|
||||||
|
{votes > 0 && <span className="badge vote-badge">{votes}票</span>}
|
||||||
|
<div className="avatar">{emoji}</div>
|
||||||
|
<div className="seat-name">
|
||||||
|
{p.empty ? '空位' : `${p.seat}号 ${p.name}`}
|
||||||
|
</div>
|
||||||
|
{myRoleVisible && (
|
||||||
|
<div className={`role-tag ${p.role_key}`}>
|
||||||
|
{ROLE_EMOJI[p.role_key || ''] || ''} {p.role_name}
|
||||||
|
{mate ? ' ·队友' : ''}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
// 样式在 global.scss 的 .theme-toggle 中统一维护
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import './ThemeToggle.scss'
|
||||||
|
|
||||||
|
export type Theme = 'light' | 'dark'
|
||||||
|
|
||||||
|
/** 在 React 渲染前调用,避免主题闪烁 */
|
||||||
|
export function initTheme(): Theme {
|
||||||
|
const saved = localStorage.getItem('ww_theme') as Theme | null
|
||||||
|
const theme: Theme = saved === 'dark' ? 'dark' : 'light'
|
||||||
|
document.documentElement.dataset.theme = theme
|
||||||
|
return theme
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ThemeToggle() {
|
||||||
|
const [theme, setTheme] = useState<Theme>(
|
||||||
|
() => (document.documentElement.dataset.theme as Theme) || initTheme(),
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.documentElement.dataset.theme = theme
|
||||||
|
localStorage.setItem('ww_theme', theme)
|
||||||
|
}, [theme])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="theme-toggle"
|
||||||
|
title={theme === 'light' ? '切换到暗色模式' : '切换到亮色模式'}
|
||||||
|
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
|
||||||
|
>
|
||||||
|
{theme === 'light' ? '🌙' : '☀️'}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import App from './App'
|
||||||
|
import { initTheme } from './components/ThemeToggle'
|
||||||
|
import './styles/global.scss'
|
||||||
|
|
||||||
|
initTheme()
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
@use '../styles/variables' as *;
|
||||||
|
|
||||||
|
.lobby {
|
||||||
|
min-height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lobby-card {
|
||||||
|
position: relative;
|
||||||
|
width: 420px;
|
||||||
|
max-width: 100%;
|
||||||
|
background: $panel-strong;
|
||||||
|
border: 1px solid $border;
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 36px 32px;
|
||||||
|
animation: fade-up 0.35s ease;
|
||||||
|
box-shadow: $shadow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-top {
|
||||||
|
position: absolute;
|
||||||
|
top: 14px;
|
||||||
|
right: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
|
||||||
|
.moon {
|
||||||
|
font-size: 40px;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
filter: drop-shadow(0 0 18px $glow-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 34px;
|
||||||
|
letter-spacing: 10px;
|
||||||
|
text-indent: 10px;
|
||||||
|
background: $logo-grad;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 3px;
|
||||||
|
color: $text-faint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
color: $text-dim;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: $text-faint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button.big {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
color: $text-faint;
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 20px 0;
|
||||||
|
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background: $border;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.code-input {
|
||||||
|
flex: 1;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
margin-top: 14px;
|
||||||
|
color: $accent;
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-status {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: $text-dim;
|
||||||
|
|
||||||
|
.ok {
|
||||||
|
color: $green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fallback {
|
||||||
|
color: $gold;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import type { BoardInfo } from '../types'
|
||||||
|
import ThemeToggle from '../components/ThemeToggle'
|
||||||
|
import './Lobby.scss'
|
||||||
|
|
||||||
|
export function getNickname(): string {
|
||||||
|
return localStorage.getItem('ww_name') || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setNickname(name: string) {
|
||||||
|
localStorage.setItem('ww_name', name)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Lobby() {
|
||||||
|
const nav = useNavigate()
|
||||||
|
const [name, setName] = useState(getNickname())
|
||||||
|
const [boards, setBoards] = useState<BoardInfo[]>([])
|
||||||
|
const [board, setBoard] = useState('classic12')
|
||||||
|
const [joinCode, setJoinCode] = useState('')
|
||||||
|
const [health, setHealth] = useState<{ llm_configured: boolean; model: string | null } | null>(null)
|
||||||
|
const [busy, setBusy] = useState(false)
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('/api/health').then((r) => r.json()).then(setHealth).catch(() => {})
|
||||||
|
fetch('/api/boards').then((r) => r.json()).then((b: BoardInfo[]) => {
|
||||||
|
setBoards(b)
|
||||||
|
if (b.length && !b.some((x) => x.key === board)) setBoard(b[0].key)
|
||||||
|
}).catch(() => {})
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const ensureName = (): boolean => {
|
||||||
|
if (!name.trim()) {
|
||||||
|
setError('请先输入你的昵称')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
setNickname(name.trim())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const createRoom = async () => {
|
||||||
|
if (!ensureName()) return
|
||||||
|
setBusy(true)
|
||||||
|
setError('')
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/rooms', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ board, host_name: name.trim() }),
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
nav(`/room/${data.code}`)
|
||||||
|
} catch {
|
||||||
|
setError('创建房间失败,请确认后端已启动')
|
||||||
|
} finally {
|
||||||
|
setBusy(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const joinRoom = () => {
|
||||||
|
if (!ensureName()) return
|
||||||
|
const code = joinCode.trim().toUpperCase()
|
||||||
|
if (!code) {
|
||||||
|
setError('请输入房间号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nav(`/room/${code}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedBoard = boards.find((b) => b.key === board)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="lobby">
|
||||||
|
<div className="lobby-card">
|
||||||
|
<div className="card-top">
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
|
<div className="logo">
|
||||||
|
<span className="moon">🌕</span>
|
||||||
|
<h1>狼人杀</h1>
|
||||||
|
<p className="subtitle">WEREWOLVES · 人机混战 · 多人联机</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="field">
|
||||||
|
<label>你的昵称</label>
|
||||||
|
<input
|
||||||
|
value={name}
|
||||||
|
maxLength={12}
|
||||||
|
placeholder="给自己起个名字"
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && createRoom()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="field">
|
||||||
|
<label>选择板子</label>
|
||||||
|
<select value={board} onChange={(e) => setBoard(e.target.value)}>
|
||||||
|
{boards.map((b) => (
|
||||||
|
<option key={b.key} value={b.key}>
|
||||||
|
{b.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{selectedBoard && <p className="hint">共 {selectedBoard.seat_count} 个座位</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button className="primary big" disabled={busy} onClick={createRoom}>
|
||||||
|
{busy ? '创建中…' : '创建房间'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="divider">或者</div>
|
||||||
|
|
||||||
|
<div className="join-row">
|
||||||
|
<input
|
||||||
|
className="code-input"
|
||||||
|
value={joinCode}
|
||||||
|
maxLength={4}
|
||||||
|
placeholder="房间号"
|
||||||
|
onChange={(e) => setJoinCode(e.target.value.toUpperCase())}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && joinRoom()}
|
||||||
|
/>
|
||||||
|
<button onClick={joinRoom}>加入房间</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <p className="error">{error}</p>}
|
||||||
|
|
||||||
|
{health && (
|
||||||
|
<p className="ai-status">
|
||||||
|
AI 玩家:
|
||||||
|
{health.llm_configured ? (
|
||||||
|
<span className="ok">大模型已接入({health.model})</span>
|
||||||
|
) : (
|
||||||
|
<span className="fallback">未配置密钥,使用内置启发式 AI</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
@use '../styles/variables' as *;
|
||||||
|
|
||||||
|
.room {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 12px 16px;
|
||||||
|
gap: 12px;
|
||||||
|
max-width: 1500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
&.theme-night .stage {
|
||||||
|
background:
|
||||||
|
radial-gradient(600px 300px at 50% 0%, $night-soft, transparent 70%),
|
||||||
|
$panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.theme-day .stage {
|
||||||
|
background:
|
||||||
|
radial-gradient(600px 300px at 50% 0%, $gold-soft, transparent 70%),
|
||||||
|
$panel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
// 结算弹窗出现时顶栏仍可操作(主题切换/退出)
|
||||||
|
position: relative;
|
||||||
|
z-index: 101;
|
||||||
|
|
||||||
|
.left,
|
||||||
|
.right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-chip {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
b {
|
||||||
|
letter-spacing: 2px;
|
||||||
|
color: $gold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-chip {
|
||||||
|
font-size: 12px;
|
||||||
|
color: $text-dim;
|
||||||
|
border: 1px solid $border;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
white-space: nowrap;
|
||||||
|
background: $panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conn {
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
&.on {
|
||||||
|
color: $green;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.off {
|
||||||
|
color: $accent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.exit {
|
||||||
|
color: $text-dim;
|
||||||
|
font-size: 13px;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 1px solid $border;
|
||||||
|
padding: 5px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $text;
|
||||||
|
border-color: $border-strong;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-main {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
position: relative;
|
||||||
|
transition: background 0.6s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 340px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-gone {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 18px;
|
||||||
|
color: $text-dim;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toasts {
|
||||||
|
position: fixed;
|
||||||
|
top: 18px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
z-index: 200;
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
background: $toast-bg;
|
||||||
|
border: 1px solid $border-strong;
|
||||||
|
color: $text;
|
||||||
|
padding: 9px 18px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 13px;
|
||||||
|
animation: fade-up 0.2s ease;
|
||||||
|
box-shadow: $shadow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 980px) {
|
||||||
|
.room-main {
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage {
|
||||||
|
min-height: 420px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
import { Link, useParams } from 'react-router-dom'
|
||||||
|
import { WsClient, wsUrl } from '../api/ws'
|
||||||
|
import type { Snapshot } from '../types'
|
||||||
|
import GameLog from '../components/GameLog'
|
||||||
|
import InfoPanel from '../components/InfoPanel'
|
||||||
|
import PhaseBanner from '../components/PhaseBanner'
|
||||||
|
import SeatCircle from '../components/SeatCircle'
|
||||||
|
import ActionPanel from '../components/ActionPanel'
|
||||||
|
import GameOver from '../components/GameOver'
|
||||||
|
import ThemeToggle from '../components/ThemeToggle'
|
||||||
|
import './Room.scss'
|
||||||
|
|
||||||
|
interface SnapState {
|
||||||
|
snap: Snapshot
|
||||||
|
receivedAt: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Room() {
|
||||||
|
const { code = '' } = useParams()
|
||||||
|
const [state, setState] = useState<SnapState | null>(null)
|
||||||
|
const [connected, setConnected] = useState(false)
|
||||||
|
const [gone, setGone] = useState(false)
|
||||||
|
const [toasts, setToasts] = useState<{ id: number; text: string }[]>([])
|
||||||
|
const [now, setNow] = useState(Date.now())
|
||||||
|
const clientRef = useRef<WsClient | null>(null)
|
||||||
|
const toastId = useRef(0)
|
||||||
|
|
||||||
|
const pushToast = useCallback((text: string) => {
|
||||||
|
const id = ++toastId.current
|
||||||
|
setToasts((ts) => [...ts, { id, text }])
|
||||||
|
window.setTimeout(() => setToasts((ts) => ts.filter((t) => t.id !== id)), 3200)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// token 存 sessionStorage:同浏览器多标签页互不干扰,刷新仍可断线重连
|
||||||
|
const token = sessionStorage.getItem(`ww_token_${code}`)
|
||||||
|
const name = localStorage.getItem('ww_name') || '玩家'
|
||||||
|
const client = new WsClient(wsUrl(code, name, token))
|
||||||
|
clientRef.current = client
|
||||||
|
client.onMessage = (data) => {
|
||||||
|
if (data.type === 'snapshot') {
|
||||||
|
const snap = data.data as Snapshot
|
||||||
|
if (snap.token) sessionStorage.setItem(`ww_token_${code}`, snap.token)
|
||||||
|
setState({ snap, receivedAt: Date.now() })
|
||||||
|
} else if (data.type === 'error') {
|
||||||
|
pushToast(data.message || '操作失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
client.onStateChange = setConnected
|
||||||
|
client.onClose = (c) => {
|
||||||
|
if (c === 4404) setGone(true)
|
||||||
|
}
|
||||||
|
client.connect()
|
||||||
|
const timer = window.setInterval(() => setNow(Date.now()), 500)
|
||||||
|
return () => {
|
||||||
|
window.clearInterval(timer)
|
||||||
|
client.close()
|
||||||
|
}
|
||||||
|
}, [code, pushToast])
|
||||||
|
|
||||||
|
const sendAction = useCallback((key: string, target: number | null) => {
|
||||||
|
clientRef.current?.send({ type: 'action', action: key, target })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const sendSpeech = useCallback((text: string) => {
|
||||||
|
clientRef.current?.send({ type: 'speech', text })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const sendControl = useCallback((op: string, extra: Record<string, unknown> = {}) => {
|
||||||
|
clientRef.current?.send({ type: 'control', op, ...extra })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (gone) {
|
||||||
|
return (
|
||||||
|
<div className="room-gone">
|
||||||
|
<p>🌙 房间 {code} 不存在或已解散</p>
|
||||||
|
<Link to="/">
|
||||||
|
<button className="primary">返回大厅</button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state) {
|
||||||
|
return (
|
||||||
|
<div className="room-gone">
|
||||||
|
<span className="spinner" /> 正在连接房间 {code} …
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { snap, receivedAt } = state
|
||||||
|
const secondsLeft = snap.pending
|
||||||
|
? Math.max(0, Math.ceil(snap.pending.seconds_left - (now - receivedAt) / 1000))
|
||||||
|
: 0
|
||||||
|
const pending = snap.pending ? { ...snap.pending, seconds_left: secondsLeft } : null
|
||||||
|
|
||||||
|
const isMyTurn =
|
||||||
|
!!pending && !snap.spectator && snap.me?.alive !== false
|
||||||
|
const pickable =
|
||||||
|
isMyTurn && pending && !pending.input_text && pending.key !== 'elect'
|
||||||
|
? new Set(pending.options)
|
||||||
|
: new Set<number>()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`room theme-${snap.phase === 'night' ? 'night' : 'day'}`}>
|
||||||
|
<header className="room-header">
|
||||||
|
<div className="left">
|
||||||
|
<button
|
||||||
|
className="code-chip"
|
||||||
|
title="点击复制房间号"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard?.writeText(snap.room_code)
|
||||||
|
pushToast('房间号已复制')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
房间 <b>{snap.room_code}</b> 📋
|
||||||
|
</button>
|
||||||
|
<span className={`conn ${connected ? 'on' : 'off'}`}>
|
||||||
|
{connected ? '已连接' : '重连中…'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<PhaseBanner snap={snap} />
|
||||||
|
<div className="right">
|
||||||
|
<ThemeToggle />
|
||||||
|
<span className="board-chip">
|
||||||
|
{snap.settings.board_name} · {snap.settings.win_rule === 'bian' ? '屠边' : '屠城'}
|
||||||
|
</span>
|
||||||
|
<Link to="/" className="exit">
|
||||||
|
退出
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="room-main">
|
||||||
|
<section className="stage panel">
|
||||||
|
<SeatCircle
|
||||||
|
snap={snap}
|
||||||
|
pickable={pickable}
|
||||||
|
onPick={(seat) => pending && sendAction(pending.key, seat)}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
<aside className="sidebar">
|
||||||
|
<InfoPanel snap={snap} />
|
||||||
|
<GameLog log={snap.log} />
|
||||||
|
</aside>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<ActionPanel
|
||||||
|
snap={snap}
|
||||||
|
pending={pending}
|
||||||
|
isMyTurn={isMyTurn}
|
||||||
|
onAction={sendAction}
|
||||||
|
onSpeech={sendSpeech}
|
||||||
|
onControl={sendControl}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{snap.phase === 'game_over' && (
|
||||||
|
<GameOver snap={snap} isHost={snap.me?.is_host} onReset={() => sendControl('reset')} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="toasts">
|
||||||
|
{toasts.map((t) => (
|
||||||
|
<div key={t.id} className="toast">
|
||||||
|
{t.text}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// 主题变量:颜色全部走 CSS 自定义属性(定义见 global.scss 的 :root 与 [data-theme]),
|
||||||
|
// 因此亮/暗切换即时生效;组件继续沿用这些 Sass 变量名即可。
|
||||||
|
// 亮色为默认主题(清爽浅色:白底 + 靛蓝主色 + 琥珀警徽金)。
|
||||||
|
|
||||||
|
// ---- 不随主题变化的常量 ----
|
||||||
|
$radius: 12px;
|
||||||
|
$radius-sm: 8px;
|
||||||
|
$font: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', 'Noto Sans SC', sans-serif;
|
||||||
|
|
||||||
|
// ---- 语义色引用 ----
|
||||||
|
$bg-glow-a: var(--bg-glow-a);
|
||||||
|
$bg-glow-b: var(--bg-glow-b);
|
||||||
|
$panel: var(--panel);
|
||||||
|
$panel-strong: var(--panel-strong);
|
||||||
|
$hover: var(--hover);
|
||||||
|
$border: var(--border);
|
||||||
|
$border-strong: var(--border-strong);
|
||||||
|
$input-bg: var(--input-bg);
|
||||||
|
$text: var(--text);
|
||||||
|
$text-dim: var(--text-dim);
|
||||||
|
$text-faint: var(--text-faint);
|
||||||
|
$primary: var(--primary);
|
||||||
|
$primary-deep: var(--primary-deep);
|
||||||
|
$primary-soft: var(--primary-soft);
|
||||||
|
$accent: var(--accent);
|
||||||
|
$accent-soft: var(--accent-soft);
|
||||||
|
$gold: var(--gold);
|
||||||
|
$gold-deep: var(--gold-deep);
|
||||||
|
$gold-soft: var(--gold-soft);
|
||||||
|
$night: var(--night);
|
||||||
|
$night-soft: var(--night-soft);
|
||||||
|
$green: var(--green);
|
||||||
|
$green-soft: var(--green-soft);
|
||||||
|
$purple: var(--purple);
|
||||||
|
$purple-soft: var(--purple-soft);
|
||||||
|
$overlay: var(--overlay);
|
||||||
|
$dialog-bg: var(--dialog-bg);
|
||||||
|
$shadow: var(--shadow);
|
||||||
|
$toast-bg: var(--toast-bg);
|
||||||
|
$logo-grad: var(--logo-grad);
|
||||||
|
$avatar-grad: var(--avatar-grad);
|
||||||
|
$glow-gold: var(--glow-gold);
|
||||||
|
$glow-night: var(--glow-night);
|
||||||
|
$glow-accent: var(--glow-accent);
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
@use './variables' as *;
|
||||||
|
|
||||||
|
/* ================= 主题定义 ================= */
|
||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
--bg-glow-a: rgba(91, 108, 240, 0.08);
|
||||||
|
--bg-glow-b: rgba(217, 153, 46, 0.08);
|
||||||
|
|
||||||
|
--panel: rgba(255, 255, 255, 0.72);
|
||||||
|
--panel-strong: #ffffff;
|
||||||
|
--hover: rgba(35, 48, 80, 0.06);
|
||||||
|
--border: rgba(35, 48, 80, 0.12);
|
||||||
|
--border-strong: rgba(35, 48, 80, 0.24);
|
||||||
|
--input-bg: #ffffff;
|
||||||
|
|
||||||
|
--text: #252b3b;
|
||||||
|
--text-dim: #5a6376;
|
||||||
|
--text-faint: #9aa2b5;
|
||||||
|
|
||||||
|
--primary: #5566ee;
|
||||||
|
--primary-deep: #3f50d8;
|
||||||
|
--primary-soft: rgba(85, 102, 238, 0.1);
|
||||||
|
--accent: #d9534e;
|
||||||
|
--accent-soft: rgba(217, 83, 78, 0.1);
|
||||||
|
--gold: #c98f1f;
|
||||||
|
--gold-deep: #a87614;
|
||||||
|
--gold-soft: rgba(201, 143, 31, 0.12);
|
||||||
|
--night: #4a63d2;
|
||||||
|
--night-soft: rgba(74, 99, 210, 0.1);
|
||||||
|
--green: #27995f;
|
||||||
|
--green-soft: rgba(39, 153, 95, 0.12);
|
||||||
|
--purple: #7d4fd0;
|
||||||
|
--purple-soft: rgba(125, 79, 208, 0.1);
|
||||||
|
|
||||||
|
--overlay: rgba(240, 242, 248, 0.75);
|
||||||
|
--dialog-bg: linear-gradient(170deg, #ffffff, #f2f4fa);
|
||||||
|
--shadow: 0 12px 36px rgba(35, 48, 80, 0.12);
|
||||||
|
--toast-bg: rgba(255, 255, 255, 0.96);
|
||||||
|
--logo-grad: linear-gradient(135deg, #4353d8, #8a5fd6);
|
||||||
|
--avatar-grad: linear-gradient(160deg, #ffffff, #eef1f8);
|
||||||
|
--glow-gold: rgba(201, 143, 31, 0.35);
|
||||||
|
--glow-night: rgba(74, 99, 210, 0.3);
|
||||||
|
--glow-accent: rgba(217, 83, 78, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='dark'] {
|
||||||
|
color-scheme: dark;
|
||||||
|
--bg-glow-a: rgba(99, 116, 240, 0.1);
|
||||||
|
--bg-glow-b: rgba(212, 169, 78, 0.06);
|
||||||
|
|
||||||
|
--panel: rgba(255, 255, 255, 0.045);
|
||||||
|
--panel-strong: rgba(255, 255, 255, 0.08);
|
||||||
|
--hover: rgba(255, 255, 255, 0.09);
|
||||||
|
--border: rgba(255, 255, 255, 0.09);
|
||||||
|
--border-strong: rgba(255, 255, 255, 0.18);
|
||||||
|
--input-bg: rgba(0, 0, 0, 0.3);
|
||||||
|
|
||||||
|
--text: #e7eaf3;
|
||||||
|
--text-dim: #9aa2b8;
|
||||||
|
--text-faint: #6d7488;
|
||||||
|
|
||||||
|
--primary: #6b7cf5;
|
||||||
|
--primary-deep: #4c5de0;
|
||||||
|
--primary-soft: rgba(107, 124, 245, 0.14);
|
||||||
|
--accent: #e0615c;
|
||||||
|
--accent-soft: rgba(224, 97, 92, 0.16);
|
||||||
|
--gold: #d4a94e;
|
||||||
|
--gold-deep: #b8873a;
|
||||||
|
--gold-soft: rgba(212, 169, 78, 0.14);
|
||||||
|
--night: #6f88e8;
|
||||||
|
--night-soft: rgba(111, 136, 232, 0.14);
|
||||||
|
--green: #4fc07a;
|
||||||
|
--green-soft: rgba(79, 192, 122, 0.14);
|
||||||
|
--purple: #a678e0;
|
||||||
|
--purple-soft: rgba(166, 120, 224, 0.14);
|
||||||
|
|
||||||
|
--overlay: rgba(10, 12, 20, 0.78);
|
||||||
|
--dialog-bg: linear-gradient(170deg, #232839, #171b28);
|
||||||
|
--shadow: 0 10px 34px rgba(0, 0, 0, 0.4);
|
||||||
|
--toast-bg: rgba(30, 34, 48, 0.95);
|
||||||
|
--logo-grad: linear-gradient(135deg, #aebbe8, #d4a94e);
|
||||||
|
--avatar-grad: linear-gradient(160deg, rgba(255, 255, 255, 0.14), rgba(255, 255, 255, 0.04));
|
||||||
|
--glow-gold: rgba(212, 169, 78, 0.45);
|
||||||
|
--glow-night: rgba(111, 136, 232, 0.4);
|
||||||
|
--glow-accent: rgba(224, 97, 92, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= 基础 ================= */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: $font;
|
||||||
|
color: $text;
|
||||||
|
background:
|
||||||
|
radial-gradient(1100px 600px at 80% -10%, $bg-glow-a, transparent 60%),
|
||||||
|
radial-gradient(900px 520px at 10% 110%, $bg-glow-b, transparent 55%),
|
||||||
|
linear-gradient(160deg, var(--app-bg-a, #f7f8fc), var(--app-bg-b, #eef1f7) 70%);
|
||||||
|
background-attachment: fixed;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='dark'] body {
|
||||||
|
background:
|
||||||
|
radial-gradient(1100px 600px at 80% -10%, $bg-glow-a, transparent 60%),
|
||||||
|
radial-gradient(900px 520px at 10% 110%, $bg-glow-b, transparent 55%),
|
||||||
|
linear-gradient(160deg, #1a1e2b, #12141d 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid $border-strong;
|
||||||
|
background: $panel-strong;
|
||||||
|
color: $text;
|
||||||
|
border-radius: $radius-sm;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: $hover;
|
||||||
|
border-color: var(--border-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
background: linear-gradient(135deg, $primary, $primary-deep);
|
||||||
|
border-color: transparent;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
filter: brightness(1.08);
|
||||||
|
background: linear-gradient(135deg, $primary, $primary-deep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.gold {
|
||||||
|
background: linear-gradient(135deg, $gold, $gold-deep);
|
||||||
|
border-color: transparent;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
filter: brightness(1.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ghost {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
font-family: inherit;
|
||||||
|
background: $input-bg;
|
||||||
|
border: 1px solid $border;
|
||||||
|
color: $text;
|
||||||
|
border-radius: $radius-sm;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: $text-faint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
select option {
|
||||||
|
color: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--border-strong);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
background: $panel;
|
||||||
|
border: 1px solid $border;
|
||||||
|
border-radius: $radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= 主题切换按钮 ================= */
|
||||||
|
.theme-toggle {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 17px;
|
||||||
|
line-height: 1;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
display: inline-block;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border: 2px solid var(--border-strong);
|
||||||
|
border-top-color: $text;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
vertical-align: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-ring {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 $glow-accent;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 12px rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-up {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(8px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
// 与后端快照协议对齐的类型定义
|
||||||
|
export type Phase =
|
||||||
|
| 'lobby'
|
||||||
|
| 'role_reveal'
|
||||||
|
| 'night'
|
||||||
|
| 'dawn'
|
||||||
|
| 'sheriff_elect'
|
||||||
|
| 'speech'
|
||||||
|
| 'vote'
|
||||||
|
| 'game_over'
|
||||||
|
|
||||||
|
export interface Settings {
|
||||||
|
board: string
|
||||||
|
board_name: string
|
||||||
|
win_rule: 'bian' | 'cheng'
|
||||||
|
night_seconds: number
|
||||||
|
speech_seconds: number
|
||||||
|
vote_seconds: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerView {
|
||||||
|
seat: number
|
||||||
|
name: string | null
|
||||||
|
empty: boolean
|
||||||
|
alive: boolean
|
||||||
|
is_ai: boolean
|
||||||
|
connected: boolean
|
||||||
|
speaking: boolean
|
||||||
|
sheriff: boolean
|
||||||
|
candidate: boolean
|
||||||
|
role_key: string | null
|
||||||
|
role_name: string | null
|
||||||
|
is_me: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MeView {
|
||||||
|
seat: number
|
||||||
|
name: string
|
||||||
|
alive: boolean
|
||||||
|
is_host: boolean
|
||||||
|
is_ai: boolean
|
||||||
|
role_key?: string
|
||||||
|
role_name?: string
|
||||||
|
role_camp?: string
|
||||||
|
role_emoji?: string
|
||||||
|
role_desc?: string
|
||||||
|
teammates?: number[]
|
||||||
|
seer_results?: { seat: number; result: string }[]
|
||||||
|
potions?: { antidote: boolean; poison: boolean }
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PendingView {
|
||||||
|
key: string
|
||||||
|
options: number[]
|
||||||
|
text: string
|
||||||
|
allow_skip: boolean
|
||||||
|
input_text: boolean
|
||||||
|
purpose: string
|
||||||
|
seconds_left: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LogEntry {
|
||||||
|
text: string
|
||||||
|
kind: 'public' | 'private'
|
||||||
|
tag: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VotePair {
|
||||||
|
voter: number
|
||||||
|
target: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Snapshot {
|
||||||
|
room_code: string
|
||||||
|
phase: Phase
|
||||||
|
day: number
|
||||||
|
phase_text: string
|
||||||
|
seat_count: number
|
||||||
|
host_seat: number | null
|
||||||
|
spectator: boolean
|
||||||
|
spectator_note: string
|
||||||
|
settings: Settings
|
||||||
|
players: PlayerView[]
|
||||||
|
me: MeView | null
|
||||||
|
token: string
|
||||||
|
log: LogEntry[]
|
||||||
|
pending: PendingView | null
|
||||||
|
sheriff_seat: number | null
|
||||||
|
candidates: number[]
|
||||||
|
current_speaker: number | null
|
||||||
|
last_votes: VotePair[]
|
||||||
|
winner: 'wolf' | 'good' | null
|
||||||
|
winner_text: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BoardInfo {
|
||||||
|
key: string
|
||||||
|
name: string
|
||||||
|
seat_count: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ROLE_EMOJI: Record<string, string> = {
|
||||||
|
werewolf: '🐺',
|
||||||
|
villager: '👨🌾',
|
||||||
|
seer: '🔮',
|
||||||
|
witch: '🧪',
|
||||||
|
hunter: '🏹',
|
||||||
|
guard: '🛡️',
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// 开发模式:把 /api 与 /ws 代理到 FastAPI 后端
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
css: {
|
||||||
|
preprocessorOptions: {
|
||||||
|
scss: { api: 'modern-compiler' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
proxy: {
|
||||||
|
'/api': { target: 'http://127.0.0.1:8000', changeOrigin: true },
|
||||||
|
'/ws': { target: 'ws://127.0.0.1:8000', ws: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in new issue
Block a user