fix: 用户可见的操作提示统一为 uv run 前缀

项目由 uv 管理,依赖装在 .venv。此前各处的提示语仍写作 `python main.py ...`,用户若在未激活虚拟环境时照抄,会因缺少 fastapi 而失败——这是正确性问题,不只是措辞。

涉及:main.py(12 行,含 prog 与各条 hint)、ai_player.py(5 行)、farm/client.py、farm/server/__main__.py(3 行,含 prog)、farm/server/__init__.py、farm/server/daemon.py、README(4 处示例与目录树注释)。

刻意保留 main.py:26 与 README:49 的「没装 uv?」兜底说明,那里 `python main.py` 是正确写法。

验收:ruff 全通过 / format 26 文件无改动 / pyright 0 错误 / 97 项测试通过。
This commit is contained in:
snowy-yang committed 2026-09-18 21:25:52 +08:00
1 parent d7a7107b0a
commit c414db4217
7 files changed
+27 -27

No files matched your search

+4 -4
View File
@@ -84,7 +84,7 @@ uv run main.py solo # 本地引擎 + 终端界面,不开
**TUI 是纯客户端**:它通过 WebSocket 订阅状态、通过 WebSocket/HTTP 提交动作,
自己不持有任何游戏状态。所以:
* 服务端可以在另一台机器上,`python main.py play --url http://10.0.0.5:8765 --farm alice`;
* 服务端可以在另一台机器上,`uv run main.py play --url http://10.0.0.5:8765 --farm alice`;
* 关掉 TUI 不会影响服务器,你的牧场继续按时间生长;
* 你在 TUI 里操作的同时,AI 可以对同一个牧场操作,双方立刻能看到对方的结果。
@@ -113,7 +113,7 @@ uv run main.py solo # 本地引擎 + 终端界面,不开
实例:
```bash
$ python main.py actions
$ uv run main.py actions
牧场 default · 第 3 天 · 金币 96 · 行动点 7/10
共 9 个候选动作:
@@ -123,7 +123,7 @@ $ python main.py actions
4 号地空闲,可种小麦
...
$ python main.py do harvest '{"plot":2}'
$ uv run main.py do harvest '{"plot":2}'
✓ 收获 2 份胡萝卜(丰收!),获得 104 金币 (0.01s)
第 3 天 · 金币 200 · 行动点 7/10 · 净资产 512.0
```
@@ -347,7 +347,7 @@ farm/
hub.py WebSocket 连接池与状态推送(线程 → 事件循环的桥)
daemon.py 守护进程:start / stop / status / 日志 / pid
webui.py 内嵌网页客户端(单页,原生 JS + WebSocket)
__main__.py python -m farm.server(前台运行)
__main__.py uv run python -m farm.server(前台运行)
tools/
preview_tui.py 把 TUI 渲染成文本(--url 可连真实服务器)
ws_demo.py 最小 WebSocket 客户端,验证协议
+5 -5
View File
@@ -6,9 +6,9 @@
用法
----
python ai_player.py # 连本机 default 牧场
python ai_player.py --farm ai-1 --minutes 5 # 经营 ai-1 五分钟
python ai_player.py --url http://10.0.0.5:8765 --farm my-bot
uv run ai_player.py # 连本机 default 牧场
uv run ai_player.py --farm ai-1 --minutes 5 # 经营 ai-1 五分钟
uv run ai_player.py --url http://10.0.0.5:8765 --farm my-bot
策略在哪里
----------
@@ -32,7 +32,7 @@ from farm.engine import GameError
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="python ai_player.py",
prog="uv run ai_player.py",
description="通过 HTTP/WebSocket 接口参与 SimpleFarm 的机器人玩家",
)
parser.add_argument("--url", default="http://127.0.0.1:8765", help="服务器地址")
@@ -59,7 +59,7 @@ def main(argv=None) -> int:
)
except GameError as exc:
print(f"✗ 连不上服务器:{exc.message}", file=sys.stderr)
print(" 先启动:python main.py serve --daemon", file=sys.stderr)
print(" 先启动:uv run main.py serve --daemon", file=sys.stderr)
return 2
snapshot = client.snapshot()
+1 -1
View File
@@ -322,7 +322,7 @@ class RemoteFarm:
self.connected = False
raise GameError(
"network",
f"连不上服务器 {self.endpoint}({exc})。请先启动:python main.py serve --daemon",
f"连不上服务器 {self.endpoint}({exc})。请先启动:uv run main.py serve --daemon",
status=503,
) from exc
try:
+1 -1
View File
@@ -13,7 +13,7 @@
* :mod:`farm.server.webui` —— 内嵌网页客户端
``app`` 依赖 FastAPI,而 ``daemon`` 只依赖标准库。为了让
``python main.py stop`` 在没有装 FastAPI 的机器上也能用,这里采用**惰性导出**:
``uv run main.py stop`` 在没有装 FastAPI 的机器上也能用,这里采用**惰性导出**:
真正 import 到用例时才加载对应子模块。
"""
+3 -3
View File
@@ -1,4 +1,4 @@
"""``python -m farm.server`` —— 服务器进程入口(前台运行)。
"""``uv run python -m farm.server`` —— 服务器进程入口(前台运行)。
守护进程模式会由 :class:`farm.server.daemon.DaemonManager` 用同样的命令行
再拉起一个本模块的实例,只是不带 ``--daemon``。
@@ -17,7 +17,7 @@ from .daemon import DEFAULT_LOG_FILE, DEFAULT_PID_FILE, PROJECT_ROOT, DaemonMana
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="python -m farm.server",
prog="uv run python -m farm.server",
description="SimpleFarm 游戏服务器:HTTP 接口给 AI,WebSocket 给实时界面。",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
@@ -50,7 +50,7 @@ def build_parser() -> argparse.ArgumentParser:
parser.add_argument("--access-log", action="store_true", help="打印每个 HTTP 请求")
parser.add_argument("--no-remote-control", action="store_true", help="禁止通过接口停机")
parser.add_argument(
"--daemon", action="store_true", help="转到后台运行(等价于 python main.py serve --daemon)"
"--daemon", action="store_true", help="转到后台运行(等价于 uv run main.py serve --daemon)"
)
parser.add_argument(
"--pid-file", default=None, help="把本进程 pid 写到该文件(守护进程模式会自动指定)"
+1 -1
View File
@@ -231,7 +231,7 @@ class DaemonManager:
f"服务器已在后台启动(pid {pid})\n"
f" 网页 http://{self.host}:{self.port}/\n"
f" 日志 {self.log_file}\n"
f" 停止 python main.py stop"
f" 停止 uv run main.py stop"
)
if process.poll() is not None and self.read_pid() is None:
return False, (
+12 -12
View File
@@ -88,7 +88,7 @@ def die(message: str, hint: str = "", code: int = 1) -> int:
# ---------------------------------------------------------------------- 参数
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="python main.py",
prog="uv run main.py",
description="SimpleFarm:终端农场游戏 + 小型多人游戏服务器(HTTP/WebSocket 供 AI 调用)",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=BANNER,
@@ -222,8 +222,8 @@ def make_client(args, realtime: bool = False):
except GameError as exc:
hint = (
f"服务器地址:{args.url}\n"
"先启动服务器:python main.py serve --daemon\n"
"再确认地址端口:python main.py status"
"先启动服务器:uv run main.py serve --daemon\n"
"再确认地址端口:uv run main.py status"
)
raise SystemExit(die(f"无法连接服务器:{exc.message}", hint)) from None
@@ -286,7 +286,7 @@ def cmd_status(args) -> int:
if not info["running"]:
print(warn("• 服务器未运行"))
print(dim(" 启动:python main.py serve --daemon"))
print(dim(" 启动:uv run main.py serve --daemon"))
print(dim(f" 地址:http://{args.host}:{args.port}/"))
return 1
@@ -336,7 +336,7 @@ def cmd_play(args) -> int:
if not (sys.stdin is not None and sys.stdin.isatty()):
return die(
"当前终端不是交互式终端,无法启动界面。",
"在真实终端里运行,或用命令行模式:python main.py actions / do / watch",
"在真实终端里运行,或用命令行模式:uv run main.py actions / do / watch",
)
client = make_client(args, realtime=not args.no_realtime)
label = f"{client.farm} @ {client.host}"
@@ -392,7 +392,7 @@ def cmd_do(args) -> int:
except json.JSONDecodeError as exc:
return die(
f"参数不是合法 JSON:{exc}",
'例子:python main.py do plant \'{"plot":1,"crop":"wheat"}\'',
'例子:uv run main.py do plant \'{"plot":1,"crop":"wheat"}\'',
)
if not isinstance(params, dict):
return die("参数必须是 JSON 对象")
@@ -403,7 +403,7 @@ def cmd_do(args) -> int:
except GameError as exc:
return die(
f"{args.action} 失败({exc.code}):{exc.message}",
"看看现在能做什么:python main.py actions",
"看看现在能做什么:uv run main.py actions",
)
print(ok("✓ ") + str(result.get("message", "完成")) + dim(f" ({time.time() - started:.2f}s)"))
@@ -432,7 +432,7 @@ def cmd_batch(args) -> int:
except json.JSONDecodeError as exc:
return die(
f"actions 不是合法 JSON:{exc}",
"例子:python main.py batch "
"例子:uv run main.py batch "
'\'[{"action":"harvest_all"},{"action":"buy_feed","params":{"quantity":5}}]\'',
)
if not isinstance(items, list):
@@ -458,7 +458,7 @@ def cmd_actions(args) -> int:
)
if not items:
print(warn("• 当前没有可执行的动作"))
print(dim(" 试试:python main.py do sleep (推进一天)"))
print(dim(" 试试:uv run main.py do sleep (推进一天)"))
return 0
print(f"共 {len(items)} 个候选动作:\n")
for index, item in enumerate(items, start=1):
@@ -467,7 +467,7 @@ def cmd_actions(args) -> int:
params_text = json.dumps(item["params"], ensure_ascii=False)
print(f" [{index:>2}] {item['action']:<12} {params_text:<42} ({cost_text})")
print(dim(f" {item.get('reason', '')}"))
print(dim("\n执行方式:python main.py do <动作名> '<参数JSON>'"))
print(dim("\n执行方式:uv run main.py do <动作名> '<参数JSON>'"))
return 0
@@ -561,7 +561,7 @@ def cmd_bot(args) -> int:
print("\n已中断。")
summary = {"actions": bot.actions_executed, "failures": bot.failures}
print(json.dumps(summary, ensure_ascii=False))
print(dim("提示:机器人和你调的是同一套接口,随时可以用 python main.py actions 接手。"))
print(dim("提示:机器人和你调的是同一套接口,随时可以用 uv run main.py actions 接手。"))
return 0
@@ -670,7 +670,7 @@ def main(argv=None) -> int:
handler = handlers.get(args.command)
if handler is None:
return die(f"未知命令 {args.command}", "python main.py --help 查看全部命令")
return die(f"未知命令 {args.command}", "uv run main.py --help 查看全部命令")
try:
return handler(args)
except SystemExit as exc: # make_client 里的友好退出