From d6cfaf6014df4e8d245d2a76b7e20d6b83f13cae Mon Sep 17 00:00:00 2001 From: yumoqing Date: Sun, 6 Sep 2026 10:48:02 +0800 Subject: [PATCH] =?UTF-8?q?fix(platform):=20param=5Fschema=20=E6=8C=89=20b?= =?UTF-8?q?ricks=20=E8=A7=84=E8=8C=83=E7=94=9F=E6=88=90=E2=80=94=E2=80=94u?= =?UTF-8?q?itype=20=E7=99=BD=E5=90=8D=E5=8D=95=E7=A1=AC=E6=A0=A1=E9=AA=8C(?= =?UTF-8?q?=E7=A6=81=E6=9C=AA=E6=B3=A8=E5=86=8C=E7=9A=84textarea/number,?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E4=BC=9A=E9=9D=99=E9=BB=98=E4=B8=A2=E5=AD=97?= =?UTF-8?q?=E6=AE=B5);=E5=AA=92=E4=BD=93=E7=94=A8=E4=B8=93=E7=94=A8?= =?UTF-8?q?=E6=8E=A7=E4=BB=B6image/audio/video+multiple:true(=E6=95=B0?= =?UTF-8?q?=E7=BB=84=E5=A5=91=E7=BA=A6=E9=9C=80=E5=A4=9A=E9=80=89);?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=80=BC=E5=AD=97=E6=AE=B5=E5=90=8Ddefaultva?= =?UTF-8?q?lue(default=E6=97=A0=E4=BA=BA=E6=B6=88=E8=B4=B9);=E6=9E=9A?= =?UTF-8?q?=E4=B8=BE=E5=8F=82=E6=95=B0=E7=94=A8code+data;=E5=89=94?= =?UTF-8?q?=E9=99=A4=E7=BB=93=E6=9E=84=E5=B8=B8=E9=87=8Ftype=E5=B9=B6?= =?UTF-8?q?=E5=90=8C=E5=90=8D=E5=8E=BB=E9=87=8D(=E6=97=A7=E7=89=88?= =?UTF-8?q?=E6=9B=BE=E4=BA=A7=E5=87=BA3=E4=B8=AA=E9=87=8D=E5=A4=8Dtype);?= =?UTF-8?q?=E6=8F=90=E5=8F=96=E5=B1=82=E6=96=B0=E5=A2=9Efield=5Fenums?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pipeline_platform/platform_ability.py | 127 +++++++++++++++++++++++--- 1 file changed, 116 insertions(+), 11 deletions(-) diff --git a/pipeline_platform/platform_ability.py b/pipeline_platform/platform_ability.py index fc08ffa..c5289d1 100644 --- a/pipeline_platform/platform_ability.py +++ b/pipeline_platform/platform_ability.py @@ -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):模板引用了运行时拿不到的变量当场报错。