fix(platform): 模板复用门禁加param_schema体检自愈——存量脏schema(未注册uitype/default/重复字段/结构常量)判死弃用重建,soft级(缺multiple)仅提示;防代码修好前端仍坏表单

This commit is contained in:
yumoqing 2026-09-06 13:25:37 +08:00
parent d6cfaf6014
commit 9cc92dc84a

View File

@ -582,7 +582,8 @@ async def _h_apply_llm_config(sor, params, ctx):
# 名称过滤必须加(2026-09-05 e2e 实测 bug):查询/下载步骤模板同库同协议 # 名称过滤必须加(2026-09-05 e2e 实测 bug):查询/下载步骤模板同库同协议
# 同能力,不加过滤会被「最新优先」误选为提交模板 → 提交打到查询 path → 404 # 同能力,不加过滤会被「最新优先」误选为提交模板 → 提交打到查询 path → 404
recs = await sor.sqlExe( recs = await sor.sqlExe(
"SELECT id, request_template, response_template FROM llm_api_profile " "SELECT id, request_template, response_template, param_schema "
"FROM llm_api_profile "
"WHERE protocol=${p}$ AND capability=${c}$ AND status='active' " "WHERE protocol=${p}$ AND capability=${c}$ AND status='active' "
"AND name NOT LIKE ${l1}$ AND name NOT LIKE ${l2}$ " "AND name NOT LIKE ${l1}$ AND name NOT LIKE ${l2}$ "
"ORDER BY created_at DESC LIMIT 1", "ORDER BY created_at DESC LIMIT 1",
@ -593,14 +594,23 @@ async def _h_apply_llm_config(sor, params, ctx):
pid_old = getattr(recs[0], "id", "") pid_old = getattr(recs[0], "id", "")
old_tpl = (getattr(recs[0], "request_template", "") or "") + \ old_tpl = (getattr(recs[0], "request_template", "") or "") + \
(getattr(recs[0], "response_template", "") or "") (getattr(recs[0], "response_template", "") or "")
if not any(mk in old_tpl for mk in _SKELETON_MARKERS): # schema 体检:存量脏 schema(未注册 uitype/default/重复字段/结构常量)判死重建,
# 否则代码修了、用户前端还是坏表单(自愈门禁,见 _schema_violations)
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):
profile_ids[cap] = pid_old profile_ids[cap] = pid_old
continue continue
await sor.sqlExe("UPDATE llm_api_profile SET status='deprecated' " await sor.sqlExe("UPDATE llm_api_profile SET status='deprecated' "
"WHERE id=${i}$", {"i": pid_old}) "WHERE id=${i}$", {"i": pid_old})
await sor.sqlExe("COMMIT", {}) await sor.sqlExe("COMMIT", {})
tpl_notes.append("旧骨架模板 %s(%s)含硬编码占位符,已弃用并按文档示例重建" if sv['hard']:
% (pid_old, cap)) tpl_notes.append("存量模板 %s(%s)param_schema 不合规,已弃用重建:%s"
% (pid_old, cap, ";".join(sv['hard'])))
else:
tpl_notes.append("旧骨架模板 %s(%s)含硬编码占位符,已弃用并按文档示例重建"
% (pid_old, cap))
tpl = _gen_templates(protocol, cap, spec) tpl = _gen_templates(protocol, cap, spec)
dry_err = _dry_render_check(tpl["req"], cap, tpl["biz_params"], tpl["media_params"]) dry_err = _dry_render_check(tpl["req"], cap, tpl["biz_params"], tpl["media_params"])
if dry_err: if dry_err:
@ -1114,6 +1124,60 @@ _SKELETON_MARKERS = ('__from_doc__', '__note__', 'xxx_file',
'params.image_file)', 'params.video_file)', 'params.audio_file)') 'params.image_file)', 'params.video_file)', 'params.audio_file)')
def _schema_violations(raw):
"""存量 param_schema 合规体检(2026-09-06 用户指正后补的自愈门禁)。
为什么必须有它:apply 的模板复用门禁原先只看 request_template 里的骨架标记,
完全不看 param_schema —— 于是「模板正常但 schema 是 textarea/number/default」的
存量 profile 会被永久复用(实测 happyhorse r2v/t2v/i2v 三个 active profile 全中招,
r2v 还带 3 个重复 type 字段)。代码修好了,用户在前端看到的仍是坏表单:
Input.create 遇未注册 uitype 返回 null 只打一行 debug(input.js:1294),字段静默消失。
所以复用前必须体检 schema,违规一律弃用重建,让存量数据跟着代码自愈。
分级(避免误杀仿权威样例的 profile):
hard = 会导致字段消失/撞名覆盖/默认值不生效 → 判死重建
(未注册 uitype、用 default、同名字段重复、结构常量入表单)
soft = 功能受限但不崩 → 只提示,不判死。典型:媒体数组缺 multiple:true
(input.js:426 决定值是数组还是 files[0];权威样例 minimax_h3_setup.sql:13
的 image_files 就没带 multiple,模板靠 for 循环兼容 str,故不能判死)
返回 {'hard': [...], 'soft': [...]}。
"""
out = {'hard': [], 'soft': []}
raw = (raw or "").strip()
if not raw:
return out
try:
schema = json.loads(raw)
except Exception as exc:
out['hard'].append("param_schema 不是合法 JSON(%s)" % exc)
return out
if not isinstance(schema, list):
out['hard'].append("param_schema 顶层应为数组")
return out
seen = set()
for f in schema:
if not isinstance(f, dict):
out['hard'].append("schema 元素非对象: %r" % (f,))
continue
nm = f.get("name")
ut = f.get("uitype")
if ut not in _BRICKS_UITYPES:
out['hard'].append("字段 %s 的 uitype %r 未注册" % (nm, ut))
if "default" in f:
out['hard'].append("字段 %s 用了 default(应为 defaultvalue)" % nm)
# 同名字段重复:前端 dom_element.id 撞名,后者覆盖前者
if nm in seen:
out['hard'].append("字段 %s 重复出现" % nm)
seen.add(nm)
# 结构常量不该出现在表单(值已固化在模板里)
if nm in _STRUCT_CONST_FIELDS:
out['hard'].append("结构常量字段 %s 不该让用户填" % nm)
# 媒体数组参数缺 multiple:UI 只能选 1 个(soft,权威样例也这样)
if nm in _MEDIA_UITYPE and not f.get("multiple"):
out['soft'].append("媒体数组字段 %s 缺 multiple:true(UI 只能选 1 个)" % nm)
return out
def _biz_field_uitype(name, example, enums): def _biz_field_uitype(name, example, enums):
"""业务参数 → bricks uitype(2026-09-06 用户指正:只准用注册过的类型)。 """业务参数 → bricks uitype(2026-09-06 用户指正:只准用注册过的类型)。