chore: 工程化改造前的基线快照
在引入 uv 依赖管理、ruff 静态检查与格式化、pyright 类型检查之前, 先把当前可正常运行的状态固定下来,作为后续大规模自动改动的回滚点。 此时状态:97 项测试全绿,HTTP + WebSocket 多牧场服务器可用。
This commit is contained in:
commit
a7725f8812
35 files changed
+11871
No files matched your search
@@ -0,0 +1,455 @@
|
||||
"""内嵌的网页客户端。
|
||||
|
||||
为什么要内嵌而不是写一堆静态文件
|
||||
--------------------------------
|
||||
服务端只需要一个 ``/`` 路由就能给出完整的可视化客户端,部署时不用管理
|
||||
静态目录、不用配 CORS,`pip install` 完就能玩。页面本身也是 WebSocket
|
||||
协议的一份"活文档":想接自己的客户端时,照着这里的 JS 抄就行。
|
||||
|
||||
注意:这个字符串里既有 CSS 又有 JS,花括号很多,所以**不要**用 ``f-string``
|
||||
渲染它(为此把需要拼接的部分放在 :func:`dashboard_text` 里)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
DASHBOARD_HTML = r"""<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>SimpleFarm 服务器</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f4f7f2; --card: #ffffff; --line: #dde5d8; --ink: #23301f;
|
||||
--dim: #6b7a66; --green: #3f8f4f; --red: #c0392b; --gold: #b8860b;
|
||||
--blue: #2f6fb0; --cyan: #2b8a8a;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; background: var(--bg); color: var(--ink);
|
||||
font: 14px/1.6 "Segoe UI", "Microsoft YaHei", system-ui, sans-serif;
|
||||
}
|
||||
header {
|
||||
display: flex; flex-wrap: wrap; gap: 12px; align-items: center;
|
||||
padding: 14px 20px; background: var(--card); border-bottom: 1px solid var(--line);
|
||||
position: sticky; top: 0; z-index: 5;
|
||||
}
|
||||
header h1 { font-size: 18px; margin: 0; }
|
||||
header h1 span { font-size: 12px; color: var(--dim); font-weight: normal; margin-left: 6px; }
|
||||
.spacer { flex: 1; }
|
||||
input, select, button {
|
||||
font: inherit; padding: 6px 10px; border: 1px solid var(--line);
|
||||
border-radius: 8px; background: #fff; color: var(--ink);
|
||||
}
|
||||
button { cursor: pointer; transition: .15s; }
|
||||
button:hover { border-color: var(--green); color: var(--green); }
|
||||
button.primary { background: var(--green); border-color: var(--green); color: #fff; }
|
||||
button.primary:hover { background: #357a43; color: #fff; }
|
||||
button:disabled { opacity: .45; cursor: not-allowed; }
|
||||
.dot { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--dim); }
|
||||
.dot i { width: 9px; height: 9px; border-radius: 50%; background: #c9d2c4; display: block; }
|
||||
.dot.on i { background: var(--green); box-shadow: 0 0 0 3px rgba(63,143,79,.18); }
|
||||
.dot.bad i { background: var(--red); }
|
||||
main { padding: 16px 20px 40px; max-width: 1180px; margin: 0 auto; }
|
||||
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(118px, 1fr)); gap: 10px; }
|
||||
.stat { background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 10px 12px; }
|
||||
.stat b { display: block; font-size: 18px; }
|
||||
.stat small { color: var(--dim); }
|
||||
.cols { display: grid; grid-template-columns: 1.35fr 1fr; gap: 14px; margin-top: 14px; }
|
||||
@media (max-width: 900px) { .cols { grid-template-columns: 1fr; } }
|
||||
.card { background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 12px 14px; margin-bottom: 14px; }
|
||||
.card h2 { font-size: 14px; margin: 0 0 10px; display: flex; align-items: center; gap: 8px; }
|
||||
.card h2 em { font-style: normal; color: var(--dim); font-size: 12px; font-weight: normal; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(152px, 1fr)); gap: 10px; }
|
||||
.plot { border: 1px solid var(--line); border-radius: 10px; padding: 9px 10px; background: #fbfdfa; }
|
||||
.plot.ripe { border-color: var(--green); background: #f1faf2; }
|
||||
.plot.dry { border-color: var(--red); background: #fdf3f2; }
|
||||
.plot .top { display: flex; justify-content: space-between; font-weight: 600; }
|
||||
.plot .meta { color: var(--dim); font-size: 12px; }
|
||||
.bar { height: 6px; border-radius: 4px; background: #e7ede4; overflow: hidden; margin: 5px 0; }
|
||||
.bar i { display: block; height: 100%; background: var(--green); }
|
||||
.water i { background: var(--blue); }
|
||||
.plot .btns { display: flex; gap: 6px; margin-top: 7px; }
|
||||
.plot .btns button { flex: 1; padding: 3px 0; font-size: 12px; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||
th, td { text-align: left; padding: 4px 6px; border-bottom: 1px solid var(--line); }
|
||||
th { color: var(--dim); font-weight: normal; }
|
||||
td.num { text-align: right; font-variant-numeric: tabular-nums; }
|
||||
.up { color: var(--red); } .down { color: var(--green); } .flat { color: var(--dim); }
|
||||
.log { max-height: 190px; overflow: auto; font-size: 13px; }
|
||||
.log div { padding: 1px 0; }
|
||||
.log .d { color: var(--dim); margin-right: 6px; font-variant-numeric: tabular-nums; }
|
||||
.k-good { color: var(--green); } .k-bad { color: var(--red); }
|
||||
.k-warn { color: var(--gold); } .k-action { color: var(--cyan); }
|
||||
.notice { font-size: 12px; color: var(--dim); min-height: 18px; }
|
||||
.toast {
|
||||
position: fixed; left: 50%; bottom: 26px; transform: translateX(-50%);
|
||||
background: #23301f; color: #fff; padding: 9px 18px; border-radius: 20px;
|
||||
font-size: 13px; opacity: 0; transition: .25s; pointer-events: none; z-index: 20;
|
||||
}
|
||||
.toast.show { opacity: .93; }
|
||||
.row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
|
||||
.animals li { display: flex; align-items: center; gap: 8px; padding: 4px 0; border-bottom: 1px dashed var(--line); list-style: none; }
|
||||
ul { padding: 0; margin: 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>SimpleFarm <span>小型农场游戏服务器</span></h1>
|
||||
<div class="dot" id="status"><i></i><span id="statustext">未连接</span></div>
|
||||
<div class="spacer"></div>
|
||||
<input id="farm" size="10" placeholder="牧场名" />
|
||||
<input id="token" size="10" placeholder="令牌(可选)" />
|
||||
<button class="primary" id="join">进入牧场</button>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="stats" id="stats"></div>
|
||||
|
||||
<div class="cols">
|
||||
<div>
|
||||
<div class="card">
|
||||
<h2>农田 <em id="farmhint"></em></h2>
|
||||
<div class="grid" id="plots"></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>牧场 <em id="animalhint"></em></h2>
|
||||
<ul class="animals" id="animals"></ul>
|
||||
<div class="row" style="margin-top:8px">
|
||||
<select id="buyanimal">
|
||||
<option value="chicken">鸡 90</option>
|
||||
<option value="sheep">羊 220</option>
|
||||
<option value="cow">牛 480</option>
|
||||
</select>
|
||||
<button data-act="buy_animal">买牲畜</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="card">
|
||||
<h2>快捷动作</h2>
|
||||
<div class="row">
|
||||
<button class="primary" data-act="harvest_all">一键收获</button>
|
||||
<button data-act="buy_plot">买地</button>
|
||||
<button data-act="buy_feed">买饲料x10</button>
|
||||
<button data-act="sleep">休息一天</button>
|
||||
<button data-act="pause">暂停/继续</button>
|
||||
<button data-act="save">存档</button>
|
||||
<button data-act="reset">重置</button>
|
||||
</div>
|
||||
<div class="notice" id="notice"></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>市场行情 <em>红涨绿跌</em></h2>
|
||||
<table id="market"></table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>服务器排行榜 <em id="worldinfo"></em></h2>
|
||||
<table id="board"></table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>农事日志</h2>
|
||||
<div class="log" id="log"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script>
|
||||
const $ = (id) => document.getElementById(id);
|
||||
let ws = null, state = null, catalog = null, pollTimer = null, reconnectTimer = null, pending = 0;
|
||||
|
||||
const cropNames = {
|
||||
wheat: "小麦", carrot: "胡萝卜", tomato: "番茄",
|
||||
corn: "玉米", pumpkin: "南瓜", strawberry: "草莓"
|
||||
};
|
||||
|
||||
function toast(text) {
|
||||
const el = $("toast");
|
||||
el.textContent = text;
|
||||
el.classList.add("show");
|
||||
clearTimeout(el._t);
|
||||
el._t = setTimeout(() => el.classList.remove("show"), 2200);
|
||||
}
|
||||
|
||||
function setStatus(text, cls) {
|
||||
$("statustext").textContent = text;
|
||||
$("status").className = "dot " + (cls || "");
|
||||
}
|
||||
|
||||
function farmName() { return ($("farm").value || "default").trim() || "default"; }
|
||||
function token() { return ($("token").value || "").trim(); }
|
||||
|
||||
function headers() {
|
||||
const h = { "Content-Type": "application/json", "X-Farm-Id": farmName() };
|
||||
if (token()) h["X-API-Token"] = token();
|
||||
return h;
|
||||
}
|
||||
|
||||
async function api(path, options) {
|
||||
const res = await fetch(path, options);
|
||||
let data = null;
|
||||
try { data = await res.json(); } catch (e) { data = { ok: false, message: "响应不是 JSON" }; }
|
||||
if (!res.ok || data.ok === false) {
|
||||
toast(data.message || ("请求失败 " + res.status));
|
||||
return null;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
async function act(action, params) {
|
||||
const data = await api("/api/actions" + (token() ? "?token=" + encodeURIComponent(token()) : ""), {
|
||||
method: "POST", headers: headers(),
|
||||
body: JSON.stringify({ action: action, params: params || {} })
|
||||
});
|
||||
if (data) {
|
||||
toast(data.message);
|
||||
if (data.state) refreshFromBrief(data.state);
|
||||
}
|
||||
}
|
||||
|
||||
/* 动作响应里只有精简状态,这里把它合并进完整状态,避免多查一次 */
|
||||
function refreshFromBrief(brief) {
|
||||
if (!state) { syncNow(); return; }
|
||||
Object.assign(state, brief);
|
||||
render();
|
||||
}
|
||||
|
||||
function syncNow() {
|
||||
if (ws && ws.readyState === 1) {
|
||||
ws.send(JSON.stringify({ type: "sync" }));
|
||||
return;
|
||||
}
|
||||
api("/api/state?farm=" + encodeURIComponent(farmName()) +
|
||||
(token() ? "&token=" + encodeURIComponent(token()) : ""), { headers: headers() })
|
||||
.then((d) => { if (d) { state = d.state; render(); } });
|
||||
}
|
||||
|
||||
function connect() {
|
||||
if (ws) { try { ws.close(); } catch (e) {} }
|
||||
clearTimeout(reconnectTimer);
|
||||
const proto = location.protocol === "https:" ? "wss:" : "ws:";
|
||||
const url = proto + "//" + location.host + "/ws?farm=" + encodeURIComponent(farmName()) +
|
||||
(token() ? "&token=" + encodeURIComponent(token()) : "") + "&name=web";
|
||||
setStatus("连接中…", "");
|
||||
ws = new WebSocket(url);
|
||||
ws.onopen = () => {
|
||||
setStatus("已连接(WebSocket)", "on");
|
||||
clearInterval(pollTimer); pollTimer = null;
|
||||
api("/api/farms", { headers: headers() });
|
||||
};
|
||||
ws.onmessage = (ev) => {
|
||||
let msg = null;
|
||||
try { msg = JSON.parse(ev.data); } catch (e) { return; }
|
||||
handle(msg);
|
||||
};
|
||||
ws.onclose = (ev) => {
|
||||
if (ev.code === 4401) { setStatus("令牌不正确", "bad"); }
|
||||
else setStatus("已断开,3 秒后重连", "bad");
|
||||
if (!pollTimer) pollTimer = setInterval(syncNow, 1500); // 降级为轮询
|
||||
reconnectTimer = setTimeout(connect, 3000);
|
||||
};
|
||||
ws.onerror = () => setStatus("连接异常", "bad");
|
||||
}
|
||||
|
||||
function handle(msg) {
|
||||
if (msg.type === "welcome") {
|
||||
state = msg.state; catalog = msg.catalog;
|
||||
$("farmhint").textContent = "共 " + state.plot_count + "/" + state.max_plots + " 块";
|
||||
render();
|
||||
} else if (msg.type === "state" || msg.type === "sync") {
|
||||
state = msg.state; if (msg.catalog) catalog = msg.catalog;
|
||||
renderLog(msg.log);
|
||||
render();
|
||||
} else if (msg.type === "result") {
|
||||
toast(msg.message);
|
||||
if (msg.snapshot) { state = msg.snapshot; render(); }
|
||||
else if (msg.state) { refreshFromBrief(msg.state); }
|
||||
renderLog(msg.log);
|
||||
} else if (msg.type === "day") {
|
||||
renderLog(null);
|
||||
} else if (msg.type === "notice") {
|
||||
$("notice").textContent = msg.text;
|
||||
} else if (msg.type === "error") {
|
||||
toast(msg.message || "服务器返回错误");
|
||||
}
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (!state) return;
|
||||
const s = state;
|
||||
$("stats").innerHTML = [
|
||||
["第几天", s.day], ["金币", s.gold], ["等级", "Lv." + s.level + " (" + s.exp + "/" + s.exp_needed + ")"],
|
||||
["行动点", s.ap + "/" + s.max_ap], ["饲料", s.feed], ["净资产", Math.round(s.net_worth)],
|
||||
["在线", s.online !== undefined ? s.online : "—"],
|
||||
["下一天", s.seconds_to_tick === null ? "已暂停" : s.seconds_to_tick + "s"]
|
||||
].map((p) => '<div class="stat"><small>' + p[0] + '</small><b>' + p[1] + '</b></div>').join("");
|
||||
|
||||
/* 农田 */
|
||||
$("plots").innerHTML = s.plots.map((p) => {
|
||||
const ratio = p.grow_days ? Math.round(p.growth / p.grow_days * 100) : 0;
|
||||
const water = Math.round(p.moisture / (p.water_capacity || 3) * 100);
|
||||
const cls = p.is_ripe ? "ripe" : (p.crop && p.moisture === 0 ? "dry" : "");
|
||||
const title = p.is_empty ? (p.position + " 号地 · 空地")
|
||||
: (p.position + " 号地 · " + (cropNames[p.crop] || p.crop) + " · " + p.stage);
|
||||
const meta = p.is_empty ? "空闲" : (p.is_ripe ? "已成熟,可收获" : "还需 " + p.days_left + " 天");
|
||||
let btns = "";
|
||||
if (p.is_ripe) btns = '<button data-h="' + p.position + '">收获</button>';
|
||||
else if (p.is_empty) btns = '<button data-p="' + p.position + '">播种</button>';
|
||||
else if (p.moisture < p.water_capacity) btns = '<button data-w="' + p.position + '">浇水</button>';
|
||||
return '<div class="plot ' + cls + '"><div class="top"><span>' + title +
|
||||
'</span></div><div class="meta">' + meta + '</div>' +
|
||||
'<div class="bar"><i style="width:' + ratio + '%"></i></div>' +
|
||||
'<div class="bar water"><i style="width:' + water + '%"></i></div>' +
|
||||
'<div class="btns">' + btns + '</div></div>';
|
||||
}).join("");
|
||||
|
||||
/* 牧场 */
|
||||
$("animalhint").textContent = s.animal_count + "/" + s.max_animals + " 只";
|
||||
$("animals").innerHTML = s.animals.length ? s.animals.map((a) =>
|
||||
'<li><b>#' + a.id + ' ' + a.name + '</b><span class="meta">' + (
|
||||
a.pending > 0 ? '<span class="up">' + a.product + ' x' + a.pending + ' 待收</span>'
|
||||
: a.is_sick ? '<span class="down">虚弱,急需喂食</span>'
|
||||
: a.fed ? '已喂食' : '<span class="k-warn">今天还没喂</span>') +
|
||||
'</span><span class="spacer"></span>' +
|
||||
(a.fed ? '' : '<button data-f="' + a.id + '">喂食</button>') +
|
||||
(a.pending > 0 ? '<button data-c="' + a.id + '">收取</button>' : '') + '</li>'
|
||||
).join("") : '<li style="color:#6b7a66">牧场空空的,先买一只吧</li>';
|
||||
|
||||
/* 市场 */
|
||||
const rows = Object.entries(s.market).filter((kv) => kv[0].indexOf("animal:") !== 0);
|
||||
const base = { wheat: 14, carrot: 26, tomato: 52, corn: 95, pumpkin: 180, strawberry: 320 };
|
||||
$("market").innerHTML = '<tr><th>作物</th><th class="num">现价</th><th class="num">基准</th></tr>' +
|
||||
rows.sort((a, b) => b[1] - a[1]).map((kv) => {
|
||||
const b = base[kv[0]] || kv[1];
|
||||
const cls = kv[1] > b * 1.03 ? "up" : (kv[1] < b * 0.97 ? "down" : "flat");
|
||||
const arrow = cls === "up" ? "↑" : (cls === "down" ? "↓" : "→");
|
||||
return '<tr><td>' + (cropNames[kv[0]] || kv[0]) + '</td><td class="num ' + cls + '">' +
|
||||
kv[1].toFixed(0) + " " + arrow + '</td><td class="num">' + b + '</td></tr>';
|
||||
}).join("");
|
||||
|
||||
renderLog(null);
|
||||
renderBoard();
|
||||
}
|
||||
|
||||
function renderLog(entries) {
|
||||
if (entries) entries.forEach((e) => logAppend(e));
|
||||
else if (!logAppend._cache) {
|
||||
api("/api/log?limit=12&farm=" + encodeURIComponent(farmName()), { headers: headers() })
|
||||
.then((d) => { if (d) d.entries.forEach(logAppend); });
|
||||
}
|
||||
}
|
||||
|
||||
function logAppend(e) {
|
||||
if (logAppend._last && e.seq <= logAppend._last) return;
|
||||
logAppend._last = e.seq;
|
||||
logAppend._cache = true;
|
||||
const box = $("log");
|
||||
const div = document.createElement("div");
|
||||
div.className = "k-" + e.kind;
|
||||
div.innerHTML = '<span class="d">D' + e.day + '</span>' + e.text;
|
||||
box.insertBefore(div, box.firstChild);
|
||||
while (box.childNodes.length > 60) box.removeChild(box.lastChild);
|
||||
}
|
||||
|
||||
async function renderBoard() {
|
||||
const d = await api("/api/world", { headers: headers() });
|
||||
if (!d) return;
|
||||
$("worldinfo").textContent = d.farms + " 个牧场 · " + d.clients + " 个连接 · 运行 " +
|
||||
Math.round(d.uptime) + " 秒";
|
||||
$("board").innerHTML = '<tr><th>#</th><th>牧场</th><th class="num">净资产</th>' +
|
||||
'<th class="num">天</th><th class="num">在线</th></tr>' +
|
||||
d.leaderboard.slice(0, 10).map((r) =>
|
||||
'<tr><td>' + r.rank + '</td><td>' + r.farm + '</td><td class="num">' +
|
||||
Math.round(r.net_worth) + '</td><td class="num">' + r.day + '</td><td class="num">' +
|
||||
r.online + '</td></tr>').join("");
|
||||
}
|
||||
|
||||
/* ---- 事件委托:地块按钮 / 牲畜按钮 / 快捷动作 ---- */
|
||||
document.addEventListener("click", (ev) => {
|
||||
const t = ev.target;
|
||||
if (t.tagName !== "BUTTON") return;
|
||||
if (t.dataset.w) act("water", { plot: +t.dataset.w });
|
||||
else if (t.dataset.h) act("harvest", { plot: +t.dataset.h });
|
||||
else if (t.dataset.p) {
|
||||
const plot = +t.dataset.p;
|
||||
const pool = state ? Object.keys(cropNames).filter((k) => {
|
||||
const c = (catalog || { crops: [] }).crops.find((x) => x.key === k);
|
||||
return !c || !c.locked;
|
||||
}) : [];
|
||||
const pick = window.prompt("要种什么?输入编号:\n" +
|
||||
pool.map((k, i) => (i + 1) + ". " + cropNames[k]).join("\n"), "1");
|
||||
const idx = parseInt(pick, 10);
|
||||
if (!idx || idx < 1 || idx > pool.length) return;
|
||||
act("plant", { plot: plot, crop: pool[idx - 1] });
|
||||
}
|
||||
else if (t.dataset.f) act("feed", { animal_id: +t.dataset.f });
|
||||
else if (t.dataset.c) act("collect", { animal_id: +t.dataset.c });
|
||||
else if (t.dataset.act === "buy_animal") act("buy_animal", { kind: $("buyanimal").value });
|
||||
else if (t.dataset.act === "buy_feed") act("buy_feed", { quantity: 10 });
|
||||
else if (t.dataset.act === "reset") { if (confirm("确定重置牧场?所有进度都会清空")) act("reset"); }
|
||||
else if (t.dataset.act) act(t.dataset.act);
|
||||
});
|
||||
|
||||
$("join").addEventListener("click", () => {
|
||||
localStorage.setItem("sf.farm", farmName());
|
||||
localStorage.setItem("sf.token", token());
|
||||
fetch("/api/farms", {
|
||||
method: "POST", headers: headers(),
|
||||
body: JSON.stringify({ farm: farmName() })
|
||||
}).then((r) => r.json()).then((d) => { toast(d.message || ""); });
|
||||
connect();
|
||||
});
|
||||
document.addEventListener("keydown", (e) => { if (e.key === "Enter") $("join").click(); });
|
||||
|
||||
/* ---- 启动 ---- */
|
||||
$("farm").value = localStorage.getItem("sf.farm") || "default";
|
||||
$("token").value = localStorage.getItem("sf.token") || "";
|
||||
connect();
|
||||
setInterval(renderBoard, 5000);
|
||||
setInterval(() => { if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: "ping" })); }, 20000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
def dashboard_text(host: str, port: int) -> str:
|
||||
"""给终端/curl 看的纯文本索引。"""
|
||||
return (
|
||||
"SimpleFarm 游戏服务器\n"
|
||||
"======================\n"
|
||||
f"网页客户端:http://{host}:{port}/\n"
|
||||
f"接口文档 :http://{host}:{port}/docs\n"
|
||||
f"健康检查 :http://{host}:{port}/api/health\n"
|
||||
f"实时通道 :ws://{host}:{port}/ws?farm=<牧场名>\n"
|
||||
"\n"
|
||||
"给 AI / 脚本的最短路径\n"
|
||||
"----------------------\n"
|
||||
f"curl 'http://{host}:{port}/api/actions?farm=default'\n"
|
||||
f"curl -X POST 'http://{host}:{port}/api/actions?farm=default' \\\n"
|
||||
" -H 'Content-Type: application/json' \\\n"
|
||||
" -d '{\"action\":\"plant\",\"params\":{\"plot\":1,\"crop\":\"wheat\"}}'\n"
|
||||
"\n"
|
||||
"读取类\n"
|
||||
"------\n"
|
||||
"GET /api/world 世界概览 + 排行榜\n"
|
||||
"GET /api/farms 牧场列表\n"
|
||||
"GET /api/state 完整状态\n"
|
||||
"GET /api/catalog 作物/牲畜/价格\n"
|
||||
"GET /api/actions 当前合法动作(AI 推荐入口)\n"
|
||||
"GET /api/log 最近事件\n"
|
||||
"GET /api/help 规则 + 动作清单 + 参数说明\n"
|
||||
"\n"
|
||||
"写入类\n"
|
||||
"------\n"
|
||||
"POST /api/actions {\"action\":\"...\",\"params\":{...}} 通用入口\n"
|
||||
"POST /api/batch {\"actions\":[{...},{...}]} 批量\n"
|
||||
"POST /api/plant|water|harvest|harvest_all|buy_animal|feed|\n"
|
||||
" collect|buy_feed|buy_plot|sleep|pause|save|reset 便捷入口\n"
|
||||
"POST /api/farms {\"farm\":\"名字\"} 创建/进入牧场\n"
|
||||
"POST /api/admin/save 全部存档\n"
|
||||
"POST /api/admin/shutdown 优雅停机\n"
|
||||
)
|
||||
Reference in new issue
Block a user