@qingflow-tech/qingflow-cli 1.1.61-beta.3 → 1.1.61-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-cli/SKILL.md +2 -0
- package/skills/qingflow-cli/reference/builder/30-schema-fields.md +13 -0
- package/skills/qingflow-cli/reference/builder/70-portal.md +37 -16
- package/skills/qingflow-cli/reference/data/README.md +38 -0
- package/skills/qingflow-cli/reference/examples/portal/portal_apply_v1alpha1.example.json +100 -0
- package/skills/qingflow-cli/reference/examples/portal/portal_sections_all_types.example.json +6 -7
- package/src/qingflow_mcp/app_form_validator.py +20 -3
- package/src/qingflow_mcp/builder_facade/app_form.py +137 -17
- package/src/qingflow_mcp/builder_facade/models.py +164 -13
- package/src/qingflow_mcp/builder_facade/portals_schema.py +397 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +14 -1
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +49 -9
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +27 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_content_classification.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_content_extraction.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_content_generation.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_document_parsing.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_image_generation.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_image_recognition.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_machine_translation.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_text_summary.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +2 -2
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +27 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date_duration.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/embed_field.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/image_select.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/location.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +2 -2
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ocr.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +2 -2
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +27 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +29 -2
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +20 -7
- package/src/qingflow_mcp/builder_facade/schemas/portals/manifest.json +59 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/complete.json +1397 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/carousel.json +193 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/chart.json +643 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/favourite.json +203 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/filter.json +268 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/grid.json +265 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/link.json +131 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/task.json +199 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/text.json +123 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/view.json +196 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/summary.json +800 -0
- package/src/qingflow_mcp/builder_facade/service.py +1104 -71
- package/src/qingflow_mcp/cli/commands/builder.py +114 -5
- package/src/qingflow_mcp/public_surface.py +2 -0
- package/src/qingflow_mcp/server_app_builder.py +32 -1
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +73 -0
- package/src/qingflow_mcp/solution/executor.py +5 -2
- package/src/qingflow_mcp/tools/ai_builder_tools.py +110 -12
- package/src/qingflow_mcp/tools/portal_tools.py +4 -54
- package/src/qingflow_mcp/tools/portal_writer.py +59 -0
- package/src/qingflow_mcp/tools/solution_tools.py +2 -0
|
@@ -54,6 +54,24 @@ QUE_TYPE_TO_CANONICAL = {
|
|
|
54
54
|
22: "department",
|
|
55
55
|
25: "relation",
|
|
56
56
|
26: "code_block",
|
|
57
|
+
# 透传型字段(r10 读面 65 app 缺口):声明层 EmptyConfig,round-trip 经后端
|
|
58
|
+
# 模板保真(选项/技能配置列随模板透传);语义 = QuestionTypeEnum 14/15/17/23
|
|
59
|
+
# 与 Constants QUE_TYPE_EMBED_FIELD(27)、AI 系 30-37(AI_CONTENT_GENERATION
|
|
60
|
+
# /EXTRACTION/CLASSIFICATION、AI_MACHINE_TRANSLATION、AI_IMAGE_RECOGNITION
|
|
61
|
+
# /GENERATION、AI_DOCUMENT_PARSING、AI_TEXT_SUMMARY)
|
|
62
|
+
14: "date_duration",
|
|
63
|
+
15: "image_select",
|
|
64
|
+
17: "location",
|
|
65
|
+
23: "ocr",
|
|
66
|
+
27: "embed_field",
|
|
67
|
+
30: "ai_content_generation",
|
|
68
|
+
31: "ai_content_extraction",
|
|
69
|
+
32: "ai_content_classification",
|
|
70
|
+
33: "ai_machine_translation",
|
|
71
|
+
34: "ai_image_recognition",
|
|
72
|
+
35: "ai_image_generation",
|
|
73
|
+
36: "ai_document_parsing",
|
|
74
|
+
37: "ai_text_summary",
|
|
57
75
|
}
|
|
58
76
|
CANONICAL_TO_QUE_TYPE = {
|
|
59
77
|
"text": 2,
|
|
@@ -78,6 +96,20 @@ CANONICAL_TO_QUE_TYPE = {
|
|
|
78
96
|
"data_relation": 19,
|
|
79
97
|
"relation": 25,
|
|
80
98
|
"subtable": 18,
|
|
99
|
+
|
|
100
|
+
"date_duration": 14,
|
|
101
|
+
"image_select": 15,
|
|
102
|
+
"location": 17,
|
|
103
|
+
"ocr": 23,
|
|
104
|
+
"embed_field": 27,
|
|
105
|
+
"ai_content_generation": 30,
|
|
106
|
+
"ai_content_extraction": 31,
|
|
107
|
+
"ai_content_classification": 32,
|
|
108
|
+
"ai_machine_translation": 33,
|
|
109
|
+
"ai_image_recognition": 34,
|
|
110
|
+
"ai_image_generation": 35,
|
|
111
|
+
"ai_document_parsing": 36,
|
|
112
|
+
"ai_text_summary": 37,
|
|
81
113
|
}
|
|
82
114
|
SUBFIELD_TYPES = {
|
|
83
115
|
"text",
|
|
@@ -1227,11 +1259,20 @@ class AppFormNormalizer:
|
|
|
1227
1259
|
name = match.group("name").strip()
|
|
1228
1260
|
if name:
|
|
1229
1261
|
token = match.group("token").strip()
|
|
1262
|
+
# 名字两侧 strip 匹配:存量字段名带尾随空格(如 am4oh08v2401
|
|
1263
|
+
# 「答:Answer 」),模板引用名无空格——严格相等会丢 queId 锚定
|
|
1230
1264
|
matching_ids = [
|
|
1231
1265
|
que_id
|
|
1232
1266
|
for que_id, field_name in field_names.items()
|
|
1233
|
-
if field_name == name and _encrypt_question_id(que_id) == token
|
|
1267
|
+
if str(field_name).strip() == name and _encrypt_question_id(que_id) == token
|
|
1234
1268
|
]
|
|
1269
|
+
if not matching_ids:
|
|
1270
|
+
# 悬空引用(字段已删/改名,模板仍留旧引用):原文透传——占位符
|
|
1271
|
+
# 化会丢 token 无法复原(加密不可逆),后端写链对模板原样落库;
|
|
1272
|
+
# 不注册 input(声明无此输入,写回与 B0 模板逐字一致,
|
|
1273
|
+
# r10 实测 9gtommb3ng01 等 6 app「inputs[N].field could not
|
|
1274
|
+
# be resolved」即此形态)
|
|
1275
|
+
return match.group(0)
|
|
1235
1276
|
json_path_inputs.setdefault(
|
|
1236
1277
|
name,
|
|
1237
1278
|
{
|
|
@@ -1244,11 +1285,21 @@ class AppFormNormalizer:
|
|
|
1244
1285
|
|
|
1245
1286
|
request_values[request_key] = Q_LINKER_VALUE_REFERENCE_RE.sub(replace_reference, raw_value)
|
|
1246
1287
|
inputs.extend(json_path_inputs.values())
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1288
|
+
# URL 空容忍边界(#208③):后端写链仅 configMode=1(自定义)校验 url/method/
|
|
1289
|
+
# resultType 非空(FormQuestionSvcImpl.createQuestionRemoteLookUp:444 抛
|
|
1290
|
+
# REMOTE_LOOK_UP_SETTING_WRONG);configMode=2 插件模板 / 3 连接中心 /
|
|
1291
|
+
# 4 轻翼智能体的 URL 由模板侧提供,本表 url 空 是合法存量形态——非自定义
|
|
1292
|
+
# 模式放行空 URL 透传(写回走模板透传,round-trip 保真;r12 归因 27 app;
|
|
1293
|
+
# 自定义模式 url 空的 2 app 脓数据仍拒绝,入排除账本)。
|
|
1294
|
+
config_mode = _positive_int(config.get("configMode")) or 1
|
|
1295
|
+
if config_mode == 1:
|
|
1296
|
+
request_url = _required_text(
|
|
1297
|
+
request_values["url"],
|
|
1298
|
+
message="Q-Linker request URL is empty",
|
|
1299
|
+
details={"queId": source_id, "path": "/config/qLinkerBinding/request/url"},
|
|
1300
|
+
)
|
|
1301
|
+
else:
|
|
1302
|
+
request_url = str(request_values["url"] or "")
|
|
1252
1303
|
request = _without_none(
|
|
1253
1304
|
{
|
|
1254
1305
|
"url": request_url,
|
|
@@ -1771,7 +1822,9 @@ class AppFormCompiler:
|
|
|
1771
1822
|
if not isinstance(item, dict) or item.get("source") != "json_path":
|
|
1772
1823
|
continue
|
|
1773
1824
|
selected = _resolve_internal_selector(item.get("field") or {}, internal_fields, f"qLinkerBinding.inputs[{index}].field")
|
|
1774
|
-
|
|
1825
|
+
# 两侧 strip:模板引用名提取时已 strip(存量字段名带尾随空格,
|
|
1826
|
+
# 如 r10 am4oh08v2401「答:Answer 」),严格相等误报 key 不一致
|
|
1827
|
+
if str(item.get("key") or "").strip() != str(selected.get("name") or "").strip():
|
|
1775
1828
|
raise AppFormFailure(
|
|
1776
1829
|
"UNSUPPORTED_FIELD_CONFIG",
|
|
1777
1830
|
"json_path input key must equal the selected field name for lossless readback",
|
|
@@ -2449,7 +2502,18 @@ class AppFormCompiler:
|
|
|
2449
2502
|
def _question(self, profile: str, app_key: str, field: JSONObject, internal: JSONObject, temp_id: int, relations: list[JSONObject]) -> JSONObject:
|
|
2450
2503
|
field_type = _field_type(field)
|
|
2451
2504
|
que_type = CANONICAL_TO_QUE_TYPE[field_type]
|
|
2452
|
-
build_type =
|
|
2505
|
+
build_type = (
|
|
2506
|
+
"boolean" if field_type == "single_choice"
|
|
2507
|
+
else "text" if field_type in {
|
|
2508
|
+
"link", "data_relation", "rich_text",
|
|
2509
|
+
# 透传型新类型(r10 读面覆盖):build 走 text 兜底,类型/配置经模板保真
|
|
2510
|
+
"date_duration", "image_select", "location", "ocr", "embed_field",
|
|
2511
|
+
"ai_content_generation", "ai_content_extraction", "ai_content_classification",
|
|
2512
|
+
"ai_machine_translation", "ai_image_recognition", "ai_image_generation",
|
|
2513
|
+
"ai_document_parsing", "ai_text_summary",
|
|
2514
|
+
}
|
|
2515
|
+
else field_type
|
|
2516
|
+
)
|
|
2453
2517
|
built_internal = deepcopy(internal)
|
|
2454
2518
|
built_internal["type"] = build_type
|
|
2455
2519
|
if build_type == "subtable":
|
|
@@ -2821,7 +2885,21 @@ class AppFormCompiler:
|
|
|
2821
2885
|
if source.get("kind") == "app":
|
|
2822
2886
|
source_fields, _target_form = self._target_form_candidates(profile, str(source.get("appKey") or ""))
|
|
2823
2887
|
source_fields_are_backend = True
|
|
2824
|
-
|
|
2888
|
+
# 展示字段同样按后端语义透传:写链对 displayedQueId 无存在性校验
|
|
2889
|
+
# 原样落库(fillMatchRulesList 读链按全局 question∪draft_question
|
|
2890
|
+
# 解析)。解析不到时 queType=None → 写侧 origin_type 回退 current
|
|
2891
|
+
# 模板值(见 data_relation relatedQueId/queOriginType 组装)。
|
|
2892
|
+
try:
|
|
2893
|
+
target = _resolve_backend_selector(source.get("field") or {}, source_fields, "source.field")
|
|
2894
|
+
except AppFormFailure as error:
|
|
2895
|
+
if error.error_code != "INVALID_FIELD_SELECTOR" or _positive_int((source.get("field") or {}).get("queId")) is None:
|
|
2896
|
+
raise
|
|
2897
|
+
target = {
|
|
2898
|
+
"queId": _positive_int(source["field"].get("queId")),
|
|
2899
|
+
"queTitle": str(source["field"].get("name") or "").strip() or None,
|
|
2900
|
+
"queType": None,
|
|
2901
|
+
"_app_form_unresolved": True,
|
|
2902
|
+
}
|
|
2825
2903
|
relation.update(
|
|
2826
2904
|
{
|
|
2827
2905
|
"displayedQueId": target["queId"],
|
|
@@ -3070,7 +3148,23 @@ class AppFormCompiler:
|
|
|
3070
3148
|
location: str,
|
|
3071
3149
|
) -> JSONObject:
|
|
3072
3150
|
if source_fields_are_backend:
|
|
3073
|
-
|
|
3151
|
+
try:
|
|
3152
|
+
return _resolve_backend_selector(selector, source_fields, location)
|
|
3153
|
+
except AppFormFailure as error:
|
|
3154
|
+
# 目标表单候选集(草稿∪发布 formQues)解析不到、但 selector 带正
|
|
3155
|
+
# queId:按后端语义透传——读链 fillMatchRulesList 按 queId 查
|
|
3156
|
+
# question∪draft_question 全局表,查不到容忍置空(new Question());
|
|
3157
|
+
# 写链 updateFormInfo 对 matchRules/sortConfig 源 queId 无存在性
|
|
3158
|
+
# 校验、原样落库。存量里该形态 = 引用指向目标表单 GET 响应之外
|
|
3159
|
+
# 的字段(跨表/已删,如 r8 实测 00e0e0d8 筛选源),严格拒绝即
|
|
3160
|
+
# round-trip 断裂。queType 置 None 与后端「查不到置空」同构。
|
|
3161
|
+
if error.error_code == "INVALID_FIELD_SELECTOR" and _positive_int(selector.get("queId")) is not None:
|
|
3162
|
+
return {
|
|
3163
|
+
"queId": _positive_int(selector.get("queId")),
|
|
3164
|
+
"queTitle": str(selector.get("name") or "").strip() or None,
|
|
3165
|
+
"_app_form_unresolved": True,
|
|
3166
|
+
}
|
|
3167
|
+
raise
|
|
3074
3168
|
return _resolve_internal_selector(selector, source_fields, location)
|
|
3075
3169
|
|
|
3076
3170
|
@staticmethod
|
|
@@ -3088,7 +3182,11 @@ class AppFormCompiler:
|
|
|
3088
3182
|
return title
|
|
3089
3183
|
|
|
3090
3184
|
@staticmethod
|
|
3091
|
-
def _data_relation_que_type(field: JSONObject) -> int:
|
|
3185
|
+
def _data_relation_que_type(field: JSONObject) -> int | None:
|
|
3186
|
+
if field.get("_app_form_unresolved"):
|
|
3187
|
+
# 透传形态(目标表单候选集解析不到):后端 fillMatchRulesList 对查
|
|
3188
|
+
# 不到的 queId 置空 queType 继续出参,写链不校验——同构置 None。
|
|
3189
|
+
return None
|
|
3092
3190
|
backend_type = _positive_int(field.get("queType"))
|
|
3093
3191
|
if backend_type is not None:
|
|
3094
3192
|
return backend_type
|
|
@@ -3142,7 +3240,12 @@ class AppFormCompiler:
|
|
|
3142
3240
|
ordered_ops = common | {"greater_than", "greater_or_equal", "less_than", "less_or_equal", "dynamic_range", "decimal_range"}
|
|
3143
3241
|
organization_ops = common | {"belongs_to", "not_belongs_to", "subordinate_to", "not_subordinate_to", "subordinate_included", "subordinate_not_included", "contains_any", "contains_all"}
|
|
3144
3242
|
field_type = _canonical_backend_field_type(source)
|
|
3145
|
-
if
|
|
3243
|
+
if source.get("_app_form_unresolved"):
|
|
3244
|
+
# 透传形态:源字段类型不可知(后端读链查不到置空),后端写链对
|
|
3245
|
+
# operator×源类型组合无校验——跳过类型白名单,仅保留 operand 形状
|
|
3246
|
+
# 校验(下方),与「读面保留、写面透传」的存量形态处理同口径。
|
|
3247
|
+
allowed = common | text_ops | ordered_ops | organization_ops
|
|
3248
|
+
elif field_type in {"number", "amount", "date", "datetime"}:
|
|
3146
3249
|
# date/datetime 补 belongs_to/not_belongs_to:后端 judgeType=13(JUDGE_BELONG_TO)
|
|
3147
3250
|
# 对时间字段的存量语义是「属于时间范围」(judgeValues 形如
|
|
3148
3251
|
# "2025-01-30 14:20:59~2026-01-30 14:20:59"),与组织字段的属于语义
|
|
@@ -3150,6 +3253,12 @@ class AppFormCompiler:
|
|
|
3150
3253
|
allowed = ordered_ops | ({"belongs_to", "not_belongs_to"} if field_type in {"date", "datetime"} else set())
|
|
3151
3254
|
elif field_type in {"member", "department"}:
|
|
3152
3255
|
allowed = organization_ops
|
|
3256
|
+
elif field_type in {"text", "multi_select"}:
|
|
3257
|
+
# text/multi_select 补 belongs_to/not_belongs_to:judgeType=13 同为多态
|
|
3258
|
+
# 语义(选项「属于」集合),后端写链对 operator×源类型无校验——存量
|
|
3259
|
+
# 实证 r8:bjqj2bge2g02(multi_select 资产类型 belongs_to 当前字段)、
|
|
3260
|
+
# e4f644fe(text 客户等级 belongs_to literal ["BC"])。
|
|
3261
|
+
allowed = text_ops | {"belongs_to", "not_belongs_to"}
|
|
3153
3262
|
else:
|
|
3154
3263
|
allowed = text_ops
|
|
3155
3264
|
if operator not in allowed:
|
|
@@ -3163,7 +3272,10 @@ class AppFormCompiler:
|
|
|
3163
3272
|
if operator in EMPTY_OPERATORS:
|
|
3164
3273
|
if operand.get("kind") != "literal" or values not in ([], None):
|
|
3165
3274
|
raise AppFormFailure("UNSUPPORTED_FIELD_CONFIG", "empty-value operators require an empty literal operand", "CONTRACT_PRECHECK")
|
|
3166
|
-
elif operand.get("kind") == "literal" and not values:
|
|
3275
|
+
elif operand.get("kind") == "literal" and not values and operator not in {"subordinate_to", "not_subordinate_to"}:
|
|
3276
|
+
# subordinate 两算子放行空 literal:存量「设置后清空」形态(r13 实证
|
|
3277
|
+
# 41ce0ad8)——后端写链仅校验 dynamic_range(checkRelationMatchRules),
|
|
3278
|
+
# formula 对空 judgeValueDetails 渲染 [],读回解析对称,往返保真
|
|
3167
3279
|
raise AppFormFailure("UNSUPPORTED_FIELD_CONFIG", "literal data_relation operands must contain at least one value", "CONTRACT_PRECHECK")
|
|
3168
3280
|
|
|
3169
3281
|
@staticmethod
|
|
@@ -3279,11 +3391,19 @@ class AppFormCompiler:
|
|
|
3279
3391
|
merged: list[JSONObject] = []
|
|
3280
3392
|
seen: set[int] = set()
|
|
3281
3393
|
for item in [*_backend_question_candidates(draft_form), *_backend_question_candidates(published_form)]:
|
|
3282
|
-
|
|
3283
|
-
if
|
|
3284
|
-
|
|
3394
|
+
raw_id = item.get("queId")
|
|
3395
|
+
if isinstance(raw_id, int) and not isinstance(raw_id, bool):
|
|
3396
|
+
# queId 是字段身份,身份语义覆盖 0 与负数:0 是系统「编号」虚拟行
|
|
3397
|
+
# (que_type=8,GET 草稿/发布两态各带一行)、负数是系统行(-1 数据来源
|
|
3398
|
+
# 等,补导后物理在表)。只按正整数去重会让 queId=0 的「编号」在
|
|
3399
|
+
# 并集里出现两次,by_name 引用恒定命中 2 个候选 → AMBIGUOUS_
|
|
3400
|
+
# FIELD_SELECTOR(r13 103 app data_relation/relation 深层配置写回
|
|
3401
|
+
# 全量失败根因)。同 queId 即同字段,草稿优先(与正整数身份同
|
|
3402
|
+
# 语义);同名不同 queId 的真实用户字段仍双候选保留,by_name 歧义
|
|
3403
|
+
# 照常显式拒绝(存量脏形态不消歧,禁止静默取第一个)。
|
|
3404
|
+
if raw_id in seen:
|
|
3285
3405
|
continue
|
|
3286
|
-
seen.add(
|
|
3406
|
+
seen.add(raw_id)
|
|
3287
3407
|
merged.append(item)
|
|
3288
3408
|
return merged, (draft_form or published_form)
|
|
3289
3409
|
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
from enum import Enum
|
|
5
|
-
from typing import Any, Union
|
|
5
|
+
from typing import Any, Literal, Union
|
|
6
6
|
|
|
7
7
|
from pydantic import AliasChoices, Field, model_validator
|
|
8
8
|
|
|
@@ -2508,14 +2508,17 @@ class PortalInlineChartPatch(ChartUpsertPatch):
|
|
|
2508
2508
|
|
|
2509
2509
|
|
|
2510
2510
|
class PortalComponentPositionPatch(StrictModel):
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2511
|
+
# 后端语义(DashSvcImpl 读/写链):position 各坐标可为 null——生产数据中筛选组件
|
|
2512
|
+
# (type=6)的 x/y/cols/rows 原生为 NULL,读面透传 null、写链 setPosition 直存 null。
|
|
2513
|
+
# 因此显式 null 必须原样透传(round-trip 保真);仅键缺省时才回落布局默认值。
|
|
2514
|
+
pc_x: int | None = Field(default=0, validation_alias=AliasChoices("pc_x", "pcX", "x"))
|
|
2515
|
+
pc_y: int | None = Field(default=0, validation_alias=AliasChoices("pc_y", "pcY", "y"))
|
|
2516
|
+
pc_w: int | None = Field(default=12, validation_alias=AliasChoices("pc_w", "pcW", "w"))
|
|
2517
|
+
pc_h: int | None = Field(default=8, validation_alias=AliasChoices("pc_h", "pcH", "h"))
|
|
2518
|
+
mobile_x: int | None = Field(default=0, validation_alias=AliasChoices("mobile_x", "mobileX"))
|
|
2519
|
+
mobile_y: int | None = Field(default=0, validation_alias=AliasChoices("mobile_y", "mobileY"))
|
|
2520
|
+
mobile_w: int | None = Field(default=6, validation_alias=AliasChoices("mobile_w", "mobileW"))
|
|
2521
|
+
mobile_h: int | None = Field(default=8, validation_alias=AliasChoices("mobile_h", "mobileH"))
|
|
2519
2522
|
mobile_provided: bool = Field(default=False, exclude=True)
|
|
2520
2523
|
|
|
2521
2524
|
@model_validator(mode="before")
|
|
@@ -2554,6 +2557,10 @@ class PortalChartRefPatch(StrictModel):
|
|
|
2554
2557
|
app_key: str | None = None
|
|
2555
2558
|
chart_id: str | None = None
|
|
2556
2559
|
chart_name: str | None = None
|
|
2560
|
+
# 引用面判别:后端有两类图表组件——轻流报表(DASH_CONTENT_TYPE_CHART=1,chart 表唯一键
|
|
2561
|
+
# chartKey,写链 addChartInfos 按 chartKey 解析)与轻析 BI 报表(DASH_CONTENT_TYPE_BICHART=9,
|
|
2562
|
+
# bi_chart 表整型 chart_id,写链 addBiChartInfo 直存)。缺省 bi_chart(向后兼容既有声明)。
|
|
2563
|
+
ref_kind: Literal["chart", "bi_chart"] | None = None
|
|
2557
2564
|
|
|
2558
2565
|
@model_validator(mode="before")
|
|
2559
2566
|
@classmethod
|
|
@@ -2573,12 +2580,18 @@ class PortalChartRefPatch(StrictModel):
|
|
|
2573
2580
|
payload["chart_name"] = payload.pop("name")
|
|
2574
2581
|
if "appKey" in payload and "app_key" not in payload:
|
|
2575
2582
|
payload["app_key"] = payload.pop("appKey")
|
|
2583
|
+
if "refKind" in payload and "ref_kind" not in payload:
|
|
2584
|
+
payload["ref_kind"] = payload.pop("refKind")
|
|
2585
|
+
if "kind" in payload and "ref_kind" not in payload:
|
|
2586
|
+
payload["ref_kind"] = payload.pop("kind")
|
|
2576
2587
|
return payload
|
|
2577
2588
|
|
|
2578
2589
|
@model_validator(mode="after")
|
|
2579
2590
|
def validate_target(self) -> "PortalChartRefPatch":
|
|
2580
2591
|
if not (self.chart_id or self.chart_name):
|
|
2581
2592
|
raise ValueError("chart_ref requires chart_id or chart_name")
|
|
2593
|
+
if self.ref_kind == "chart" and not self.chart_id:
|
|
2594
|
+
raise ValueError("chart_ref with ref_kind=chart requires chart_id (chart 表唯一键 chartKey)")
|
|
2582
2595
|
if self.chart_name and not self.chart_id and not self.app_key:
|
|
2583
2596
|
raise ValueError("chart_ref.app_key is required when resolving by chart_name")
|
|
2584
2597
|
return self
|
|
@@ -2604,17 +2617,49 @@ class PortalViewRefPatch(StrictModel):
|
|
|
2604
2617
|
|
|
2605
2618
|
|
|
2606
2619
|
class PortalSectionPatch(StrictModel):
|
|
2607
|
-
title
|
|
2620
|
+
# title 语义=组件显示标题(draft_dash_chart.component_title / grid_config.gridTitle 等),
|
|
2621
|
+
# 生产存量存在空标题形态(显示标题开但标题空,读链回退视图名/图表名)——None=不设标题,
|
|
2622
|
+
# 编译对 None 不落库(后端透传 NULL),round-trip 保真;手写声明缺省 title=None(不设)。
|
|
2623
|
+
title: str | None = None
|
|
2608
2624
|
source_type: str = Field(validation_alias=AliasChoices("source_type", "sourceType"))
|
|
2609
2625
|
role: str | None = Field(default=None, validation_alias=AliasChoices("role", "zone", "section_role", "sectionRole"))
|
|
2626
|
+
# 显示标题开关(draft_dash_chart.being_show_title):提取侧读面必有值(True/False);
|
|
2627
|
+
# 声明缺省 None 时编译回落产品默认 True(与既有创建语义兼容)。
|
|
2628
|
+
show_title: bool | None = Field(
|
|
2629
|
+
default=None,
|
|
2630
|
+
validation_alias=AliasChoices("show_title", "showTitle", "being_show_title", "beingShowTitle"),
|
|
2631
|
+
)
|
|
2610
2632
|
position: PortalComponentPositionPatch | None = None
|
|
2611
|
-
dash_style_config: dict[str, Any] | None = Field(
|
|
2633
|
+
dash_style_config: dict[str, Any] | None = Field(
|
|
2634
|
+
default=None,
|
|
2635
|
+
validation_alias=AliasChoices("dash_style_config", "style_config", "dashStyleConfigBO", "dashStyleConfig"),
|
|
2636
|
+
)
|
|
2612
2637
|
config: dict[str, Any] = Field(default_factory=dict)
|
|
2613
2638
|
chart_ref: PortalChartRefPatch | None = None
|
|
2614
2639
|
chart: PortalInlineChartPatch | None = Field(default=None, validation_alias=AliasChoices("chart", "inline_chart", "inlineChart", "create_chart", "createChart"))
|
|
2615
2640
|
view_ref: PortalViewRefPatch | None = None
|
|
2641
|
+
view_display: dict[str, Any] | None = Field(
|
|
2642
|
+
default=None,
|
|
2643
|
+
validation_alias=AliasChoices("view_display", "viewDisplay"),
|
|
2644
|
+
)
|
|
2616
2645
|
text: str | None = None
|
|
2617
2646
|
url: str | None = None
|
|
2647
|
+
login_auth: bool | None = Field(
|
|
2648
|
+
default=None,
|
|
2649
|
+
validation_alias=AliasChoices("login_auth", "loginAuth", "being_login_auth", "beingLoginAuth"),
|
|
2650
|
+
)
|
|
2651
|
+
carousel: dict[str, Any] | None = Field(
|
|
2652
|
+
default=None,
|
|
2653
|
+
validation_alias=AliasChoices("carousel", "carousel_config", "carouselConfig"),
|
|
2654
|
+
)
|
|
2655
|
+
task: dict[str, Any] | None = Field(
|
|
2656
|
+
default=None,
|
|
2657
|
+
validation_alias=AliasChoices("task", "task_config", "taskConfig"),
|
|
2658
|
+
)
|
|
2659
|
+
favourite: dict[str, Any] | None = Field(
|
|
2660
|
+
default=None,
|
|
2661
|
+
validation_alias=AliasChoices("favourite", "favourite_config", "favouriteConfig"),
|
|
2662
|
+
)
|
|
2618
2663
|
|
|
2619
2664
|
@model_validator(mode="before")
|
|
2620
2665
|
@classmethod
|
|
@@ -2659,7 +2704,7 @@ class PortalSectionPatch(StrictModel):
|
|
|
2659
2704
|
|
|
2660
2705
|
@model_validator(mode="after")
|
|
2661
2706
|
def validate_shape(self) -> "PortalSectionPatch":
|
|
2662
|
-
supported = {"chart", "view", "grid", "filter", "text", "link"}
|
|
2707
|
+
supported = {"chart", "view", "grid", "filter", "text", "link", "carousel", "task", "favourite"}
|
|
2663
2708
|
if self.source_type not in supported:
|
|
2664
2709
|
raise ValueError(f"unsupported portal source_type '{self.source_type}'")
|
|
2665
2710
|
if self.source_type == "chart" and self.chart_ref is None and self.chart is None:
|
|
@@ -2670,6 +2715,12 @@ class PortalSectionPatch(StrictModel):
|
|
|
2670
2715
|
raise ValueError("text section requires text")
|
|
2671
2716
|
if self.source_type == "link" and self.url is None:
|
|
2672
2717
|
raise ValueError("link section requires url")
|
|
2718
|
+
if self.source_type == "carousel" and self.carousel is None:
|
|
2719
|
+
raise ValueError("carousel section requires carousel")
|
|
2720
|
+
if self.source_type == "task" and self.task is None:
|
|
2721
|
+
raise ValueError("task section requires task")
|
|
2722
|
+
if self.source_type == "favourite" and self.favourite is None:
|
|
2723
|
+
raise ValueError("favourite section requires favourite")
|
|
2673
2724
|
return self
|
|
2674
2725
|
|
|
2675
2726
|
|
|
@@ -2685,8 +2736,20 @@ class PortalApplyRequest(StrictModel):
|
|
|
2685
2736
|
icon: str | None = None
|
|
2686
2737
|
color: str | None = None
|
|
2687
2738
|
hide_copyright: bool | None = Field(default=None, validation_alias=AliasChoices("hide_copyright", "hideCopyright"))
|
|
2688
|
-
|
|
2739
|
+
global_config: dict[str, Any] | None = Field(
|
|
2740
|
+
default=None,
|
|
2741
|
+
validation_alias=AliasChoices("global_config", "globalConfig", "dash_global_config", "dashGlobalConfig"),
|
|
2742
|
+
)
|
|
2743
|
+
style_template_id: int | None = Field(
|
|
2744
|
+
default=None,
|
|
2745
|
+
validation_alias=AliasChoices("style_template_id", "styleTemplateId"),
|
|
2746
|
+
)
|
|
2747
|
+
overall_style: dict[str, Any] | None = Field(
|
|
2748
|
+
default=None,
|
|
2749
|
+
validation_alias=AliasChoices("overall_style", "overallStyle", "overallConfigBO"),
|
|
2750
|
+
)
|
|
2689
2751
|
config: dict[str, Any] = Field(default_factory=dict)
|
|
2752
|
+
schema_version: str | None = Field(default=None, validation_alias=AliasChoices("schema_version", "schemaVersion"))
|
|
2690
2753
|
|
|
2691
2754
|
@model_validator(mode="before")
|
|
2692
2755
|
@classmethod
|
|
@@ -2694,8 +2757,14 @@ class PortalApplyRequest(StrictModel):
|
|
|
2694
2757
|
if not isinstance(value, dict):
|
|
2695
2758
|
return value
|
|
2696
2759
|
payload = dict(value)
|
|
2760
|
+
payload.pop("schema_version", None)
|
|
2761
|
+
payload.pop("schemaVersion", None)
|
|
2697
2762
|
if "dash_name" not in payload and "dashName" not in payload and "name" in payload:
|
|
2698
2763
|
payload["dash_name"] = payload.pop("name")
|
|
2764
|
+
if "dash_global_config" in payload and "global_config" not in payload:
|
|
2765
|
+
payload["global_config"] = payload.pop("dash_global_config")
|
|
2766
|
+
elif "dashGlobalConfig" in payload and "global_config" not in payload:
|
|
2767
|
+
payload["global_config"] = payload.pop("dashGlobalConfig")
|
|
2699
2768
|
if "sections" not in payload and "pages" in payload:
|
|
2700
2769
|
pages = payload.pop("pages")
|
|
2701
2770
|
if not isinstance(pages, list):
|
|
@@ -2730,6 +2799,88 @@ class PortalApplyRequest(StrictModel):
|
|
|
2730
2799
|
return self
|
|
2731
2800
|
|
|
2732
2801
|
|
|
2802
|
+
def _public_portal_section_payload(section: PortalSectionPatch) -> dict[str, Any]:
|
|
2803
|
+
"""Dump one portal section with canonical naming for schema validation."""
|
|
2804
|
+
payload: dict[str, Any] = {
|
|
2805
|
+
"title": section.title,
|
|
2806
|
+
"source_type": section.source_type,
|
|
2807
|
+
}
|
|
2808
|
+
if section.role is not None:
|
|
2809
|
+
payload["role"] = section.role
|
|
2810
|
+
if section.position is not None:
|
|
2811
|
+
payload["position"] = {
|
|
2812
|
+
"pc": {
|
|
2813
|
+
"x": section.position.pc_x,
|
|
2814
|
+
"y": section.position.pc_y,
|
|
2815
|
+
"cols": section.position.pc_w,
|
|
2816
|
+
"rows": section.position.pc_h,
|
|
2817
|
+
},
|
|
2818
|
+
}
|
|
2819
|
+
if section.position.mobile_provided:
|
|
2820
|
+
payload["position"]["mobile"] = {
|
|
2821
|
+
"x": section.position.mobile_x,
|
|
2822
|
+
"y": section.position.mobile_y,
|
|
2823
|
+
"cols": section.position.mobile_w,
|
|
2824
|
+
"rows": section.position.mobile_h,
|
|
2825
|
+
}
|
|
2826
|
+
if section.dash_style_config is not None:
|
|
2827
|
+
payload["style_config"] = section.dash_style_config
|
|
2828
|
+
if section.chart_ref is not None:
|
|
2829
|
+
payload["chart_ref"] = section.chart_ref.model_dump(mode="json", exclude_none=True)
|
|
2830
|
+
if section.chart is not None:
|
|
2831
|
+
payload["chart"] = section.chart.model_dump(mode="json", exclude_none=True)
|
|
2832
|
+
if section.view_ref is not None:
|
|
2833
|
+
payload["view_ref"] = section.view_ref.model_dump(mode="json", exclude_none=True)
|
|
2834
|
+
if section.view_display is not None:
|
|
2835
|
+
payload["view_display"] = section.view_display
|
|
2836
|
+
if section.text is not None:
|
|
2837
|
+
payload["text"] = section.text
|
|
2838
|
+
if section.url is not None:
|
|
2839
|
+
payload["url"] = section.url
|
|
2840
|
+
if section.login_auth is not None:
|
|
2841
|
+
payload["login_auth"] = section.login_auth
|
|
2842
|
+
if section.carousel is not None:
|
|
2843
|
+
payload["carousel"] = section.carousel
|
|
2844
|
+
if section.task is not None:
|
|
2845
|
+
payload["task"] = section.task
|
|
2846
|
+
if section.favourite is not None:
|
|
2847
|
+
payload["favourite"] = section.favourite
|
|
2848
|
+
if section.config:
|
|
2849
|
+
payload["config"] = section.config
|
|
2850
|
+
return payload
|
|
2851
|
+
|
|
2852
|
+
|
|
2853
|
+
def public_portal_apply_payload(request: PortalApplyRequest) -> dict[str, Any]:
|
|
2854
|
+
"""Canonical-named portal declaration used for schema validation."""
|
|
2855
|
+
payload: dict[str, Any] = {}
|
|
2856
|
+
if request.dash_key:
|
|
2857
|
+
payload["dash_key"] = request.dash_key
|
|
2858
|
+
if request.dash_name is not None:
|
|
2859
|
+
payload["name"] = request.dash_name
|
|
2860
|
+
if request.package_tag_id is not None:
|
|
2861
|
+
payload["package_tag_id"] = request.package_tag_id
|
|
2862
|
+
payload["publish"] = request.publish
|
|
2863
|
+
if request.sections is not None:
|
|
2864
|
+
payload["sections"] = [_public_portal_section_payload(section) for section in request.sections]
|
|
2865
|
+
if request.layout_preset is not None:
|
|
2866
|
+
payload["layout_preset"] = request.layout_preset
|
|
2867
|
+
if request.visibility is not None:
|
|
2868
|
+
payload["visibility"] = request.visibility.model_dump(mode="json", exclude_none=True)
|
|
2869
|
+
if request.icon is not None:
|
|
2870
|
+
payload["icon"] = request.icon
|
|
2871
|
+
if request.color is not None:
|
|
2872
|
+
payload["color"] = request.color
|
|
2873
|
+
if request.hide_copyright is not None:
|
|
2874
|
+
payload["hide_copyright"] = request.hide_copyright
|
|
2875
|
+
if request.global_config is not None:
|
|
2876
|
+
payload["global_config"] = request.global_config
|
|
2877
|
+
if request.style_template_id is not None:
|
|
2878
|
+
payload["style_template_id"] = request.style_template_id
|
|
2879
|
+
if request.overall_style is not None:
|
|
2880
|
+
payload["overall_style"] = request.overall_style
|
|
2881
|
+
return payload
|
|
2882
|
+
|
|
2883
|
+
|
|
2733
2884
|
FieldPatch.model_rebuild()
|
|
2734
2885
|
FieldMutation.model_rebuild()
|
|
2735
2886
|
FieldUpdatePatch.model_rebuild()
|