feat(platform): 提交模板复用键改形态指纹(协议×能力×指纹)——根治r2v媒体模态混用(2026-09-06用户确认方案)
- _tpl_fingerprint: 归一化(path,req,resp,schema) sha256[:12],JSON往返消键序/空白差,Jinja原文退化去空白
- 命名 {protocol}-{cap}-自动配置-{指纹}: 同形态复用(旧命名收编改名),异形态各建各的(旧模板保留不断存量)
- 坏模板(hard违规/骨架)一律弃用重建;指纹命名但内容漂移的也弃用重建
- 精确命名查找取代LIKE排除查询/下载模板(命名形态不同天然不误选)
- 效果: happyhorse r2v纯图与wan3.0 r2v四模态各挂各的;wan3.1同API零新增
- 本地单测8项全过(键序归一/媒体差异敏感/path敏感/schema混传等价/Jinja退化)
This commit is contained in:
parent
3c0de3bad4
commit
7af34c1251
@ -16,6 +16,7 @@ pipeline-service: platform_ability — 平台内部 agent 能力包(pipeline_i
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
@ -525,8 +526,10 @@ def _capability_guard(capability, description):
|
||||
async def _h_apply_llm_config(sor, params, ctx):
|
||||
"""按提取的规格写库:供应商(复用/新建)+ 适配模板 + 模型。
|
||||
|
||||
幂等:供应商按名称复用;模型按 name/vendor_model_id 复用(name 全局唯一键)——
|
||||
已存在则更新元数据;供应商归属不一致时不改挂、如实报 vendor_conflicts。
|
||||
幂等:供应商按名称复用;模板按 (协议×能力×形态指纹) 复用——同形态复用/收编,
|
||||
异形态各建各的(见 _tpl_fingerprint);模型按 name/vendor_model_id 复用
|
||||
(name 全局唯一键)——已存在则更新元数据;供应商归属不一致时不改挂、
|
||||
如实报 vendor_conflicts。
|
||||
|
||||
规格来源(2026-09-05 防编造改造):
|
||||
use_last_extract=true(推荐)→ 取本会话 extract 锚定的规格,
|
||||
@ -681,34 +684,40 @@ async def _h_apply_llm_config(sor, params, ctx):
|
||||
vendor_action = "新建供应商 %s(%s)" % (vendor_id, vendor_name)
|
||||
vendor_endpoints = eps_norm
|
||||
|
||||
# 2. 适配模板:按 (协议×能力) 复用;旧骨架模板(硬编码 xxx_file/__from_doc__,
|
||||
# 2026-09-05 实测 t2v 因它渲染崩)视为不可用,按文档示例自愈重建
|
||||
# 2. 适配模板:按 (协议×能力×形态指纹) 复用(2026-09-06 用户定夺)。
|
||||
# 纯按能力复用的事故:happyhorse r2v 纯图 vs wan3.0 r2v 四模态,共享模板
|
||||
# 静默丢媒体;一模型一模板则 wan3.1 同 API 时白白新增。指纹同→复用,
|
||||
# 指纹异→各建各的。旧骨架模板(硬编码 xxx_file/__from_doc__,2026-09-05
|
||||
# 实测 t2v 因它渲染崩)视为不可用,按文档示例自愈重建。
|
||||
# 旧命名 {protocol}-{cap}-自动配置(无指纹段):形态一致则改名收编,
|
||||
# 不一致则保留(已挂模型继续可用),新形态另建。
|
||||
profile_ids = {}
|
||||
tpl_errors = []
|
||||
tpl_notes = []
|
||||
tpl_notes.extend(cap_notes) # 能力守卫纠正/告警先进 notes(前置阶段产生)
|
||||
caps = sorted(set((m.get("capability") or "t2t") for m in models))
|
||||
for cap in caps:
|
||||
# 名称过滤必须加(2026-09-05 e2e 实测 bug):查询/下载步骤模板同库同协议
|
||||
# 同能力,不加过滤会被「最新优先」误选为提交模板 → 提交打到查询 path → 404
|
||||
# 两种命名都要排(2026-09-06 i2v 实测漏网):
|
||||
# 共享命名 {protocol}-{purpose}-{path} → 含中缀 -query-
|
||||
# legacy 命名 {vmid}-{purpose} → 以 -query 结尾(无后置短横)
|
||||
# 只排中缀时 legacy 查询模板会被当成提交模板挂到模型上(schema 为空
|
||||
# 过不了体检门禁也不报错——空 schema 无 hard 违规,静默误挂)
|
||||
# 先生成再比对:指纹来自生成结果(path/req/resp/schema 归一化 sha256[:12])
|
||||
tpl = _gen_templates(protocol, cap, spec)
|
||||
dry_err = _dry_render_check(tpl["req"], cap, tpl["biz_params"], tpl["media_params"])
|
||||
if dry_err:
|
||||
# 宁可不建,不埋雷:渲染不了的模板落库=运行时必崩,如实报错由助手修正规格重跑
|
||||
# 模型已挂的旧模板不动(更新路径 IF 保护,profile_id 保持原值)
|
||||
tpl_errors.append("%s: %s" % (cap, dry_err))
|
||||
continue
|
||||
fp = _tpl_fingerprint(tpl["path"], tpl["req"], tpl["resp"], tpl["param_schema"])
|
||||
shared_name = "%s-%s-自动配置-%s" % (protocol, cap, fp)
|
||||
legacy_name = "%s-%s-自动配置" % (protocol, cap)
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT id, request_template, response_template, param_schema "
|
||||
"SELECT id, name, path, request_template, response_template, param_schema "
|
||||
"FROM llm_api_profile "
|
||||
"WHERE protocol=${p}$ AND capability=${c}$ AND status='active' "
|
||||
"AND name NOT LIKE ${l1}$ AND name NOT LIKE ${l2}$ "
|
||||
"AND name NOT LIKE ${l3}$ AND name NOT LIKE ${l4}$ "
|
||||
"ORDER BY created_at DESC LIMIT 1",
|
||||
{"p": protocol, "c": cap,
|
||||
"l1": "%-query-%", "l2": "%-download-%",
|
||||
"l3": "%-query", "l4": "%-download"})
|
||||
"WHERE name IN (${n1}$, ${n2}$) AND status='active' "
|
||||
"ORDER BY CASE WHEN name=${n1}$ THEN 0 ELSE 1 END LIMIT 1",
|
||||
{"n1": shared_name, "n2": legacy_name})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
if recs:
|
||||
pid_old = getattr(recs[0], "id", "")
|
||||
nm_old = getattr(recs[0], "name", "")
|
||||
old_tpl = (getattr(recs[0], "request_template", "") or "") + \
|
||||
(getattr(recs[0], "response_template", "") or "")
|
||||
# schema 体检:存量脏 schema(未注册 uitype/default/重复字段/结构常量)判死重建,
|
||||
@ -716,27 +725,45 @@ async def _h_apply_llm_config(sor, params, ctx):
|
||||
sv = _schema_violations(getattr(recs[0], "param_schema", ""))
|
||||
for s in sv['soft']:
|
||||
tpl_notes.append("模板 %s(%s)schema 提示:%s" % (pid_old, cap, s))
|
||||
if not sv['hard'] and not any(mk in old_tpl for mk in _SKELETON_MARKERS):
|
||||
fp_old = _tpl_fingerprint(getattr(recs[0], "path", "") or "",
|
||||
getattr(recs[0], "request_template", "") or "",
|
||||
getattr(recs[0], "response_template", "") or "",
|
||||
getattr(recs[0], "param_schema", "") or "")
|
||||
broken = bool(sv['hard']) or any(mk in old_tpl for mk in _SKELETON_MARKERS)
|
||||
if not broken and fp_old == fp:
|
||||
if nm_old != shared_name:
|
||||
# 旧命名收编:形态一致,改名挂指纹(存量模型按 id 引用,不受影响)
|
||||
await sor.sqlExe(
|
||||
"UPDATE llm_api_profile SET name=${n}$ WHERE id=${i}$",
|
||||
{"n": shared_name, "i": pid_old})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
tpl_notes.append("模板 %s(%s)形态与本次生成一致,旧命名收编为 %s"
|
||||
% (pid_old, cap, shared_name))
|
||||
profile_ids[cap] = pid_old
|
||||
continue
|
||||
await sor.sqlExe("UPDATE llm_api_profile SET status='deprecated' "
|
||||
"WHERE id=${i}$", {"i": pid_old})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
if sv['hard']:
|
||||
tpl_notes.append("存量模板 %s(%s)param_schema 不合规,已弃用重建:%s"
|
||||
% (pid_old, cap, ";".join(sv['hard'])))
|
||||
if broken or nm_old == shared_name:
|
||||
# 坏模板(hard 违规/骨架标记)一律弃用——不论命名;
|
||||
# 指纹命名但内容漂移(人工改过库)的也弃用重建
|
||||
await sor.sqlExe("UPDATE llm_api_profile SET status='deprecated' "
|
||||
"WHERE id=${i}$", {"i": pid_old})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
if sv['hard']:
|
||||
tpl_notes.append("存量模板 %s(%s)param_schema 不合规,已弃用重建:%s"
|
||||
% (pid_old, cap, ";".join(sv['hard'])))
|
||||
elif any(mk in old_tpl for mk in _SKELETON_MARKERS):
|
||||
tpl_notes.append("旧骨架模板 %s(%s)含硬编码占位符,已弃用并按文档示例重建"
|
||||
% (pid_old, cap))
|
||||
else:
|
||||
tpl_notes.append("模板 %s(%s)内容与命名指纹不符(漂移),已弃用重建"
|
||||
% (pid_old, cap))
|
||||
else:
|
||||
tpl_notes.append("旧骨架模板 %s(%s)含硬编码占位符,已弃用并按文档示例重建"
|
||||
% (pid_old, cap))
|
||||
tpl = _gen_templates(protocol, cap, spec)
|
||||
dry_err = _dry_render_check(tpl["req"], cap, tpl["biz_params"], tpl["media_params"])
|
||||
if dry_err:
|
||||
# 宁可不建,不埋雷:渲染不了的模板落库=运行时必崩,如实报错由助手修正规格重跑
|
||||
tpl_errors.append("%s: %s" % (cap, dry_err))
|
||||
continue
|
||||
# 旧命名、健康、但形态不一致:保留(还挂着存量模型,弃用会断运行时),
|
||||
# 新形态走下面新建
|
||||
tpl_notes.append("旧命名模板 %s(%s)形态与本次生成不一致(指纹 %s≠%s),"
|
||||
"保留旧模板、按新形态另建" % (pid_old, cap, fp_old, fp))
|
||||
pid = getID()
|
||||
await sor.C("llm_api_profile", {
|
||||
"id": pid, "name": "%s-%s-自动配置" % (protocol, cap),
|
||||
"id": pid, "name": shared_name,
|
||||
"protocol": protocol, "capability": cap,
|
||||
"path": tpl["path"],
|
||||
# method 独立列(2026-09-06 用户定夺):提交类模板恒 POST
|
||||
@ -1276,6 +1303,36 @@ _SKELETON_MARKERS = ('__from_doc__', '__note__', 'xxx_file',
|
||||
'params.image_file)', 'params.video_file)', 'params.audio_file)')
|
||||
|
||||
|
||||
def _tpl_fingerprint(path: str, req: str, resp: str, schema) -> str:
|
||||
"""模板形态指纹(2026-09-06 用户定夺):提交模板复用键=协议×能力×指纹。
|
||||
|
||||
为什么不能只按能力复用(实测事故):happyhorse r2v 纯图参考、wan3.0 r2v
|
||||
四模态参考(图/视频/音频)——按能力共享一份模板,后配模型的媒体模态被
|
||||
静默丢弃。也不能一模型一模板:wan3.1 若 API 与 wan3.0 完全一致应零新增。
|
||||
指纹=归一化 (path, request_template, response_template, param_schema) 的
|
||||
sha256 前 12 位:形态同→复用,形态异→各建各的,无需人工设计签名维度。
|
||||
|
||||
归一化规则:path 去空白;req/resp/schema JSON 往返(loads→dumps sorted
|
||||
紧凑分隔符)消除键序/空白差异;解析失败退化为去空白原文(保守不误判同形)。
|
||||
headers/method 不入指纹:headers 按协议统一,提交模板 method 恒 POST。
|
||||
"""
|
||||
def _canon(s):
|
||||
s = (s or "").strip()
|
||||
if not s:
|
||||
return ""
|
||||
try:
|
||||
return json.dumps(json.loads(s), sort_keys=True,
|
||||
ensure_ascii=False, separators=(",", ":"))
|
||||
except Exception:
|
||||
return re.sub(r"\s+", "", s)
|
||||
|
||||
if not isinstance(schema, str):
|
||||
schema = json.dumps(schema, sort_keys=True, ensure_ascii=False)
|
||||
blob = "|".join([(path or "").strip(),
|
||||
_canon(req), _canon(resp), _canon(schema)])
|
||||
return hashlib.sha256(blob.encode("utf-8")).hexdigest()[:12]
|
||||
|
||||
|
||||
def _schema_violations(raw):
|
||||
"""存量 param_schema 合规体检(2026-09-06 用户指正后补的自愈门禁)。
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user