将 zTools2 托管的页面/静态/探针/WebSocket 路由全部从顶级路径迁移到 /api/ 下:
- 页面:/pdf -> /api/pdf、/pdf-admin -> /api/pdf-admin、/upload -> /api/upload、
/files -> /api/files-page、/wb/{id} -> /api/wb-page/{id}、/wb-admin -> /api/wb-admin
(页面类加 -page 后缀以规避同名 REST API /api/files、/api/wb/{id})
- 静态资源:/static -> /api/static
- 探针:/health -> /api/health
- WebSocket:/ws/wb/{id} -> /api/ws/wb/{id}
- 前端 HTML 壳与 JS 中的资源/页间链接/WS URL 同步更新
- 手动测试脚本 BASE_WS 同步
这样反代与 vite proxy 只需一条 /api/ 规则即可转发全部入口;
前端 iframe 用同源相对路径 /api/pdf,与环境无关,不再误打到其它环境域名。
README 新增「路由约定」说明。
93 lines
3.5 KiB
Python
93 lines
3.5 KiB
Python
"""白板 WebSocket 端到端烟测(文本记事本):两客户端实时同步 + 心跳 + 清空。
|
||
|
||
验证:
|
||
1. 客户端 A 连入 -> 收到 init(含 content/version)
|
||
2. 客户端 B 连入 -> 收到 init
|
||
3. A 编辑文本 -> B 收到 update(A 不收自己的)
|
||
4. 心跳 ping -> pong
|
||
5. A 清空 -> A、B 都收到 cleared
|
||
6. 持久化:重连后 init 应返回清空后的内容
|
||
7. edit_count 累计
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import json
|
||
import urllib.request
|
||
|
||
import websockets
|
||
|
||
BASE_WS = "ws://127.0.0.1:6867/api/ws/wb"
|
||
BOARD = "e2etest"
|
||
|
||
|
||
async def recv_msg(ws, timeout=2.0) -> dict | None:
|
||
try:
|
||
raw = await asyncio.wait_for(ws.recv(), timeout=timeout)
|
||
return json.loads(raw)
|
||
except asyncio.TimeoutError:
|
||
return None
|
||
|
||
|
||
async def main() -> None:
|
||
async with websockets.connect(f"{BASE_WS}/{BOARD}") as a, \
|
||
websockets.connect(f"{BASE_WS}/{BOARD}") as b:
|
||
await a.send(json.dumps({"type": "hello", "client_id": "A"}))
|
||
await b.send(json.dumps({"type": "hello", "client_id": "B"}))
|
||
|
||
init_a = await recv_msg(a)
|
||
init_b = await recv_msg(b)
|
||
print("A init:", init_a.get("type") if init_a else None)
|
||
print("B init:", init_b.get("type") if init_b else None)
|
||
assert init_a and init_a["type"] == "init"
|
||
assert init_b and init_b["type"] == "init"
|
||
|
||
# A 编辑文本
|
||
text = "Hello, this is a shared note.\nSecond line."
|
||
await a.send(json.dumps({"type": "edit", "content": text}))
|
||
# A 不应收到自己的 update
|
||
echo = await recv_msg(a, timeout=1.0)
|
||
print("A self-echo (expect None):", echo)
|
||
assert echo is None, "发送者不应收到自己的 update"
|
||
# B 应收到 update
|
||
got = await recv_msg(b)
|
||
print("B recv:", got.get("type") if got else None, "content=", repr(got.get("content")) if got else None)
|
||
assert got and got["type"] == "update" and got["client_id"] == "A"
|
||
assert got["content"] == text
|
||
|
||
# 心跳
|
||
await a.send(json.dumps({"type": "ping"}))
|
||
pong = await recv_msg(a)
|
||
print("A pong:", pong.get("type") if pong else None)
|
||
assert pong and pong["type"] == "pong"
|
||
|
||
# 清空 -> 两端都收 cleared
|
||
await b.send(json.dumps({"type": "clear"}))
|
||
cleared_b = await recv_msg(b)
|
||
cleared_a = await recv_msg(a)
|
||
print("B cleared:", cleared_b.get("type") if cleared_b else None,
|
||
"A cleared:", cleared_a.get("type") if cleared_a else None)
|
||
assert cleared_b and cleared_b["type"] == "cleared" and cleared_b["client_id"] == "B"
|
||
assert cleared_a and cleared_a["type"] == "cleared" and cleared_a["client_id"] == "B"
|
||
|
||
# 验证持久化:重连后 init 应 content 为空(已清空)
|
||
async with websockets.connect(f"{BASE_WS}/{BOARD}") as c:
|
||
await c.send(json.dumps({"type": "hello", "client_id": "C"}))
|
||
init_c = await recv_msg(c)
|
||
print("C init after clear, content=", repr(init_c.get("content")) if init_c else None)
|
||
assert init_c and init_c["type"] == "init"
|
||
assert init_c["content"] == "", "清空后重连应得到空 content"
|
||
|
||
# 验证 edit_count 累计(1 次编辑 + 1 次清空 = 2)
|
||
with urllib.request.urlopen(f"http://127.0.0.1:6867/api/wb/{BOARD}") as r:
|
||
meta = json.load(r)
|
||
print("edit_count after ops:", meta["edit_count"])
|
||
assert meta["edit_count"] == 2, "1 编辑 + 1 清空 = 2 次"
|
||
|
||
print("\nWS 端到端全部通过 ✅")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
asyncio.run(main())
|