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:
parent
4573aeba3f
commit
d6cfaf6014
@ -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 uitype(2026-09-06 用户指正:只准用注册过的类型)。
|
||||
|
||||
合法全集(bricks/input.js Input.register):str/hide/tel/date/int/float/check/
|
||||
checkbox/email/file/image/code/text/password/audio/video/audiorecorder/
|
||||
audiotext/search/group。**textarea / number 均未注册**,用了前端渲染不出来。
|
||||
|
||||
规则:有枚举 → code(UiCode=select,data:[{value,text}]);
|
||||
数值 → int/float;布尔 → check;其余 → text(UiText 自动增高多行)。
|
||||
返回 (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 白名单校验(防未注册类型静默丢字段)。
|
||||
|
||||
另校验默认值字段名:只准 defaultvalue——input.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)
|
||||
- 默认值字段名必须是 defaultvalue(input.js 只读它,default 无人消费)
|
||||
- 数组媒体参数带 multiple:true(input.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):模板引用了运行时拿不到的变量当场报错。
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user