fix(platform): 端点协议感知治理(404根因)——端点落库带protocol标签;裸域退化拦截(同host已有带路径端点则跳过);账号端点绑定自检:活跃账号未绑本协议端点自动补绑并报account_notes

This commit is contained in:
yumoqing 2026-09-05 23:08:15 +08:00
parent ac52a61e0b
commit d6228c59c6

View File

@ -499,7 +499,13 @@ async def _h_apply_llm_config(sor, params, ctx):
from appPublic.uniqueID import getID from appPublic.uniqueID import getID
# 1. 供应商:按名称复用 # 1. 供应商:按名称复用。端点带协议标签(2026-09-05 404 实测教训):
# 同一供应商可同时有 openai_compat(compatible-mode/v1) 与原生异步(api/v1)
# 两类端点,账号绑定/运行时选择必须按协议匹配,否则原生模型被拼到
# compatible-mode 前缀下 → 404。另拦截「裸域退化」:提取 LLM 偶发把
# base_url 提成 https://host(丢了 /api/v1),与已有带路径端点同 host
# 时该裸域无意义,跳过不入目录。
from urllib.parse import urlparse as _urlparse
recs = await sor.sqlExe( recs = await sor.sqlExe(
"SELECT id, endpoints FROM llm_vendor WHERE name=${n}$", {"n": vendor_name}) "SELECT id, endpoints FROM llm_vendor WHERE name=${n}$", {"n": vendor_name})
await sor.sqlExe("COMMIT", {}) await sor.sqlExe("COMMIT", {})
@ -512,29 +518,55 @@ async def _h_apply_llm_config(sor, params, ctx):
eps_old = [] eps_old = []
merged = list(eps_old) merged = list(eps_old)
added_eps = [] added_eps = []
skipped_eps = []
for ep in endpoints: for ep in endpoints:
bu = (ep.get("base_url") or "").rstrip("/") bu = (ep.get("base_url") or "").rstrip("/")
if bu and bu not in [(e.get("base_url") or "").rstrip("/") for e in merged]: if not bu:
merged.append({"base_url": bu, "region": ep.get("region") or "domestic", continue
"timeout": int(_f(ep.get("timeout"), 60))}) # 同 base_url 已存在:只补协议标签(旧端点无 protocol 时回填)
added_eps.append(bu) hit = None
# 2026-09-05:供应商表不再有 protocol——请求形态由模型挂的适配模板决定 for e in merged:
if (e.get("base_url") or "").rstrip("/") == bu:
hit = e
break
if hit is not None:
if protocol and not hit.get("protocol"):
hit["protocol"] = protocol
continue
# 裸域退化拦截:同 host 已有带路径端点 → 裸域跳过
pu = _urlparse(bu)
if not pu.path.strip("/"):
same_host = any(_urlparse((e.get("base_url") or "")).netloc == pu.netloc
for e in merged)
if same_host:
skipped_eps.append("%s(裸域退化:同 host 已有带路径端点)" % bu)
continue
merged.append({"base_url": bu, "region": ep.get("region") or "domestic",
"timeout": int(_f(ep.get("timeout"), 60)),
"protocol": protocol})
added_eps.append(bu)
await sor.sqlExe( await sor.sqlExe(
"UPDATE llm_vendor SET endpoints=${e}$, updated_at=NOW() WHERE id=${i}$", "UPDATE llm_vendor SET endpoints=${e}$, updated_at=NOW() WHERE id=${i}$",
{"e": json.dumps(merged, ensure_ascii=False), "i": vendor_id}) {"e": json.dumps(merged, ensure_ascii=False), "i": vendor_id})
await sor.sqlExe("COMMIT", {}) await sor.sqlExe("COMMIT", {})
vendor_action = "复用供应商 %s(新增端点 %d 个)" % (vendor_id, len(added_eps)) vendor_action = "复用供应商 %s(新增端点 %d 个%s)" % (
vendor_id, len(added_eps),
(",跳过 %s" % ";".join(skipped_eps)) if skipped_eps else "")
vendor_endpoints = merged
else: else:
vendor_id = getID() vendor_id = getID()
eps_norm = [{"base_url": (ep.get("base_url") or "").rstrip("/"), eps_norm = [{"base_url": (ep.get("base_url") or "").rstrip("/"),
"region": ep.get("region") or "domestic", "region": ep.get("region") or "domestic",
"timeout": int(_f(ep.get("timeout"), 60))} for ep in endpoints] "timeout": int(_f(ep.get("timeout"), 60)),
"protocol": protocol} for ep in endpoints
if (ep.get("base_url") or "").strip()]
await sor.C("llm_vendor", { await sor.C("llm_vendor", {
"id": vendor_id, "name": vendor_name, "id": vendor_id, "name": vendor_name,
"endpoints": json.dumps(eps_norm, ensure_ascii=False), "endpoints": json.dumps(eps_norm, ensure_ascii=False),
"description": spec.get("doc_notes", "") or "", "description": spec.get("doc_notes", "") or "",
"status": "active", "org_id": ctx.get("org_id", "") or "0"}) "status": "active", "org_id": ctx.get("org_id", "") or "0"})
vendor_action = "新建供应商 %s(%s)" % (vendor_id, vendor_name) vendor_action = "新建供应商 %s(%s)" % (vendor_id, vendor_name)
vendor_endpoints = eps_norm
# 2. 适配模板:按 (协议×能力) 复用;旧骨架模板(硬编码 xxx_file/__from_doc__, # 2. 适配模板:按 (协议×能力) 复用;旧骨架模板(硬编码 xxx_file/__from_doc__,
# 2026-09-05 实测 t2v 因它渲染崩)视为不可用,按文档示例自愈重建 # 2026-09-05 实测 t2v 因它渲染崩)视为不可用,按文档示例自愈重建
@ -649,6 +681,32 @@ async def _h_apply_llm_config(sor, params, ctx):
if protocol not in RUNTIME_PROTOCOLS: if protocol not in RUNTIME_PROTOCOLS:
rt_note = ("⚠️ 协议「%s」的适配模板已存档,但当前运行时调用链仅支持 %s——" rt_note = ("⚠️ 协议「%s」的适配模板已存档,但当前运行时调用链仅支持 %s——"
"该供应商模型暂不可被产线直接调用" % (protocol, "/".join(RUNTIME_PROTOCOLS))) "该供应商模型暂不可被产线直接调用" % (protocol, "/".join(RUNTIME_PROTOCOLS)))
# 4. 账号端点绑定自检(2026-09-05 404 教训):账号只绑 compatible-mode
# 端点时,原生协议模型运行时会被选到错误端点 → 404。自动补绑本协议
# 端点到该供应商全部活跃账号(不删已有绑定),如实报告。
account_notes = []
proto_idx = [i for i, e in enumerate(vendor_endpoints or [])
if (e.get("protocol") or "") == protocol]
if proto_idx:
recs = await sor.sqlExe(
"SELECT id, name, endpoint_ids FROM llm_account "
"WHERE vendor_id=${v}$ AND status='active'", {"v": vendor_id})
await sor.sqlExe("COMMIT", {})
for acc in recs or []:
try:
bound = [int(b) for b in json.loads(getattr(acc, "endpoint_ids", "") or "[]")]
except Exception:
bound = []
if any(i in proto_idx for i in bound):
continue # 已绑本协议端点,可用
newb = sorted(set(bound) | set(proto_idx))
await sor.sqlExe(
"UPDATE llm_account SET endpoint_ids=${e}$, updated_at=NOW() WHERE id=${i}$",
{"e": json.dumps(newb), "i": getattr(acc, "id", "")})
await sor.sqlExe("COMMIT", {})
account_notes.append("账号「%s」原绑定端点 %s 不含协议 %s 的端点(运行时会 404),"
"已自动补绑为 %s" % (getattr(acc, "name", ""), bound,
protocol, newb))
audit_note = await _audit_media_convention(sor, list(profile_ids.values())) audit_note = await _audit_media_convention(sor, list(profile_ids.values()))
return json.dumps({ return json.dumps({
"vendor": vendor_action, "profiles": profile_ids, "vendor": vendor_action, "profiles": profile_ids,
@ -656,6 +714,7 @@ async def _h_apply_llm_config(sor, params, ctx):
"vendor_conflicts": conflicts, "vendor_conflicts": conflicts,
"template_errors": tpl_errors, "template_errors": tpl_errors,
"template_notes": tpl_notes, "template_notes": tpl_notes,
"account_notes": account_notes,
"runtime_note": rt_note, "runtime_note": rt_note,
"media_audit": audit_note, "media_audit": audit_note,
}, ensure_ascii=False) }, ensure_ascii=False)