first commit

This commit is contained in:
2026-09-02 11:44:52 +08:00
commit 0c8fa2653e
309 changed files with 57278 additions and 0 deletions
@@ -0,0 +1,311 @@
#!/usr/bin/env python3
"""
yida_form_inspector.py — 跨表单元数据巡查工具
为「自定义页面联动其他表单」场景提供元数据采集能力,让 AI 在生成 JSX 前就能
拿到目标应用下所有表单的 formUuid + 字段 ID,避免硬编码错误。
【调用前必读】references/yida-custom-page-codegen.md §9
跨表单联动 5 种模式(只读聚合 / 提交其他表 / Master-Detail / 多表 Dashboard /
跨表搬运)。本脚本只负责取字段元数据,JSX 写法以 codegen.md 为准——不要
看到 --help 就直接拼 JSX。
用法:
# 列出应用下全部表单(含 formUuid / formType / title
python yida_form_inspector.py --action list-forms --app APP_X
# 查看单张表单的字段(fieldId / dataType / label
python yida_form_inspector.py --action fields --app APP_X --form FORM-XXX
# 一次性导出多张表单的字段汇总(推荐:跨表整合页面用)
python yida_form_inspector.py --action bundle --app APP_X --forms FORM-A,FORM-B,FORM-C --output ./forms.json
# 直接生成可粘贴到 JSX 的 FIELDS 常量代码
python yida_form_inspector.py --action fields-snippet --app APP_X --form FORM-XXX
"""
from __future__ import annotations
import argparse
import json
import re
import subprocess
import sys
from pathlib import Path
from typing import Any
def _run_dws(args):
cmd = ["dws"] + args
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
except FileNotFoundError:
print("[FAIL] 找不到 'dws' 命令,请确认已安装并在 PATH 中", file=sys.stderr)
return None
except subprocess.TimeoutExpired:
print("[FAIL] dws 调用超时", file=sys.stderr)
return None
if result.returncode != 0:
err = result.stderr.strip() or result.stdout.strip()
print(f"[FAIL] dws 执行失败 (exit {result.returncode}): {err}", file=sys.stderr)
return None
try:
return json.loads(result.stdout)
except json.JSONDecodeError as exc:
print(f"[FAIL] 输出非 JSON: {exc}\n{result.stdout[:300]}", file=sys.stderr)
return None
def _list_forms(app, form_types=None):
args = ["yida", "app", "list-forms", "--app", app, "--format", "json"]
if form_types:
args.extend(["--form-types", form_types])
data = _run_dws(args)
if data is None:
return None
if isinstance(data, dict):
for key in ("forms", "data", "items", "list"):
if isinstance(data.get(key), list):
return data[key]
return [data]
if isinstance(data, list):
return data
return []
_FIELD_ID_VALUE_RE = re.compile(r'\b[A-Za-z]+Field_[A-Za-z0-9]+\b')
_COMPONENT_LIST_KEYS = ("components", "fields", "data", "items", "result", "children", "list")
def _walk_values(obj):
if isinstance(obj, dict):
yield obj
for value in obj.values():
yield from _walk_values(value)
elif isinstance(obj, list):
for item in obj:
yield from _walk_values(item)
def _find_nested_value(obj, keys):
if isinstance(obj, dict):
for key in keys:
value = obj.get(key)
if value not in (None, ""):
return value
for value in obj.values():
found = _find_nested_value(value, keys)
if found not in (None, ""):
return found
elif isinstance(obj, list):
for item in obj:
found = _find_nested_value(item, keys)
if found not in (None, ""):
return found
return None
def _normalize_i18n_label(label):
if isinstance(label, str):
raw = label.strip()
if raw.startswith("{") and raw.endswith("}"):
try:
parsed = json.loads(raw)
except json.JSONDecodeError:
return label
if isinstance(parsed, dict):
return (parsed.get("zh_CN") or parsed.get("zh-CN")
or parsed.get("text") or parsed.get("pureEn_US")
or parsed.get("en_US") or label)
return label
if isinstance(label, dict):
return (label.get("zh_CN") or label.get("zh-CN") or label.get("text")
or label.get("pureEn_US") or label.get("en_US") or "")
return label or ""
def _find_field_id(obj):
value = _find_nested_value(obj, ("fieldId", "fieldCode", "field_id", "fieldKey"))
if isinstance(value, str):
match = _FIELD_ID_VALUE_RE.search(value)
return match.group(0) if match else value
key_value = _find_nested_value(obj, ("key", "name", "id"))
if isinstance(key_value, str):
match = _FIELD_ID_VALUE_RE.search(key_value)
if match:
return match.group(0)
text = json.dumps(obj, ensure_ascii=False) if isinstance(obj, (dict, list)) else str(obj)
match = _FIELD_ID_VALUE_RE.search(text)
return match.group(0) if match else None
def _extract_component_items(data):
if isinstance(data, dict):
for key in _COMPONENT_LIST_KEYS:
value = data.get(key)
if isinstance(value, list):
return value
if isinstance(value, dict):
nested = _extract_component_items(value)
if nested:
return nested
if isinstance(data, list):
return data
found = []
seen = set()
for item in _walk_values(data):
if not isinstance(item, dict):
continue
field_id = _find_field_id(item)
if field_id and field_id not in seen:
seen.add(field_id)
found.append(item)
return found
def _components(app, form):
data = _run_dws(["yida", "form", "components", "--app", app,
"--form", form, "--format", "json"])
if data is None:
return None
return _extract_component_items(data)
def _normalize_field(comp):
field_id = _find_field_id(comp)
label = (_find_nested_value(comp, ("label", "title", "text", "displayName", "nameCn"))
or "")
label = _normalize_i18n_label(label)
return {
"fieldId": field_id,
"label": label,
"dataType": _find_nested_value(comp, ("dataType", "valueType")) or "",
"componentName": _find_nested_value(comp, ("componentName", "type", "component")) or "",
}
def _camel_safe(text, fallback):
if not text:
text = fallback
cleaned = re.sub(r"[^A-Za-z0-9]+", " ", text).strip()
if not cleaned:
return fallback
parts = cleaned.split()
return parts[0].lower() + "".join(p.capitalize() for p in parts[1:])
def _action_list_forms(args):
forms = _list_forms(args.app, args.form_types)
if forms is None:
return 1
summary = []
for f in forms:
summary.append({
"formUuid": f.get("formUuid") or f.get("formId") or f.get("uuid"),
"title": f.get("title") or f.get("name"),
"formType": f.get("formType") or f.get("type"),
"appType": f.get("appType") or args.app,
})
print(json.dumps({"ok": True, "app": args.app, "count": len(summary),
"forms": summary}, ensure_ascii=False, indent=2))
return 0
def _action_fields(args):
if not args.form:
print("错误: --action fields 需要 --form", file=sys.stderr)
return 1
comps = _components(args.app, args.form)
if comps is None:
return 1
fields = [_normalize_field(c) for c in comps]
print(json.dumps({"ok": True, "app": args.app, "form": args.form,
"count": len(fields), "fields": fields},
ensure_ascii=False, indent=2))
return 0
def _action_fields_snippet(args):
if not args.form:
print("错误: --action fields-snippet 需要 --form", file=sys.stderr)
return 1
comps = _components(args.app, args.form)
if comps is None:
return 1
lines = ["// 由 yida_form_inspector.py 自动生成",
f"// 表单: {args.form}",
"var FIELDS = {"]
used = set()
for idx, comp in enumerate(comps):
f = _normalize_field(comp)
if not f["fieldId"]:
continue
var_name = _camel_safe(f["label"], f"field{idx}")
base = var_name
n = 2
while var_name in used:
var_name = f"{base}{n}"
n += 1
used.add(var_name)
comment = f" // {f['label']} ({f['dataType']})" if f["label"] else ""
lines.append(f" {var_name}: '{f['fieldId']}',{comment}")
lines.append("};")
print("\n".join(lines))
return 0
def _action_bundle(args):
if not args.forms:
print("错误: --action bundle 需要 --forms FORM-A,FORM-B,...", file=sys.stderr)
return 1
form_ids = [s.strip() for s in args.forms.split(",") if s.strip()]
bundle = {"ok": True, "app": args.app, "forms": {}}
for fid in form_ids:
comps = _components(args.app, fid)
if comps is None:
bundle["forms"][fid] = {"ok": False, "error": "fetch_failed"}
continue
bundle["forms"][fid] = {
"ok": True,
"fields": [_normalize_field(c) for c in comps],
}
out = json.dumps(bundle, ensure_ascii=False, indent=2)
if args.output:
Path(args.output).write_text(out, encoding="utf-8")
print(f"[OK] 已写入 {args.output}{len(form_ids)} 张表单)")
else:
print(out)
return 0
def main():
ap = argparse.ArgumentParser(
description=(
"跨表单元数据巡查 / FIELDS 常量生成 "
"【调用前必读】references/yida-custom-page-codegen.md §9"
"(跨表单联动 5 种模式),不要只看 --help 就拼 JSX"
),
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
ap.add_argument("--action", required=True,
choices=["list-forms", "fields", "fields-snippet", "bundle"])
ap.add_argument("--app", required=True, help="应用编码 appType")
ap.add_argument("--form", help="单表 formUuidfields / fields-snippet 用)")
ap.add_argument("--forms", help="多表 formUuid 逗号分隔(bundle 用)")
ap.add_argument("--form-types",
help="list-forms 过滤:receipt / process / report / display 等")
ap.add_argument("--output", help="bundle 写入文件")
args = ap.parse_args()
handlers = {
"list-forms": _action_list_forms,
"fields": _action_fields,
"fields-snippet": _action_fields_snippet,
"bundle": _action_bundle,
}
return handlers[args.action](args)
if __name__ == "__main__":
sys.exit(main())