fix(platform): param_schema 按 bricks 规范生成——uitype 白名单硬校验(禁未注册的textarea/number,前端会静默丢字段);媒体用专用控件image/audio/video+multiple:true(数组契约需多选);默认值字段名defaultvalue(default无人消费);枚举参数用code+data;剔除结构常量type并同名去重(旧版曾产出3个重复type);提取层新增field_enums

This commit is contained in:
yumoqing 2026-09-06 10:48:02 +08:00
parent 4573aeba3f
commit d6cfaf6014

View File

@ -331,6 +331,7 @@ _EXTRACT_PROMPT = """你是大模型 API 配置专家。通读下面这份模型
"request_headers": {"说明": "文档调用示例里除认证外的必需请求头,逐字照抄(如 DashScope 异步的 X-DashScope-Async: enable无则 null"},
"request_fields": ["请求体字段名列表"],
"request_example": {"说明": "文档调用示例(cURL/代码)中的完整请求体 JSON逐字照抄结构与各字段示例值不要修改文档无示例填 null"},
"field_enums": {"说明": "业务参数的可选值枚举(从文档参数说明表逐字照抄,如 {\"resolution\": [\"480P\",\"720P\",\"1080P\"], \"duration\": [3,5,10]})。文档没列可选值的参数不要编造;无则 null"},
"response_format": "响应格式说明content 字段路径 + usage 字段路径)",
"response_example": {"说明": "文档中的响应示例 JSON或结果字段说明逐字照抄无则 null"},
"async_steps": [{"purpose": "query|download", "path": "该步骤接口路径", "method": "GET|POST", "request_fields": ["请求字段名列表"], "response_format": "该步骤响应格式说明"}],
@ -1080,8 +1081,7 @@ def _gen_templates(protocol: str, capability: str, spec: dict) -> dict:
"completion_tokens": "usage.completion_tokens"},
}, ensure_ascii=False)
return {"path": chat_path, "headers": headers, "req": req, "resp": resp,
"param_schema": [{"name": "prompt", "label": "提示词",
"uitype": "textarea", "required": True}],
"param_schema": _build_param_schema(capability, [], []),
"media_params": [], "biz_params": [], "notes": notes}
example = spec.get("request_example")
@ -1099,15 +1099,9 @@ def _gen_templates(protocol: str, capability: str, spec: dict) -> dict:
"模板未生成媒体参数,请核对文档" % capability)
else:
resp = json.dumps({"content": "{{ text }}"}, ensure_ascii=False)
schema = [{"name": "prompt", "label": "提示词", "uitype": "textarea",
"required": capability not in ('embedding', 'rerank')}]
for mp in media_params:
schema.append({"name": mp, "label": "上传媒体(%s" % mp,
"uitype": "file", "required": True})
for b in biz_params:
schema.append({"name": b['name'], "label": b['name'],
"uitype": "number" if isinstance(b['example'], (int, float)) else "text",
"required": False, "default": b['example']})
# field_enums文档参数说明表里的可选值提取层逐字照抄驱动 code 下拉
enums = spec.get("field_enums") if isinstance(spec.get("field_enums"), dict) else None
schema = _build_param_schema(capability, media_params, biz_params, enums)
return {"path": chat_path, "headers": headers, "req": req, "resp": resp,
"param_schema": schema, "media_params": media_params,
"biz_params": biz_params, "notes": notes}
@ -1120,6 +1114,117 @@ _SKELETON_MARKERS = ('__from_doc__', '__note__', 'xxx_file',
'params.image_file)', 'params.video_file)', 'params.audio_file)')
def _biz_field_uitype(name, example, enums):
"""业务参数 → bricks uitype2026-09-06 用户指正:只准用注册过的类型)。
合法全集bricks/input.js Input.registerstr/hide/tel/date/int/float/check/
checkbox/email/file/image/code/text/password/audio/video/audiorecorder/
audiotext/search/group**textarea / number 均未注册**用了前端渲染不出来
规则有枚举 codeUiCode=selectdata:[{value,text}]
数值 int/float布尔 check其余 textUiText 自动增高多行
返回 (uitype, extra_opts)
"""
opts = {}
vals = None
if isinstance(enums, dict):
for k in enums:
if str(k).lower() == str(name).lower():
v = enums[k]
if isinstance(v, (list, tuple)) and v:
vals = list(v)
break
if vals:
opts['data'] = [{'value': v, 'text': ('%s' % v)} for v in vals]
return 'code', opts
if isinstance(example, bool):
return 'check', opts
if isinstance(example, int):
return 'int', opts
if isinstance(example, float):
return 'float', opts
# 文档没给示例值也没枚举:一律 text不是未注册的 textarea/number
return 'text', opts
# 媒体参数 → bricks 专用上传控件UiImage/UiAudio/UiVideo 均继承 UiFile
_MEDIA_UITYPE = {'image_files': 'image', 'audio_files': 'audio', 'video_files': 'video'}
# 媒体参数的中文标签(人话,不做机械拼接)
_MEDIA_LABEL = {'image_files': '参考图片', 'audio_files': '参考音频',
'video_files': '参考视频'}
# bricks 已注册的 uitype 全集input.js 末尾 Input.register 逐行抄录2026-09-06
# 白名单硬校验:写进 param_schema 的类型若不在其中,前端 Input.create 返回 null
# 只打一行 debug 日志input.js:1294字段静默消失——比报错更难查。
_BRICKS_UITYPES = frozenset([
'str', 'hide', 'tel', 'date', 'int', 'float', 'check', 'checkbox', 'email',
'file', 'image', 'code', 'text', 'password', 'audio', 'video',
'audiorecorder', 'audiotext', 'search', 'group',
])
# 文档结构常量值在模板里已逐字固化不该让用户填r2v 的 media[].type 等)
_STRUCT_CONST_FIELDS = frozenset(['type', 'role', 'format', 'mime_type'])
def _validate_uitypes(schema):
"""param_schema uitype 白名单校验(防未注册类型静默丢字段)。
另校验默认值字段名只准 defaultvalueinput.js 从不读 default
写成 default 的默认值前端不生效2026-09-06 实测踩坑
违规直接抛异常 apply 当场失败并报进 template_errors不落库坏 schema
"""
for f in schema:
ut = f.get('uitype')
if ut not in _BRICKS_UITYPES:
raise ValueError('param_schema 非法 uitype %r(字段 %s——bricks 已注册类型: %s'
% (ut, f.get('name'), '/'.join(sorted(_BRICKS_UITYPES))))
if 'default' in f:
raise ValueError('param_schema 字段 %s 用了 default——bricks 只认 defaultvalue'
% f.get('name'))
return True
def _build_param_schema(capability, media_params, biz_params, enums=None):
"""生成 bricks 合规的参数表单 schema。
合规要点2026-09-06对照 bricks/input.js + uapi/sql/minimax_h3_setup.sql 权威样例
- uitype 只取注册过的值 textarea/number
- 默认值字段名必须是 defaultvalueinput.js 只读它default 无人消费
- 数组媒体参数带 multiple:trueinput.js:426 决定值是数组还是单文件
r2v 最多 9 张参考图缺它 UI 只能选 1
- 媒体 uitype 用专用控件 image/audio/video非泛用 file带预览+相机
- 文档结构常量 media 元素的 type: "reference_image"不进表单
模板里已逐字固化让用户填只会出错旧版曾生成 3 个重复 type 字段
- 同名参数去重数组多元素曾各自产出一条同名业务参数
"""
schema = [{'name': 'prompt', 'label': '提示词', 'uitype': 'text',
'required': capability not in ('embedding', 'rerank')}]
seen = {'prompt'}
for mp in media_params:
if mp in seen:
continue
seen.add(mp)
schema.append({'name': mp, 'label': _MEDIA_LABEL.get(mp, mp),
'uitype': _MEDIA_UITYPE.get(mp, 'file'), 'required': True,
# 三数组契约UI 允许多选,值成数组;运行时兼容字符串/数组两形态
'multiple': True})
for b in biz_params:
name = b['name']
# 结构常量不进表单:媒体元素的 type 等已在模板里逐字固化
if name in seen or str(name).lower() in _STRUCT_CONST_FIELDS:
continue
seen.add(name)
uitype, extra = _biz_field_uitype(name, b.get('example'), enums)
f = {'name': name, 'label': b.get('label') or name,
'uitype': uitype, 'required': False}
f.update(extra)
if b.get('example') not in (None, ''):
f['defaultvalue'] = b['example']
schema.append(f)
_validate_uitypes(schema)
return schema
def _dry_render_check(req_tpl, capability, biz_params, media_params):
"""落库前干跑渲染StrictUndefined模板引用了运行时拿不到的变量当场报错。