fix(platform): 模板复用门禁加param_schema体检自愈——存量脏schema(未注册uitype/default/重复字段/结构常量)判死弃用重建,soft级(缺multiple)仅提示;防代码修好前端仍坏表单
This commit is contained in:
parent
d6cfaf6014
commit
9cc92dc84a
@ -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 用户指正:只准用注册过的类型)。
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user