@josephyan/qingflow-app-user-mcp 0.2.0-beta.2 → 0.2.0-beta.21
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 +12 -2
- package/npm/lib/runtime.mjs +37 -0
- package/npm/scripts/postinstall.mjs +5 -1
- package/package.json +3 -2
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-user/SKILL.md +230 -0
- package/skills/qingflow-app-user/agents/openai.yaml +4 -0
- package/skills/qingflow-app-user/references/data-gotchas.md +49 -0
- package/skills/qingflow-app-user/references/environments.md +63 -0
- package/skills/qingflow-app-user/references/record-patterns.md +110 -0
- package/skills/qingflow-app-user/references/workflow-usage.md +26 -0
- package/skills/qingflow-record-analysis/SKILL.md +253 -0
- package/skills/qingflow-record-analysis/agents/openai.yaml +4 -0
- package/skills/qingflow-record-analysis/references/analysis-gotchas.md +141 -0
- package/skills/qingflow-record-analysis/references/analysis-patterns.md +113 -0
- package/skills/qingflow-record-analysis/references/confidence-reporting.md +92 -0
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/builder_facade/models.py +294 -1
- package/src/qingflow_mcp/builder_facade/service.py +2727 -235
- package/src/qingflow_mcp/server.py +7 -5
- package/src/qingflow_mcp/server_app_builder.py +80 -4
- package/src/qingflow_mcp/server_app_user.py +8 -182
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +1 -1
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +21 -2
- package/src/qingflow_mcp/solution/executor.py +34 -7
- package/src/qingflow_mcp/tools/ai_builder_tools.py +1038 -30
- package/src/qingflow_mcp/tools/app_tools.py +1 -2
- package/src/qingflow_mcp/tools/approval_tools.py +357 -75
- package/src/qingflow_mcp/tools/directory_tools.py +158 -28
- package/src/qingflow_mcp/tools/record_tools.py +1954 -973
- package/src/qingflow_mcp/tools/task_tools.py +376 -225
- package/src/qingflow_mcp/tools/workflow_tools.py +78 -4
|
@@ -43,11 +43,30 @@ FIELD_TYPE_ALIASES: dict[str, PublicFieldType] = {
|
|
|
43
43
|
"departments": PublicFieldType.department,
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
FIELD_TYPE_ID_ALIASES: dict[int, PublicFieldType] = {
|
|
47
|
+
2: PublicFieldType.text,
|
|
48
|
+
3: PublicFieldType.long_text,
|
|
49
|
+
4: PublicFieldType.date,
|
|
50
|
+
5: PublicFieldType.member,
|
|
51
|
+
6: PublicFieldType.email,
|
|
52
|
+
7: PublicFieldType.phone,
|
|
53
|
+
8: PublicFieldType.number,
|
|
54
|
+
10: PublicFieldType.boolean,
|
|
55
|
+
11: PublicFieldType.single_select,
|
|
56
|
+
12: PublicFieldType.multi_select,
|
|
57
|
+
13: PublicFieldType.attachment,
|
|
58
|
+
18: PublicFieldType.subtable,
|
|
59
|
+
21: PublicFieldType.address,
|
|
60
|
+
22: PublicFieldType.department,
|
|
61
|
+
25: PublicFieldType.relation,
|
|
62
|
+
}
|
|
63
|
+
|
|
46
64
|
|
|
47
65
|
class PublicViewType(str, Enum):
|
|
48
66
|
table = "table"
|
|
49
67
|
card = "card"
|
|
50
68
|
board = "board"
|
|
69
|
+
gantt = "gantt"
|
|
51
70
|
|
|
52
71
|
|
|
53
72
|
class LayoutApplyMode(str, Enum):
|
|
@@ -69,6 +88,7 @@ class FlowPreset(str, Enum):
|
|
|
69
88
|
class ViewsPreset(str, Enum):
|
|
70
89
|
default_table = "default_table"
|
|
71
90
|
status_board = "status_board"
|
|
91
|
+
default_gantt = "default_gantt"
|
|
72
92
|
|
|
73
93
|
|
|
74
94
|
class PublicFlowNodeType(str, Enum):
|
|
@@ -82,6 +102,143 @@ class PublicFlowNodeType(str, Enum):
|
|
|
82
102
|
end = "end"
|
|
83
103
|
|
|
84
104
|
|
|
105
|
+
class FlowConditionOperator(str, Enum):
|
|
106
|
+
eq = "eq"
|
|
107
|
+
neq = "neq"
|
|
108
|
+
in_ = "in"
|
|
109
|
+
contains = "contains"
|
|
110
|
+
gte = "gte"
|
|
111
|
+
lte = "lte"
|
|
112
|
+
is_empty = "is_empty"
|
|
113
|
+
not_empty = "not_empty"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class ViewFilterOperator(str, Enum):
|
|
117
|
+
eq = "eq"
|
|
118
|
+
neq = "neq"
|
|
119
|
+
in_ = "in"
|
|
120
|
+
contains = "contains"
|
|
121
|
+
gte = "gte"
|
|
122
|
+
lte = "lte"
|
|
123
|
+
is_empty = "is_empty"
|
|
124
|
+
not_empty = "not_empty"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class FlowAssigneePatch(StrictModel):
|
|
128
|
+
role_ids: list[int] = Field(default_factory=list)
|
|
129
|
+
role_names: list[str] = Field(default_factory=list)
|
|
130
|
+
member_uids: list[int] = Field(default_factory=list)
|
|
131
|
+
member_emails: list[str] = Field(default_factory=list)
|
|
132
|
+
member_names: list[str] = Field(default_factory=list)
|
|
133
|
+
include_sub_departs: bool | None = None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class FlowNodePermissionsPatch(StrictModel):
|
|
137
|
+
editable_fields: list[str] = Field(default_factory=list)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class FlowConditionRulePatch(StrictModel):
|
|
141
|
+
field_name: str = Field(validation_alias=AliasChoices("field_name", "fieldName", "field", "name"))
|
|
142
|
+
operator: FlowConditionOperator = Field(validation_alias=AliasChoices("operator", "op"))
|
|
143
|
+
values: list[Any] = Field(default_factory=list)
|
|
144
|
+
|
|
145
|
+
@model_validator(mode="before")
|
|
146
|
+
@classmethod
|
|
147
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
148
|
+
if not isinstance(value, dict):
|
|
149
|
+
return value
|
|
150
|
+
payload = dict(value)
|
|
151
|
+
if "value" in payload and "values" not in payload:
|
|
152
|
+
payload["values"] = [payload.pop("value")]
|
|
153
|
+
raw_operator = payload.get("operator", payload.get("op"))
|
|
154
|
+
if isinstance(raw_operator, str):
|
|
155
|
+
normalized = raw_operator.strip().lower()
|
|
156
|
+
operator_aliases = {
|
|
157
|
+
"equals": FlowConditionOperator.eq.value,
|
|
158
|
+
"equal": FlowConditionOperator.eq.value,
|
|
159
|
+
"=": FlowConditionOperator.eq.value,
|
|
160
|
+
"not_equals": FlowConditionOperator.neq.value,
|
|
161
|
+
"not_equal": FlowConditionOperator.neq.value,
|
|
162
|
+
"!=": FlowConditionOperator.neq.value,
|
|
163
|
+
">=": FlowConditionOperator.gte.value,
|
|
164
|
+
"<=": FlowConditionOperator.lte.value,
|
|
165
|
+
"any_of": FlowConditionOperator.in_.value,
|
|
166
|
+
"one_of": FlowConditionOperator.in_.value,
|
|
167
|
+
"between_any": FlowConditionOperator.in_.value,
|
|
168
|
+
"empty": FlowConditionOperator.is_empty.value,
|
|
169
|
+
"is blank": FlowConditionOperator.is_empty.value,
|
|
170
|
+
"blank": FlowConditionOperator.is_empty.value,
|
|
171
|
+
"not_empty": FlowConditionOperator.not_empty.value,
|
|
172
|
+
"not blank": FlowConditionOperator.not_empty.value,
|
|
173
|
+
}
|
|
174
|
+
if normalized in operator_aliases:
|
|
175
|
+
payload["operator"] = operator_aliases[normalized]
|
|
176
|
+
elif "operator" not in payload:
|
|
177
|
+
payload["operator"] = normalized
|
|
178
|
+
payload.pop("op", None)
|
|
179
|
+
return payload
|
|
180
|
+
|
|
181
|
+
@model_validator(mode="after")
|
|
182
|
+
def validate_shape(self) -> "FlowConditionRulePatch":
|
|
183
|
+
if self.operator in {FlowConditionOperator.is_empty, FlowConditionOperator.not_empty}:
|
|
184
|
+
self.values = []
|
|
185
|
+
return self
|
|
186
|
+
if not self.values:
|
|
187
|
+
raise ValueError("condition rule requires values")
|
|
188
|
+
return self
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class ViewFilterRulePatch(StrictModel):
|
|
192
|
+
field_name: str = Field(validation_alias=AliasChoices("field_name", "fieldName", "field", "name"))
|
|
193
|
+
operator: ViewFilterOperator = Field(validation_alias=AliasChoices("operator", "op"))
|
|
194
|
+
values: list[Any] = Field(default_factory=list)
|
|
195
|
+
|
|
196
|
+
@model_validator(mode="before")
|
|
197
|
+
@classmethod
|
|
198
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
199
|
+
if not isinstance(value, dict):
|
|
200
|
+
return value
|
|
201
|
+
payload = dict(value)
|
|
202
|
+
if "value" in payload and "values" not in payload:
|
|
203
|
+
payload["values"] = [payload.pop("value")]
|
|
204
|
+
raw_operator = payload.get("operator", payload.get("op"))
|
|
205
|
+
if isinstance(raw_operator, str):
|
|
206
|
+
normalized = raw_operator.strip().lower()
|
|
207
|
+
operator_aliases = {
|
|
208
|
+
"equals": ViewFilterOperator.eq.value,
|
|
209
|
+
"equal": ViewFilterOperator.eq.value,
|
|
210
|
+
"=": ViewFilterOperator.eq.value,
|
|
211
|
+
"not_equals": ViewFilterOperator.neq.value,
|
|
212
|
+
"not_equal": ViewFilterOperator.neq.value,
|
|
213
|
+
"!=": ViewFilterOperator.neq.value,
|
|
214
|
+
">=": ViewFilterOperator.gte.value,
|
|
215
|
+
"<=": ViewFilterOperator.lte.value,
|
|
216
|
+
"any_of": ViewFilterOperator.in_.value,
|
|
217
|
+
"one_of": ViewFilterOperator.in_.value,
|
|
218
|
+
"between_any": ViewFilterOperator.in_.value,
|
|
219
|
+
"empty": ViewFilterOperator.is_empty.value,
|
|
220
|
+
"is blank": ViewFilterOperator.is_empty.value,
|
|
221
|
+
"blank": ViewFilterOperator.is_empty.value,
|
|
222
|
+
"not_empty": ViewFilterOperator.not_empty.value,
|
|
223
|
+
"not blank": ViewFilterOperator.not_empty.value,
|
|
224
|
+
}
|
|
225
|
+
if normalized in operator_aliases:
|
|
226
|
+
payload["operator"] = operator_aliases[normalized]
|
|
227
|
+
elif "operator" not in payload:
|
|
228
|
+
payload["operator"] = normalized
|
|
229
|
+
payload.pop("op", None)
|
|
230
|
+
return payload
|
|
231
|
+
|
|
232
|
+
@model_validator(mode="after")
|
|
233
|
+
def validate_shape(self) -> "ViewFilterRulePatch":
|
|
234
|
+
if self.operator in {ViewFilterOperator.is_empty, ViewFilterOperator.not_empty}:
|
|
235
|
+
self.values = []
|
|
236
|
+
return self
|
|
237
|
+
if not self.values:
|
|
238
|
+
raise ValueError("view filter rule requires values")
|
|
239
|
+
return self
|
|
240
|
+
|
|
241
|
+
|
|
85
242
|
class FieldSelector(StrictModel):
|
|
86
243
|
field_id: str | None = None
|
|
87
244
|
que_id: int | None = None
|
|
@@ -161,15 +318,65 @@ class FieldRemovePatch(StrictModel):
|
|
|
161
318
|
return self
|
|
162
319
|
|
|
163
320
|
|
|
321
|
+
def _coerce_layout_columns(value: Any) -> int | None:
|
|
322
|
+
if isinstance(value, bool):
|
|
323
|
+
return None
|
|
324
|
+
if isinstance(value, int):
|
|
325
|
+
return value if value > 0 else None
|
|
326
|
+
if isinstance(value, str):
|
|
327
|
+
stripped = value.strip()
|
|
328
|
+
if stripped.isdigit():
|
|
329
|
+
parsed = int(stripped)
|
|
330
|
+
return parsed if parsed > 0 else None
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _normalize_layout_rows(value: Any, *, columns: int | None = None) -> Any:
|
|
335
|
+
if not isinstance(value, list):
|
|
336
|
+
return value
|
|
337
|
+
if value and all(isinstance(item, list) for item in value):
|
|
338
|
+
return value
|
|
339
|
+
if not value:
|
|
340
|
+
return []
|
|
341
|
+
width = columns if columns and columns > 0 else None
|
|
342
|
+
if width is None:
|
|
343
|
+
return [list(value)]
|
|
344
|
+
return [list(value[index : index + width]) for index in range(0, len(value), width) if value[index : index + width]]
|
|
345
|
+
|
|
346
|
+
|
|
164
347
|
class LayoutSectionPatch(StrictModel):
|
|
165
348
|
section_id: str | None = Field(default=None, validation_alias=AliasChoices("section_id", "sectionId"))
|
|
166
349
|
title: str
|
|
167
|
-
rows: list[list[
|
|
350
|
+
rows: list[list[Any]] = Field(default_factory=list)
|
|
351
|
+
|
|
352
|
+
@model_validator(mode="before")
|
|
353
|
+
@classmethod
|
|
354
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
355
|
+
if not isinstance(value, dict):
|
|
356
|
+
return value
|
|
357
|
+
payload = dict(value)
|
|
358
|
+
if "name" in payload and "title" not in payload:
|
|
359
|
+
payload["title"] = payload.pop("name")
|
|
360
|
+
shorthand: Any | None = None
|
|
361
|
+
if "rows" not in payload:
|
|
362
|
+
if "fields" in payload:
|
|
363
|
+
shorthand = payload.pop("fields")
|
|
364
|
+
elif "field_ids" in payload:
|
|
365
|
+
shorthand = payload.pop("field_ids")
|
|
366
|
+
if shorthand is not None:
|
|
367
|
+
payload["rows"] = _normalize_layout_rows(
|
|
368
|
+
shorthand,
|
|
369
|
+
columns=_coerce_layout_columns(payload.pop("columns", None)),
|
|
370
|
+
)
|
|
371
|
+
return payload
|
|
168
372
|
|
|
169
373
|
@model_validator(mode="after")
|
|
170
374
|
def validate_rows(self) -> "LayoutSectionPatch":
|
|
171
375
|
if not self.rows:
|
|
172
376
|
raise ValueError("section rows must be a non-empty list")
|
|
377
|
+
for row in self.rows:
|
|
378
|
+
if not isinstance(row, list) or not row:
|
|
379
|
+
raise ValueError("section rows must be a non-empty list")
|
|
173
380
|
if not self.section_id:
|
|
174
381
|
self.section_id = _slugify_title(self.title)
|
|
175
382
|
return self
|
|
@@ -179,8 +386,54 @@ class FlowNodePatch(StrictModel):
|
|
|
179
386
|
id: str
|
|
180
387
|
type: PublicFlowNodeType
|
|
181
388
|
name: str
|
|
389
|
+
assignees: FlowAssigneePatch = Field(default_factory=FlowAssigneePatch)
|
|
390
|
+
permissions: FlowNodePermissionsPatch = Field(default_factory=FlowNodePermissionsPatch)
|
|
391
|
+
conditions: list[FlowConditionRulePatch] = Field(default_factory=list)
|
|
392
|
+
condition_groups: list[list[FlowConditionRulePatch]] = Field(default_factory=list)
|
|
182
393
|
config: dict[str, Any] = Field(default_factory=dict)
|
|
183
394
|
|
|
395
|
+
@model_validator(mode="before")
|
|
396
|
+
@classmethod
|
|
397
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
398
|
+
if not isinstance(value, dict):
|
|
399
|
+
return value
|
|
400
|
+
payload = dict(value)
|
|
401
|
+
assignees = dict(payload.get("assignees") or {})
|
|
402
|
+
permissions = dict(payload.get("permissions") or {})
|
|
403
|
+
|
|
404
|
+
for key in ("role_ids", "role_names", "member_uids", "member_emails", "member_names", "include_sub_departs"):
|
|
405
|
+
if key in payload and key not in assignees:
|
|
406
|
+
assignees[key] = payload.pop(key)
|
|
407
|
+
for key in ("editable_fields",):
|
|
408
|
+
if key in payload and key not in permissions:
|
|
409
|
+
permissions[key] = payload.pop(key)
|
|
410
|
+
if "filters" in payload and "conditions" not in payload:
|
|
411
|
+
payload["conditions"] = payload.pop("filters")
|
|
412
|
+
if "rules" in payload and "conditions" not in payload:
|
|
413
|
+
payload["conditions"] = payload.pop("rules")
|
|
414
|
+
if "conditionRules" in payload and "condition_groups" not in payload:
|
|
415
|
+
payload["condition_groups"] = payload.pop("conditionRules")
|
|
416
|
+
if "conditionGroups" in payload and "condition_groups" not in payload:
|
|
417
|
+
payload["condition_groups"] = payload.pop("conditionGroups")
|
|
418
|
+
if "owners" in payload and "member_names" not in assignees:
|
|
419
|
+
assignees["member_names"] = payload.pop("owners")
|
|
420
|
+
if "approvers" in payload and "role_names" not in assignees:
|
|
421
|
+
assignees["role_names"] = payload.pop("approvers")
|
|
422
|
+
if assignees:
|
|
423
|
+
payload["assignees"] = assignees
|
|
424
|
+
if permissions:
|
|
425
|
+
payload["permissions"] = permissions
|
|
426
|
+
return payload
|
|
427
|
+
|
|
428
|
+
@model_validator(mode="after")
|
|
429
|
+
def validate_branch_conditions(self) -> "FlowNodePatch":
|
|
430
|
+
if self.conditions:
|
|
431
|
+
self.condition_groups = [list(self.conditions), *self.condition_groups]
|
|
432
|
+
self.conditions = []
|
|
433
|
+
if self.type != PublicFlowNodeType.condition and self.condition_groups:
|
|
434
|
+
raise ValueError("condition_groups are only allowed on condition nodes")
|
|
435
|
+
return self
|
|
436
|
+
|
|
184
437
|
|
|
185
438
|
class FlowTransitionPatch(StrictModel):
|
|
186
439
|
source: str = Field(alias="from")
|
|
@@ -189,9 +442,14 @@ class FlowTransitionPatch(StrictModel):
|
|
|
189
442
|
|
|
190
443
|
class ViewUpsertPatch(StrictModel):
|
|
191
444
|
name: str
|
|
445
|
+
view_key: str | None = Field(default=None, validation_alias=AliasChoices("view_key", "viewKey"))
|
|
192
446
|
type: PublicViewType
|
|
193
447
|
columns: list[str] = Field(default_factory=list)
|
|
194
448
|
group_by: str | None = None
|
|
449
|
+
filters: list[ViewFilterRulePatch] = Field(default_factory=list)
|
|
450
|
+
start_field: str | None = Field(default=None, validation_alias=AliasChoices("start_field", "startField"))
|
|
451
|
+
end_field: str | None = Field(default=None, validation_alias=AliasChoices("end_field", "endField"))
|
|
452
|
+
title_field: str | None = Field(default=None, validation_alias=AliasChoices("title_field", "titleField"))
|
|
195
453
|
|
|
196
454
|
@model_validator(mode="before")
|
|
197
455
|
@classmethod
|
|
@@ -201,6 +459,14 @@ class ViewUpsertPatch(StrictModel):
|
|
|
201
459
|
payload = dict(value)
|
|
202
460
|
if "fields" in payload and "columns" not in payload:
|
|
203
461
|
payload["columns"] = payload.pop("fields")
|
|
462
|
+
if "column_names" in payload and "columns" not in payload:
|
|
463
|
+
payload["columns"] = payload.pop("column_names")
|
|
464
|
+
if "columnNames" in payload and "columns" not in payload:
|
|
465
|
+
payload["columns"] = payload.pop("columnNames")
|
|
466
|
+
if "filter_rules" in payload and "filters" not in payload:
|
|
467
|
+
payload["filters"] = payload.pop("filter_rules")
|
|
468
|
+
if "filterRules" in payload and "filters" not in payload:
|
|
469
|
+
payload["filters"] = payload.pop("filterRules")
|
|
204
470
|
raw_type = payload.get("type")
|
|
205
471
|
if isinstance(raw_type, str):
|
|
206
472
|
normalized = raw_type.strip().lower()
|
|
@@ -210,6 +476,8 @@ class ViewUpsertPatch(StrictModel):
|
|
|
210
476
|
payload["type"] = "card"
|
|
211
477
|
elif normalized == "kanban":
|
|
212
478
|
payload["type"] = "board"
|
|
479
|
+
elif normalized == "ganttview":
|
|
480
|
+
payload["type"] = "gantt"
|
|
213
481
|
return payload
|
|
214
482
|
|
|
215
483
|
@model_validator(mode="after")
|
|
@@ -218,6 +486,8 @@ class ViewUpsertPatch(StrictModel):
|
|
|
218
486
|
raise ValueError("table/card views require columns")
|
|
219
487
|
if self.type == PublicViewType.board and not self.group_by:
|
|
220
488
|
raise ValueError("board view requires group_by")
|
|
489
|
+
if self.type == PublicViewType.gantt and not (self.start_field and self.end_field):
|
|
490
|
+
raise ValueError("gantt view requires start_field and end_field")
|
|
221
491
|
return self
|
|
222
492
|
|
|
223
493
|
|
|
@@ -303,6 +573,22 @@ class FlowPlanRequest(StrictModel):
|
|
|
303
573
|
payload = dict(value)
|
|
304
574
|
if str(payload.get("mode") or "").strip().lower() == "overwrite":
|
|
305
575
|
payload["mode"] = "replace"
|
|
576
|
+
raw_preset = payload.get("preset")
|
|
577
|
+
if raw_preset is None and isinstance(payload.get("base_preset"), str):
|
|
578
|
+
raw_preset = payload["base_preset"]
|
|
579
|
+
payload["preset"] = raw_preset
|
|
580
|
+
if isinstance(raw_preset, str):
|
|
581
|
+
normalized_preset = raw_preset.strip().lower()
|
|
582
|
+
preset_aliases = {
|
|
583
|
+
"default_approval": FlowPreset.basic_approval.value,
|
|
584
|
+
"approval": FlowPreset.basic_approval.value,
|
|
585
|
+
"basic approval": FlowPreset.basic_approval.value,
|
|
586
|
+
"default_fill_then_approve": FlowPreset.basic_fill_then_approve.value,
|
|
587
|
+
"default-fill-then-approve": FlowPreset.basic_fill_then_approve.value,
|
|
588
|
+
"fill_then_approve": FlowPreset.basic_fill_then_approve.value,
|
|
589
|
+
}
|
|
590
|
+
if normalized_preset in preset_aliases:
|
|
591
|
+
payload["preset"] = preset_aliases[normalized_preset]
|
|
306
592
|
return payload
|
|
307
593
|
|
|
308
594
|
|
|
@@ -332,7 +618,14 @@ def _normalize_field_payload(value: Any) -> Any:
|
|
|
332
618
|
if not isinstance(value, dict):
|
|
333
619
|
return value
|
|
334
620
|
payload = dict(value)
|
|
621
|
+
if "fields" in payload and "subfields" not in payload:
|
|
622
|
+
payload["subfields"] = payload.pop("fields")
|
|
335
623
|
raw_type = payload.get("type")
|
|
624
|
+
if isinstance(raw_type, int):
|
|
625
|
+
normalized_from_id = FIELD_TYPE_ID_ALIASES.get(raw_type)
|
|
626
|
+
if normalized_from_id is not None:
|
|
627
|
+
payload["type"] = normalized_from_id.value
|
|
628
|
+
return payload
|
|
336
629
|
if isinstance(raw_type, str):
|
|
337
630
|
normalized = FIELD_TYPE_ALIASES.get(raw_type.strip().lower())
|
|
338
631
|
if normalized is not None:
|