chore: 规范标点

This commit is contained in:
pluv committed 2026-10-03 14:56:56 +08:00
1 parent 15e79887fa
commit 3dd2231c70
81 files changed
+1751 -1751

No files matched your search

+40 -40
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""mock_server.py - 最小的 OpenAI 兼容服务端, 用于端到端测试 ligent.
不依赖任何第三方包. 行为完全由请求内容决定, 保持确定性:
不依赖任何第三方包. 行为完全由请求内容决定, 保持确定性:
- 用户消息含 ERROR_TEST -> 返回 HTTP 400 + error 对象
- 尚未有 tool 结果 -> 流式返回一个 tool_call (run_command echo ...)
@@ -25,7 +25,7 @@ def sse(wfile, obj):
def pairing_error(msgs):
"""服务端的硬要求:带 tool_calls 的 assistant 后面必须**紧跟**与每个
"""服务端的硬要求: 带 tool_calls 的 assistant 后面必须**紧跟**与每个
tool_call_id 一一对应的 tool 消息, 中间不能夹别的东西.
真实服务端就是这么回 400 的, mock 也照做 —— 这样任何会写坏历史的回归
@@ -120,7 +120,7 @@ class Handler(BaseHTTPRequestHandler):
def do_GET(self):
# doctor 的连通性/重定向检查走 GET.
# /redirect 开头故意返回 307, 用来验证客户端能把重定向指出来.
# doctor 的认证探测:端点要求认证, 且只认 "Bearer good-key".
# doctor 的认证探测: 端点要求认证, 且只认 "Bearer good-key".
# 带对 key -> 200 (认证过了)
# 带错 key -> 401 (密钥被拒)
# 不带 key -> 401 (端点要求认证, 而你没配)
@@ -140,7 +140,7 @@ class Handler(BaseHTTPRequestHandler):
# MODEL_LIST=bare 时改回裸数组, MODEL_LIST=empty 时不返回任何模型,
# 两种兼容/异常形状都由 e2e 覆盖.
if "/models" in self.path:
# 模式从**文件**读, 不从环境变量读:mock 是脚本开头起好的,
# 模式从**文件**读, 不从环境变量读: mock 是脚本开头起好的,
# 给客户端进程设的 env 它根本看不见 (第一版就是这么假通过的).
mode = ""
mode_file = os.environ.get("MODELS_MODE_FILE", "")
@@ -157,7 +157,7 @@ class Handler(BaseHTTPRequestHandler):
elif mode == "garbage":
payload = b"<html>not json at all</html>"
else:
# 默认列表里**故意包含当前模型** (deepseek-flash):
# 默认列表里**故意包含当前模型** (deepseek-flash):
# 这样才验得了"光标初始落在当前项上".
# 另外塞一个重复项和一个缺 id 的坏条目, 验去重与跳过.
payload = json.dumps({"object": "list", "data": [
@@ -191,7 +191,7 @@ class Handler(BaseHTTPRequestHandler):
length = int(self.headers.get("Content-Length", "0"))
raw = self.rfile.read(length)
# ---- UTF-8 严格校验:真服务端就是这么拒的 ----
# ---- UTF-8 严格校验: 真服务端就是这么拒的 ----
# DeepSeek 对请求体里非法的 UTF-8 回的是
# 400 Failed to parse the request body as JSON:
# messages[43].content: invalid unicode code point
@@ -208,7 +208,7 @@ class Handler(BaseHTTPRequestHandler):
return
# ---- 假的 Files API ----
# 只做够用的部分:认 multipart 里有没有图片内容, 发一个递增的 file_id.
# 只做够用的部分: 认 multipart 里有没有图片内容, 发一个递增的 file_id.
try:
body = json.loads(raw or b"{}")
except Exception:
@@ -237,7 +237,7 @@ class Handler(BaseHTTPRequestHandler):
tool_msgs = [m for m in messages if m.get("role") == "tool"]
tools_sent = body.get("tools")
# ---- 结构校验:和真服务端一样严 ----
# ---- 结构校验: 和真服务端一样严 ----
perr = pairing_error(messages)
if perr:
self.reject(400, perr)
@@ -246,7 +246,7 @@ class Handler(BaseHTTPRequestHandler):
# ---- 能力探测序列 ----
# ligent 会在第一次真实请求之前发几个试探请求 (基线 / 图片 / tools),
# 它们会被真实处理, 所以这里必须排在所有标记分支之前.
# 辨认靠形状:刚好一条 user 消息, 内容是 "1", 或者带一张 1x1 的图.
# 辨认靠形状: 刚好一条 user 消息, 内容是 "1", 或者带一张 1x1 的图.
def looks_like_probe(msgs):
if len(msgs) != 1 or msgs[0].get("role") != "user":
return False
@@ -264,7 +264,7 @@ class Handler(BaseHTTPRequestHandler):
if looks_like_probe(messages):
has_image = isinstance(messages[0].get("content"), list)
# 用模型名模拟"不支持某能力"的服务端:400 是明确的拒绝信号
# 用模型名模拟"不支持某能力"的服务端: 400 是明确的拒绝信号
if "no-vision" in model and has_image:
return self.reject(400, "unsupported content type: image_url")
if "no-tools" in model and tools_sent:
@@ -274,7 +274,7 @@ class Handler(BaseHTTPRequestHandler):
self.send_text("1", stream, model)
return
# 提权:让模型发一个带 elevated 的 run_command
# 提权: 让模型发一个带 elevated 的 run_command
if "ELEVATED_TEST" in first_user:
if not tool_msgs:
args = json.dumps({"command": "echo 提权后的输出", "elevated": True})
@@ -293,7 +293,7 @@ class Handler(BaseHTTPRequestHandler):
self.send_text("ELEV_SAW[%s]" % got.replace("\n", " | "), stream, model)
return
# /include:把收到的 user 内容块报回来.
# /include: 把收到的 user 内容块报回来.
# 用 first_user 判断——附带之后最新那条 user 是带块的消息, last_user 会漂.
if "INCLUDE_TEST" in first_user:
texts, imgs, names = 0, 0, []
@@ -330,7 +330,7 @@ class Handler(BaseHTTPRequestHandler):
stream, model)
return
# 权限/参数校验:客户端必须把 tools 传上来
# 权限/参数校验: 客户端必须把 tools 传上来
assert tools_sent, "请求里没有 tools 字段"
if "ERROR_TEST" in first_user:
@@ -384,7 +384,7 @@ class Handler(BaseHTTPRequestHandler):
self.wfile.write(payload)
return
# 复刻 OpenRouter 对"key 格式不对"的回应:头其实带了,
# 复刻 OpenRouter 对"key 格式不对"的回应: 头其实带了,
# 但文案说的是 "Missing Authentication header", 容易把人引偏
if "AUTHFAIL_TEST" in first_user:
payload = json.dumps({"error": {"message": "Missing Authentication header",
@@ -396,7 +396,7 @@ class Handler(BaseHTTPRequestHandler):
self.wfile.write(payload)
return
# 重定向:用于验证客户端不静默跟随 (跨源跟随会丢掉 Authorization,
# 重定向: 用于验证客户端不静默跟随 (跨源跟随会丢掉 Authorization,
# 服务端只会返回 401 "Missing Authentication header", 误导用户去查 key)
if "REDIRECT_TEST" in first_user:
self.send_response(307)
@@ -443,7 +443,7 @@ class Handler(BaseHTTPRequestHandler):
self.wfile.write(payload)
return
# 超过客户端响应体上限的超大响应:客户端应当主动中止而不是被撑爆
# 超过客户端响应体上限的超大响应: 客户端应当主动中止而不是被撑爆
if "HUGE_TEST" in first_user:
total = 40 * 1024 * 1024
self.send_response(200)
@@ -459,7 +459,7 @@ class Handler(BaseHTTPRequestHandler):
pass # 客户端主动断开是预期行为
return
# 畸形 SSE:各种不合法的片段, 客户端必须不崩溃且给出合理结果
# 畸形 SSE: 各种不合法的片段, 客户端必须不崩溃且给出合理结果
if "GARBAGE_TEST" in first_user:
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
@@ -479,7 +479,7 @@ class Handler(BaseHTTPRequestHandler):
self.wfile.flush()
return
# 传输中途断掉:声明了 Content-Length 却提前关闭连接
# 传输中途断掉: 声明了 Content-Length 却提前关闭连接
if "TRUNCATE_TEST" in first_user:
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
@@ -537,7 +537,7 @@ class Handler(BaseHTTPRequestHandler):
self.wfile.flush()
return
# 思考模式:验证 reasoning_content 既能被累积, 也能随 assistant 消息
# 思考模式: 验证 reasoning_content 既能被累积, 也能随 assistant 消息
# 回传. DeepSeek 要求请求带 tools 时把历史里的 reasoning_content 全带上,
# 漏了下一轮就 400, 所以这里第二轮专门检查它有没有回来.
if "THINK_TEST" in first_user:
@@ -562,7 +562,7 @@ class Handler(BaseHTTPRequestHandler):
self.send_header("Content-Type", "text/event-stream")
self.send_header("Cache-Control", "no-cache")
self.end_headers()
# 思考过程分片下发:客户端必须累积, 而不是只留最后一片
# 思考过程分片下发: 客户端必须累积, 而不是只留最后一片
for part in ("先想一下", "要不要调用工具. "):
sse(self.wfile, {"choices": [{"index": 0,
"delta": {"reasoning_content": part},
@@ -587,8 +587,8 @@ class Handler(BaseHTTPRequestHandler):
body.get("reasoning_effort") or "none"), stream, model)
return
# 慢速响应:先静默一段 (网络/排队), 再吐思维链, 再静默一段,
# 最后给正文. 用来验证等待动画的两段标签:"等待响应" → "思考中".
# 慢速响应: 先静默一段 (网络/排队), 再吐思维链, 再静默一段,
# 最后给正文. 用来验证等待动画的两段标签: "等待响应" → "思考中".
# 名字刻意不含 "THINK_TEST" —— 上面那些标记都是按子串匹配的,
# 叫 SLOWTHINK_TEST 会被 THINK_TEST 那个分支先截胡.
if "SLOWRESP_TEST" in first_user:
@@ -604,7 +604,7 @@ class Handler(BaseHTTPRequestHandler):
self.send_text("答案", stream=True, model=model)
return
# meta 模式:第一轮让 meta 模型调 delete_rounds 删掉第 2 轮,
# meta 模式: 第一轮让 meta 模型调 delete_rounds 删掉第 2 轮,
# 第二轮收工. 用来端到端验证 /meta 的完整链路.
if "META_TEST" in first_user:
if not tool_msgs:
@@ -626,7 +626,7 @@ class Handler(BaseHTTPRequestHandler):
self.send_text("已删除第 2 轮. ", stream, model)
return
# 按"最后一条用户消息"触发:上面那些模式都看 first_user,
# 按"最后一条用户消息"触发: 上面那些模式都看 first_user,
# 同一个进程里的第二轮就再也触发不了了, 没法验证"作用域仅限本轮".
if "WRITE_LAST_TEST" in last_user:
if messages[-1].get("role") == "user": # 新一轮的开头
@@ -656,7 +656,7 @@ class Handler(BaseHTTPRequestHandler):
self.send_text("第二轮写完了", stream, model)
return
# 发出一个 write_file 调用:用来验证"在确认提示上按 Ctrl-C"到底
# 发出一个 write_file 调用: 用来验证"在确认提示上按 Ctrl-C"到底
# 会不会把文件写下去
if "WRITE_TEST" in first_user:
if not tool_msgs:
@@ -686,7 +686,7 @@ class Handler(BaseHTTPRequestHandler):
self.send_text("写完了", stream, model)
return
# 读一个**含非法 UTF-8 字节**的文件. 工具输出里出现坏字节的来源很日常:
# 读一个**含非法 UTF-8 字节**的文件. 工具输出里出现坏字节的来源很日常:
# 某条命令在非 UTF-8 locale 下打出来的路径、被截断的多字节字符、二进制
# 文件里混的一小段文本. 这段字节会进对话历史, 然后被序列化进请求体 ——
# 如果序列化器原样吐出, 服务端就会 400, 而且之后每次请求都 400.
@@ -715,7 +715,7 @@ class Handler(BaseHTTPRequestHandler):
self.wfile.write(b"data: [DONE]\n\n")
self.wfile.flush()
return
# 第二轮:能走到这里就说明请求体过了上面的 UTF-8 校验.
# 第二轮: 能走到这里就说明请求体过了上面的 UTF-8 校验.
# 把工具消息里有没有替换字符也顺带报出来, 便于断言.
got = ""
for m in tool_msgs:
@@ -725,8 +725,8 @@ class Handler(BaseHTTPRequestHandler):
self.send_text("读完了, 替换字符=%d" % got.count("\ufffd"), stream, model)
return
# 一轮里连写两个文件:用来验证 Always/neVer 的作用域就是"这一轮"
# (名字刻意不含 "WRITE_TEST", 免得被上面那个分支先截胡)
# 一轮里连写两个文件: 用来验证 Always/neVer 的作用域就是"这一轮"
# (名字刻意不含 "WRITE_TEST", 以免被上面那个分支先截胡)
if "WRITE2_TEST" in first_user:
n = len(tool_msgs)
if n < 2:
@@ -759,8 +759,8 @@ class Handler(BaseHTTPRequestHandler):
# 让模型去读一个"扩展名是 .png、内容不是图片"的文件,
# 验证 read_image 按内容判断、并明确拒绝
# 两种"读不了":内容不是图片、以及大得内联不进去.
# 注意:mock 的标记靠子串匹配, 是平铺的命名空间 —— 上面第 249 行的
# 两种"读不了": 内容不是图片、以及大得内联不进去.
# 注意: mock 的标记靠子串匹配, 是平铺的命名空间 —— 上面第 249 行的
# HUGE_TEST 分支会吃掉任何含 "HUGE_TEST" 的标记, 所以这里叫 OVERSIZE.
# 两者都该在**本地**被拒 —— 工具返回错误, 之后就没有图片块可发.
if "IMAGE_BAD_TEST" in last_user or "IMAGE_OVERSIZE_TEST" in last_user:
@@ -780,9 +780,9 @@ class Handler(BaseHTTPRequestHandler):
return
# 一条 assistant 里发**两个**工具调用, 第一个是 read_image.
# 这是"图片附件插在两条 tool 消息中间"那个 bug 的最小复现形状:
# 这是"图片附件插在两条 tool 消息中间"那个 bug 的最小复现形状:
# 只要附件没被攒到最后一起推, 上面的结构校验就会回 400.
# 注意用 first_user:图片附件本身也是一条 user 消息, 而且排在最后,
# 注意用 first_user: 图片附件本身也是一条 user 消息, 而且排在最后,
# 所以第二轮请求的 last_user 是那个附件 (一个 list), 不是这条标记.
if "IMAGE_PAIR_TEST" in first_user:
if not tool_msgs:
@@ -808,7 +808,7 @@ class Handler(BaseHTTPRequestHandler):
% (len(found), len(tool_msgs)), stream, model)
return
# 图片:先让模型调一次 read_image, 下一轮把收到的图片内联块报回来.
# 图片: 先让模型调一次 read_image, 下一轮把收到的图片内联块报回来.
# Files API 已经不用了 —— 请求里应该直接是 image_url + data: base64.
if "IMAGE_TEST" in first_user:
if not tool_msgs:
@@ -835,8 +835,8 @@ class Handler(BaseHTTPRequestHandler):
self.wfile.flush()
return
# 第二轮:请求里应该已经带上内联图片了.
# 报文形状:{"type":"image_url","image_url":{"url":"data:image/png;base64,..."}}
# 第二轮: 请求里应该已经带上内联图片了.
# 报文形状: {"type":"image_url","image_url":{"url":"data:image/png;base64,..."}}
found, notes = [], []
for m in messages:
c = m.get("content")
@@ -856,7 +856,7 @@ class Handler(BaseHTTPRequestHandler):
elif part.get("type") == "text" and m.get("role") == "user":
notes.append(part.get("text", ""))
# IMAGE_REJECT_TEST:第一次故意说服务端不收这张图,
# IMAGE_REJECT_TEST: 第一次故意说服务端不收这张图,
# 客户端应该把图片从历史里摘掉再试 (而不是每轮都失败)
if "IMAGE_REJECT_TEST" in last_user and not STATE.get("told_once"):
STATE["told_once"] = 1
@@ -874,7 +874,7 @@ class Handler(BaseHTTPRequestHandler):
self.send_text(reply, stream, model)
return
# edit_file 的**调用展示**:成功时屏幕上该是"路径 + 行号"加 -/+ 新旧对照,
# edit_file 的**调用展示**: 成功时屏幕上该是"路径 + 行号"加 -/+ 新旧对照,
# 而不是一整段参数 JSON (old_string/new_string 原样打出来会刷屏).
# EDITTEST_OK 成功剧本, EDITTEST_FAIL 行号越界剧本.
if "EDITTEST_OK" in first_user or "EDITTEST_FAIL" in first_user:
@@ -883,7 +883,7 @@ class Handler(BaseHTTPRequestHandler):
stream, model)
return
line = 999 if "EDITTEST_FAIL" in first_user else 2
# 12 行的 old_string:既覆盖"多行改动"的显示, 也覆盖"超过 8 行要折叠"
# 12 行的 old_string: 既覆盖"多行改动"的显示, 也覆盖"超过 8 行要折叠"
old = "\n".join("OLD%d" % i for i in range(1, 13)) if line == 2 else "OLD"
args = json.dumps({"path": "build/tmp/edit_probe.txt", "line": line,
"old_string": old, "new_string": "NEW"})
@@ -909,7 +909,7 @@ class Handler(BaseHTTPRequestHandler):
self.wfile.flush()
return
# 多步 (4 次工具往返才收工):用来钉住"往返次数默认不限".
# 多步 (4 次工具往返才收工): 用来钉住"往返次数默认不限".
# 默认配置下必须跑得完; 显式设了正数上限时要停在那个上限上并如实报出.
if "MANYSTEPS_TEST" in first_user:
done = len(tool_msgs)