chore: 规范标点

This commit is contained in:
pluv committed 2026-10-03 14:43:43 +08:00
1 parent e54390a9f8
commit 15e79887fa
84 files changed
+5439 -5398

No files matched your search

+32 -32
View File
@@ -1,26 +1,26 @@
#!/usr/bin/env python3
"""replay.py - 把 /log 录下来的真实会话当成测试夹具回放。
"""replay.py - 把 /log 录下来的真实会话当成测试夹具回放.
为什么需要它:ligent 的协议适配一直只在自建的 mock 服务端上验证过,而 mock
是我们自己写的 —— 它接受什么,完全取决于**我们以为**服务端会接受什么。真实
服务端的差异是最大的未知数。
为什么需要它:ligent 的协议适配一直只在自建的 mock 服务端上验证过, 而 mock
是我们自己写的 —— 它接受什么, 完全取决于**我们以为**服务端会接受什么. 真实
服务端的差异是最大的未知数.
/log 导出的是**原始字节**(请求体、响应头、未经解析的响应原文,流式就是原始
SSE),正好够原样伺候回去。所以这里不重写一个"模拟客户端",而是当**离线
服务端**:把录下来的响应按顺序发给一个真的 ligent 实例,于是 http.c 的分块
读取、llm.c 的 SSE 行切分与 tool_calls 增量重组,全都在真实字节上跑。
/log 导出的是**原始字节** (请求体、响应头、未经解析的响应原文, 流式就是原始
SSE), 正好够原样伺候回去. 所以这里不重写一个"模拟客户端", 而是当**离线
服务端**:把录下来的响应按顺序发给一个真的 ligent 实例, 于是 http.c 的分块
读取、llm.c 的 SSE 行切分与 tool_calls 增量重组, 全都在真实字节上跑.
用法:
replay.py --summary FILE 这份录制里有什么
replay.py --serve PORT FILE [--strict] 当离线服务端
replay.py --check FILE 自检:能不能解析出结构完整的往返
--strict 会把每个进来的请求体与录制时那一份做**结构比对**(顶层键、消息角色
序列、工具名集合),不一致就回 400 并在 stderr 打印差异。这是这个工具最有用
的一半:它把"今天的客户端还在发同样的报文形状"变成一条会失败的断言。
--strict 会把每个进来的请求体与录制时那一份做**结构比对** (顶层键、消息角色
序列、工具名集合), 不一致就回 400 并在 stderr 打印差异. 这是这个工具最有用
的一半:它把"今天的客户端还在发同样的报文形状"变成一条会失败的断言.
注意回放时要给 ligent 加 --no-probe:能力探测会插进额外的请求,而那几轮
不在录制里。录制时也建议关掉(LIGENT_PROBE=0),两边一致。
注意回放时要给 ligent 加 --no-probe:能力探测会插进额外的请求, 而那几轮
不在录制里. 录制时也建议关掉 (LIGENT_PROBE=0), 两边一致.
"""
import argparse
import json
@@ -32,9 +32,9 @@ import threading
def parse_log(text):
"""把一份 /log 导出切成若干次 HTTP 往返。
"""把一份 /log 导出切成若干次 HTTP 往返.
格式(见 http.c 的记录点):
格式 (见 http.c 的记录点):
===== <stamp> =====
请求: POST <url>
----- 请求体 (N 字节) -----
@@ -45,8 +45,8 @@ def parse_log(text):
<响应原文>
----- 结束: HTTP <status> -----
日志是**滚动缓冲**,开头那一段可能是被截断的,所以要容忍缺标记 —— 跳过
解析不出来的块,而不是整个失败。
日志是**滚动缓冲**, 开头那一段可能是被截断的, 所以要容忍缺标记 —— 跳过
解析不出来的块, 而不是整个失败.
"""
lines = text.split("\n")
out = []
@@ -67,20 +67,20 @@ def parse_log(text):
ex["url"] = parts[1] if len(parts) > 1 else ""
i += 1
# 请求体:按标记里给出的字节数精确取,比按行取稳
# 请求体:按标记里给出的字节数精确取, 比按行取稳
if i < len(lines) and lines[i].startswith("----- 请求体 ("):
n = int(lines[i].split("(")[1].split(" ")[0])
i += 1
# 从这一行起累加字节,直到凑够 n
# 从这一行起累加字节, 直到凑够 n
blob = ""
while i < len(lines) and len(blob.encode()) < n:
blob += (lines[i] + "\n")
i += 1
ex["req"] = blob.encode()[:n]
# 上面多读了一行就把多余的还回去(blob 里包含了尾部换行)
# 上面多读了一行就把多余的还回去 (blob 里包含了尾部换行)
consumed = len(blob.encode())
if consumed > n:
# 把这一行重新交回处理(只可能多出换行)
# 把这一行重新交回处理 (只可能多出换行)
pass
# 响应头
@@ -105,17 +105,17 @@ def parse_log(text):
ex["status"] = 200
i += 1
# 结构不完整(多半是滚动缓冲把开头截了)就丢掉
# 结构不完整 (多半是滚动缓冲把开头截了)就丢掉
if ex["url"] and ex["status"] is not None:
out.append(ex)
return out
def shape_of(req_bytes):
"""一个请求体的**结构指纹**:只看形状,不看内容。
"""一个请求体的**结构指纹**:只看形状, 不看内容.
内容每轮都不一样(历史在长),但形状必须稳定 —— 顶层键、消息角色序列、
工具名集合。客户端一旦改了报文形状,这里就会显示出来。
内容每轮都不一样 (历史在长), 但形状必须稳定 —— 顶层键、消息角色序列、
工具名集合. 客户端一旦改了报文形状, 这里就会显示出来.
"""
try:
body = json.loads(req_bytes.decode("utf-8", "replace"))
@@ -162,9 +162,9 @@ class ReplayServer:
e = self.next_exchange()
if e is None:
sys.stderr.write(
"replay: 录制里没有更多响应了(已经回了 %d 个)。\n"
" 客户端比录制时多发了请求 —— 多半是多了能力探测(回放时请加 "
"--no-probe)或者客户端行为变了。\n" % len(self.ex))
"replay: 录制里没有更多响应了 (已经回了 %d 个). \n"
" 客户端比录制时多发了请求 —— 多半是多了能力探测 (回放时请加 "
"--no-probe)或者客户端行为变了. \n" % len(self.ex))
return 500, {}, b'{"error":{"message":"replay: out of recorded responses"}}'
if self.strict:
@@ -197,8 +197,8 @@ def serve(exchanges, port, strict, loop):
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("127.0.0.1", port))
s.listen(8)
print("replay server on %d(%d 个录制往返%s)"
% (port, len(exchanges), ",strict" if strict else ""), flush=True)
print("replay server on %d (%d 个录制往返%s)"
% (port, len(exchanges), ", strict" if strict else ""), flush=True)
while True:
conn, _ = s.accept()
@@ -250,7 +250,7 @@ def main():
ap.add_argument("--check", action="store_true", help="自检能否解析成完整往返")
ap.add_argument("--serve", type=int, metavar="PORT", help="当离线服务端")
ap.add_argument("--strict", action="store_true",
help="比对每个请求的结构,不一致就回 400")
help="比对每个请求的结构, 不一致就回 400")
ap.add_argument("--loop", action="store_true", help="响应用完后从头再来")
a = ap.parse_args()
@@ -282,7 +282,7 @@ def main():
serve(ex, a.serve, a.strict, a.loop)
return 0
ap.error("要么 --summary/--check,要么 --serve PORT")
ap.error("要么 --summary/--check, 要么 --serve PORT")
if __name__ == "__main__":