@josephyan/qingflow-cli 0.2.0-beta.73 → 0.2.0-beta.75
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 +2 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/qingflow_mcp/backend_client.py +102 -0
- package/src/qingflow_mcp/builder_facade/models.py +121 -0
- package/src/qingflow_mcp/builder_facade/service.py +906 -71
- package/src/qingflow_mcp/cli/commands/builder.py +33 -1
- package/src/qingflow_mcp/cli/commands/repo.py +80 -0
- package/src/qingflow_mcp/cli/commands/task.py +17 -0
- package/src/qingflow_mcp/cli/context.py +3 -0
- package/src/qingflow_mcp/config.py +147 -0
- package/src/qingflow_mcp/public_surface.py +231 -0
- package/src/qingflow_mcp/repository_store.py +71 -0
- package/src/qingflow_mcp/response_trim.py +17 -246
- package/src/qingflow_mcp/server_app_builder.py +26 -1
- package/src/qingflow_mcp/server_app_user.py +5 -1
- package/src/qingflow_mcp/tools/ai_builder_tools.py +145 -5
- package/src/qingflow_mcp/tools/record_tools.py +298 -25
- package/src/qingflow_mcp/tools/repository_dev_tools.py +533 -0
- package/src/qingflow_mcp/tools/task_context_tools.py +146 -7
|
@@ -57,6 +57,7 @@ from .models import (
|
|
|
57
57
|
PortalListResponse,
|
|
58
58
|
PortalReadSummaryResponse,
|
|
59
59
|
PortalSectionPatch,
|
|
60
|
+
PublicDepartmentScopeMode,
|
|
60
61
|
PublicFieldType,
|
|
61
62
|
PublicRelationMode,
|
|
62
63
|
PublicChartType,
|
|
@@ -379,6 +380,61 @@ class AiBuilderFacade:
|
|
|
379
380
|
"tag_icon": tag_icon,
|
|
380
381
|
}
|
|
381
382
|
|
|
383
|
+
def solution_install(
|
|
384
|
+
self,
|
|
385
|
+
*,
|
|
386
|
+
profile: str,
|
|
387
|
+
solution_key: str,
|
|
388
|
+
being_copy_data: bool = True,
|
|
389
|
+
solution_source: str = "solutionDetail",
|
|
390
|
+
) -> JSONObject:
|
|
391
|
+
requested_solution_key = str(solution_key or "").strip()
|
|
392
|
+
requested_solution_source = str(solution_source or "").strip() or "solutionDetail"
|
|
393
|
+
normalized_args = {
|
|
394
|
+
"solution_key": requested_solution_key,
|
|
395
|
+
"being_copy_data": bool(being_copy_data),
|
|
396
|
+
"solution_source": requested_solution_source,
|
|
397
|
+
}
|
|
398
|
+
if not requested_solution_key:
|
|
399
|
+
return _failed(
|
|
400
|
+
"SOLUTION_KEY_REQUIRED",
|
|
401
|
+
"solution_key is required",
|
|
402
|
+
normalized_args=normalized_args,
|
|
403
|
+
suggested_next_call=None,
|
|
404
|
+
)
|
|
405
|
+
try:
|
|
406
|
+
created = self.apps.app_create(
|
|
407
|
+
profile=profile,
|
|
408
|
+
payload={
|
|
409
|
+
"solutionKey": requested_solution_key,
|
|
410
|
+
"beingCopyData": bool(being_copy_data),
|
|
411
|
+
"solutionSource": requested_solution_source,
|
|
412
|
+
},
|
|
413
|
+
)
|
|
414
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
415
|
+
api_error = _coerce_api_error(error)
|
|
416
|
+
return _failed_from_api_error(
|
|
417
|
+
"SOLUTION_INSTALL_FAILED",
|
|
418
|
+
api_error,
|
|
419
|
+
normalized_args=normalized_args,
|
|
420
|
+
details={"solution_key": requested_solution_key},
|
|
421
|
+
suggested_next_call={"tool_name": "solution_install", "arguments": {"profile": profile, **normalized_args}},
|
|
422
|
+
)
|
|
423
|
+
return {
|
|
424
|
+
"status": "success",
|
|
425
|
+
"error_code": None,
|
|
426
|
+
"recoverable": False,
|
|
427
|
+
"message": "installed solution",
|
|
428
|
+
"normalized_args": normalized_args,
|
|
429
|
+
"missing_fields": [],
|
|
430
|
+
"allowed_values": {},
|
|
431
|
+
"details": {},
|
|
432
|
+
"request_id": created.get("request_id"),
|
|
433
|
+
"suggested_next_call": None,
|
|
434
|
+
"noop": False,
|
|
435
|
+
"verification": {},
|
|
436
|
+
}
|
|
437
|
+
|
|
382
438
|
def package_list(self, *, profile: str, trial_status: str = "all") -> JSONObject:
|
|
383
439
|
listed = self.packages.package_list(profile=profile, trial_status=trial_status, include_raw=False)
|
|
384
440
|
return {
|
|
@@ -1544,29 +1600,34 @@ class AiBuilderFacade:
|
|
|
1544
1600
|
transport_error=api_error,
|
|
1545
1601
|
),
|
|
1546
1602
|
)
|
|
1547
|
-
return finalize(response)
|
|
1603
|
+
return finalize(self._append_publish_result(profile=profile, app_key=app_key, publish=True, response=response))
|
|
1548
1604
|
button = _normalize_custom_button_detail(readback.get("result") if isinstance(readback.get("result"), dict) else {})
|
|
1549
1605
|
return finalize(
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1606
|
+
self._append_publish_result(
|
|
1607
|
+
profile=profile,
|
|
1608
|
+
app_key=app_key,
|
|
1609
|
+
publish=True,
|
|
1610
|
+
response={
|
|
1611
|
+
"status": "success",
|
|
1612
|
+
"error_code": None,
|
|
1613
|
+
"recoverable": False,
|
|
1614
|
+
"message": "created custom button",
|
|
1615
|
+
"normalized_args": normalized_args,
|
|
1616
|
+
"missing_fields": [],
|
|
1617
|
+
"allowed_values": {},
|
|
1618
|
+
"details": {},
|
|
1619
|
+
"request_id": None,
|
|
1620
|
+
"suggested_next_call": None,
|
|
1621
|
+
"noop": False,
|
|
1622
|
+
"warnings": [],
|
|
1623
|
+
"verification": {"custom_button_verified": True},
|
|
1624
|
+
"verified": True,
|
|
1625
|
+
"app_key": app_key,
|
|
1626
|
+
"button_id": button_id,
|
|
1627
|
+
"edit_version_no": edit_version_no,
|
|
1628
|
+
"button": button,
|
|
1629
|
+
},
|
|
1630
|
+
)
|
|
1570
1631
|
)
|
|
1571
1632
|
|
|
1572
1633
|
def app_custom_button_update(
|
|
@@ -1652,29 +1713,34 @@ class AiBuilderFacade:
|
|
|
1652
1713
|
transport_error=api_error,
|
|
1653
1714
|
),
|
|
1654
1715
|
)
|
|
1655
|
-
return finalize(response)
|
|
1716
|
+
return finalize(self._append_publish_result(profile=profile, app_key=app_key, publish=True, response=response))
|
|
1656
1717
|
button = _normalize_custom_button_detail(readback.get("result") if isinstance(readback.get("result"), dict) else {})
|
|
1657
1718
|
return finalize(
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1719
|
+
self._append_publish_result(
|
|
1720
|
+
profile=profile,
|
|
1721
|
+
app_key=app_key,
|
|
1722
|
+
publish=True,
|
|
1723
|
+
response={
|
|
1724
|
+
"status": "success",
|
|
1725
|
+
"error_code": None,
|
|
1726
|
+
"recoverable": False,
|
|
1727
|
+
"message": "updated custom button",
|
|
1728
|
+
"normalized_args": normalized_args,
|
|
1729
|
+
"missing_fields": [],
|
|
1730
|
+
"allowed_values": {},
|
|
1731
|
+
"details": {},
|
|
1732
|
+
"request_id": None,
|
|
1733
|
+
"suggested_next_call": None,
|
|
1734
|
+
"noop": False,
|
|
1735
|
+
"warnings": [],
|
|
1736
|
+
"verification": {"custom_button_verified": True},
|
|
1737
|
+
"verified": True,
|
|
1738
|
+
"app_key": app_key,
|
|
1739
|
+
"button_id": button_id,
|
|
1740
|
+
"edit_version_no": edit_version_no,
|
|
1741
|
+
"button": button,
|
|
1742
|
+
},
|
|
1743
|
+
)
|
|
1678
1744
|
)
|
|
1679
1745
|
|
|
1680
1746
|
def app_custom_button_delete(self, *, profile: str, app_key: str, button_id: int) -> JSONObject:
|
|
@@ -1693,27 +1759,42 @@ class AiBuilderFacade:
|
|
|
1693
1759
|
def finalize(response: JSONObject) -> JSONObject:
|
|
1694
1760
|
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
1695
1761
|
|
|
1762
|
+
edit_version_no, edit_context_error = self._ensure_app_edit_context(
|
|
1763
|
+
profile=profile,
|
|
1764
|
+
app_key=app_key,
|
|
1765
|
+
normalized_args=normalized_args,
|
|
1766
|
+
failure_code="CUSTOM_BUTTON_DELETE_FAILED",
|
|
1767
|
+
)
|
|
1768
|
+
if edit_context_error is not None:
|
|
1769
|
+
return finalize(edit_context_error)
|
|
1770
|
+
|
|
1696
1771
|
self.buttons.custom_button_delete(profile=profile, app_key=app_key, button_id=button_id)
|
|
1697
1772
|
return finalize(
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1773
|
+
self._append_publish_result(
|
|
1774
|
+
profile=profile,
|
|
1775
|
+
app_key=app_key,
|
|
1776
|
+
publish=True,
|
|
1777
|
+
response={
|
|
1778
|
+
"status": "success",
|
|
1779
|
+
"error_code": None,
|
|
1780
|
+
"recoverable": False,
|
|
1781
|
+
"message": "deleted custom button",
|
|
1782
|
+
"normalized_args": normalized_args,
|
|
1783
|
+
"missing_fields": [],
|
|
1784
|
+
"allowed_values": {},
|
|
1785
|
+
"details": {},
|
|
1786
|
+
"request_id": None,
|
|
1787
|
+
"suggested_next_call": None,
|
|
1788
|
+
"noop": False,
|
|
1789
|
+
"warnings": [],
|
|
1790
|
+
"verification": {"custom_button_deleted": True},
|
|
1791
|
+
"verified": True,
|
|
1792
|
+
"app_key": app_key,
|
|
1793
|
+
"button_id": button_id,
|
|
1794
|
+
"edit_version_no": edit_version_no,
|
|
1795
|
+
"deleted": True,
|
|
1796
|
+
},
|
|
1797
|
+
)
|
|
1717
1798
|
)
|
|
1718
1799
|
|
|
1719
1800
|
def _resolve_app_matches_in_visible_apps(
|
|
@@ -2088,6 +2169,280 @@ class AiBuilderFacade:
|
|
|
2088
2169
|
result["message"] = "read app field config"
|
|
2089
2170
|
return result
|
|
2090
2171
|
|
|
2172
|
+
def app_repair_code_blocks(
|
|
2173
|
+
self,
|
|
2174
|
+
*,
|
|
2175
|
+
profile: str,
|
|
2176
|
+
app_key: str,
|
|
2177
|
+
field: str | None = None,
|
|
2178
|
+
apply: bool = False,
|
|
2179
|
+
) -> JSONObject:
|
|
2180
|
+
try:
|
|
2181
|
+
state = self._load_base_schema_state(profile=profile, app_key=app_key)
|
|
2182
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
2183
|
+
api_error = _coerce_api_error(error)
|
|
2184
|
+
return _failed_from_api_error(
|
|
2185
|
+
"FIELDS_READ_FAILED",
|
|
2186
|
+
api_error,
|
|
2187
|
+
normalized_args={"app_key": app_key, "field": field, "apply": apply},
|
|
2188
|
+
details={"app_key": app_key},
|
|
2189
|
+
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
2190
|
+
)
|
|
2191
|
+
parsed_fields = cast(list[dict[str, Any]], state["parsed"].get("fields") or [])
|
|
2192
|
+
field_lookup = _build_public_field_lookup(parsed_fields)
|
|
2193
|
+
selected_fields: list[dict[str, Any]]
|
|
2194
|
+
requested_selector = str(field or "").strip()
|
|
2195
|
+
if requested_selector:
|
|
2196
|
+
try:
|
|
2197
|
+
selected_field = _resolve_public_field(requested_selector, field_lookup=field_lookup)
|
|
2198
|
+
except ValueError:
|
|
2199
|
+
return _failed(
|
|
2200
|
+
"FIELD_NOT_FOUND",
|
|
2201
|
+
"field selector did not match any existing field",
|
|
2202
|
+
normalized_args={"app_key": app_key, "field": requested_selector, "apply": apply},
|
|
2203
|
+
details={"app_key": app_key, "field": requested_selector},
|
|
2204
|
+
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
2205
|
+
)
|
|
2206
|
+
if str(selected_field.get("type") or "") != FieldType.code_block.value:
|
|
2207
|
+
return _failed(
|
|
2208
|
+
"CODE_BLOCK_FIELD_REQUIRED",
|
|
2209
|
+
"selected field is not a code_block field",
|
|
2210
|
+
normalized_args={"app_key": app_key, "field": requested_selector, "apply": apply},
|
|
2211
|
+
details={"app_key": app_key, "field": requested_selector, "field_type": selected_field.get("type")},
|
|
2212
|
+
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
2213
|
+
)
|
|
2214
|
+
selected_fields = [selected_field]
|
|
2215
|
+
else:
|
|
2216
|
+
selected_fields = [field_item for field_item in parsed_fields if str(field_item.get("type") or "") == FieldType.code_block.value]
|
|
2217
|
+
plans: list[dict[str, Any]] = []
|
|
2218
|
+
update_fields: list[FieldUpdatePatch] = []
|
|
2219
|
+
applied_fields: list[str] = []
|
|
2220
|
+
for code_block_field in selected_fields:
|
|
2221
|
+
field_name = _code_block_field_display_name(code_block_field)
|
|
2222
|
+
normalized_binding = _normalize_code_block_binding(code_block_field.get("code_block_binding"))
|
|
2223
|
+
normalized_config = _normalize_code_block_config(code_block_field.get("code_block_config") or code_block_field.get("config") or {}) or None
|
|
2224
|
+
raw_binding = code_block_field.get("code_block_binding") if isinstance(code_block_field.get("code_block_binding"), dict) else None
|
|
2225
|
+
preserved_output_targets = [
|
|
2226
|
+
str((output.get("target_field") or {}).get("name") or "").strip()
|
|
2227
|
+
for output in cast(list[dict[str, Any]], (normalized_binding or {}).get("outputs") or [])
|
|
2228
|
+
if isinstance(output.get("target_field"), dict) and str((output.get("target_field") or {}).get("name") or "").strip()
|
|
2229
|
+
]
|
|
2230
|
+
detected_issues: list[str] = []
|
|
2231
|
+
warnings: list[dict[str, Any]] = []
|
|
2232
|
+
normalized_code_preview: str | None = None
|
|
2233
|
+
normalized_binding_code = None
|
|
2234
|
+
normalized_config_code = None
|
|
2235
|
+
if normalized_binding is not None:
|
|
2236
|
+
raw_code = str(normalized_binding.get("code") or "")
|
|
2237
|
+
normalized_binding_code = _normalize_code_block_output_assignment(raw_code)
|
|
2238
|
+
if normalized_binding_code != raw_code:
|
|
2239
|
+
detected_issues.append("CONST_OR_LET_QF_OUTPUT_ASSIGNMENT")
|
|
2240
|
+
normalized_code_preview = normalized_binding_code
|
|
2241
|
+
if normalized_config is not None:
|
|
2242
|
+
raw_code_content = str(normalized_config.get("code_content") or "")
|
|
2243
|
+
normalized_config_code = _normalize_code_block_output_assignment(raw_code_content)
|
|
2244
|
+
if normalized_code_preview is None and normalized_config_code != raw_code_content:
|
|
2245
|
+
normalized_code_preview = normalized_config_code
|
|
2246
|
+
try:
|
|
2247
|
+
_validate_code_block_alias_config(
|
|
2248
|
+
field_name=field_name,
|
|
2249
|
+
raw_binding=raw_binding,
|
|
2250
|
+
normalized_config=normalized_config,
|
|
2251
|
+
)
|
|
2252
|
+
except _CodeBlockValidationError as error:
|
|
2253
|
+
detected_issues.append(error.error_code)
|
|
2254
|
+
warnings.append(_warning(error.error_code, error.message))
|
|
2255
|
+
has_outputs = bool((normalized_binding or {}).get("outputs")) or bool((normalized_config or {}).get("result_alias_path"))
|
|
2256
|
+
effective_code = ""
|
|
2257
|
+
if normalized_binding is not None:
|
|
2258
|
+
effective_code = str(normalized_binding_code if normalized_binding_code is not None else normalized_binding.get("code") or "")
|
|
2259
|
+
elif normalized_config is not None:
|
|
2260
|
+
code_content = str(normalized_config_code if normalized_config_code is not None else normalized_config.get("code_content") or "")
|
|
2261
|
+
effective_code = _strip_code_block_generated_input_prelude(code_content)
|
|
2262
|
+
if has_outputs and not _code_block_has_effective_output_assignment(effective_code):
|
|
2263
|
+
detected_issues.append("CODE_BLOCK_OUTPUT_ASSIGNMENT_MISSING")
|
|
2264
|
+
warnings.append(
|
|
2265
|
+
_warning(
|
|
2266
|
+
"CODE_BLOCK_OUTPUT_ASSIGNMENT_MISSING",
|
|
2267
|
+
"configured outputs require qf_output assignment before runtime writeback can succeed",
|
|
2268
|
+
)
|
|
2269
|
+
)
|
|
2270
|
+
target_relation_default_verified = True
|
|
2271
|
+
binding_has_target_bindings = False
|
|
2272
|
+
for output in cast(list[dict[str, Any]], (normalized_binding or {}).get("outputs") or []):
|
|
2273
|
+
target_payload = output.get("target_field")
|
|
2274
|
+
if not isinstance(target_payload, dict):
|
|
2275
|
+
continue
|
|
2276
|
+
if any(target_payload.values()):
|
|
2277
|
+
binding_has_target_bindings = True
|
|
2278
|
+
try:
|
|
2279
|
+
target_field = _resolve_field_selector_with_uniqueness(
|
|
2280
|
+
fields=parsed_fields,
|
|
2281
|
+
selector_payload=target_payload,
|
|
2282
|
+
location="repair.target_field",
|
|
2283
|
+
)
|
|
2284
|
+
except ValueError:
|
|
2285
|
+
target_relation_default_verified = False
|
|
2286
|
+
detected_issues.append("CODE_BLOCK_TARGET_DEFAULT_INVALID")
|
|
2287
|
+
warnings.append(_warning("CODE_BLOCK_TARGET_DEFAULT_INVALID", "bound output target could not be resolved during repair scan"))
|
|
2288
|
+
break
|
|
2289
|
+
if _coerce_any_int(target_field.get("default_type")) == DEFAULT_TYPE_RELATION:
|
|
2290
|
+
continue
|
|
2291
|
+
target_relation_default_verified = False
|
|
2292
|
+
detected_issues.append("CODE_BLOCK_TARGET_DEFAULT_INVALID")
|
|
2293
|
+
warnings.append(
|
|
2294
|
+
_warning(
|
|
2295
|
+
"CODE_BLOCK_TARGET_DEFAULT_INVALID",
|
|
2296
|
+
"bound output target is not stored as relation default and should be rebuilt through repair",
|
|
2297
|
+
target_field_name=_code_block_field_display_name(target_field),
|
|
2298
|
+
)
|
|
2299
|
+
)
|
|
2300
|
+
alias_issue_detected = "CODE_BLOCK_ALIAS_REQUIRED" in detected_issues
|
|
2301
|
+
assignment_missing_detected = "CODE_BLOCK_OUTPUT_ASSIGNMENT_MISSING" in detected_issues
|
|
2302
|
+
can_auto_repair = not alias_issue_detected and not assignment_missing_detected
|
|
2303
|
+
repair_mode = "none"
|
|
2304
|
+
patch_payload: dict[str, Any] | None = None
|
|
2305
|
+
safe_auto_fix = False
|
|
2306
|
+
if can_auto_repair and normalized_binding is not None and binding_has_target_bindings and (
|
|
2307
|
+
(normalized_binding_code is not None and normalized_binding_code != str(normalized_binding.get("code") or ""))
|
|
2308
|
+
or not target_relation_default_verified
|
|
2309
|
+
):
|
|
2310
|
+
patch_payload = {
|
|
2311
|
+
"selector": _field_selector_payload_for_field(code_block_field),
|
|
2312
|
+
"set": {
|
|
2313
|
+
"code_block_binding": _public_code_block_binding_payload(
|
|
2314
|
+
{**normalized_binding, "code": normalized_binding_code if normalized_binding_code is not None else str(normalized_binding.get("code") or "")}
|
|
2315
|
+
)
|
|
2316
|
+
},
|
|
2317
|
+
}
|
|
2318
|
+
repair_mode = "binding"
|
|
2319
|
+
safe_auto_fix = True
|
|
2320
|
+
elif (
|
|
2321
|
+
can_auto_repair
|
|
2322
|
+
and normalized_config is not None
|
|
2323
|
+
and normalized_config_code is not None
|
|
2324
|
+
and normalized_config_code != str(normalized_config.get("code_content") or "")
|
|
2325
|
+
):
|
|
2326
|
+
patch_payload = {
|
|
2327
|
+
"selector": _field_selector_payload_for_field(code_block_field),
|
|
2328
|
+
"set": {"code_block_config": {**normalized_config, "code_content": normalized_config_code}},
|
|
2329
|
+
}
|
|
2330
|
+
repair_mode = "config"
|
|
2331
|
+
safe_auto_fix = True
|
|
2332
|
+
if not detected_issues:
|
|
2333
|
+
detected_issues.append("NO_REPAIR_NEEDED")
|
|
2334
|
+
plan = {
|
|
2335
|
+
"field_name": field_name,
|
|
2336
|
+
"field_id": str(code_block_field.get("field_id") or "").strip() or None,
|
|
2337
|
+
"que_id": _coerce_positive_int(code_block_field.get("que_id")),
|
|
2338
|
+
"detected_issues": sorted(set(detected_issues)),
|
|
2339
|
+
"normalized_code_preview": normalized_code_preview,
|
|
2340
|
+
"would_update": bool(patch_payload and safe_auto_fix),
|
|
2341
|
+
"applied": False,
|
|
2342
|
+
"repair_mode": repair_mode,
|
|
2343
|
+
"preserved_output_targets": preserved_output_targets,
|
|
2344
|
+
"warnings": warnings,
|
|
2345
|
+
}
|
|
2346
|
+
if patch_payload and safe_auto_fix:
|
|
2347
|
+
update_fields.append(FieldUpdatePatch.model_validate(patch_payload))
|
|
2348
|
+
plans.append(plan)
|
|
2349
|
+
if not apply:
|
|
2350
|
+
return {
|
|
2351
|
+
"status": "success",
|
|
2352
|
+
"error_code": None,
|
|
2353
|
+
"recoverable": False,
|
|
2354
|
+
"message": "planned code block repair",
|
|
2355
|
+
"normalized_args": {"app_key": app_key, "field": requested_selector or None, "apply": False},
|
|
2356
|
+
"missing_fields": [],
|
|
2357
|
+
"allowed_values": {},
|
|
2358
|
+
"details": {},
|
|
2359
|
+
"request_id": None,
|
|
2360
|
+
"suggested_next_call": None,
|
|
2361
|
+
"noop": not bool(update_fields),
|
|
2362
|
+
"warnings": [],
|
|
2363
|
+
"verification": {
|
|
2364
|
+
"app_exists": True,
|
|
2365
|
+
"code_block_fields_scanned": len(plans),
|
|
2366
|
+
"would_update": bool(update_fields),
|
|
2367
|
+
},
|
|
2368
|
+
"verified": True,
|
|
2369
|
+
"app_key": app_key,
|
|
2370
|
+
"apply": False,
|
|
2371
|
+
"repair_plan": plans,
|
|
2372
|
+
"would_update_fields": [plan["field_name"] for plan in plans if plan["would_update"]],
|
|
2373
|
+
}
|
|
2374
|
+
if not update_fields:
|
|
2375
|
+
return {
|
|
2376
|
+
"status": "success",
|
|
2377
|
+
"error_code": None,
|
|
2378
|
+
"recoverable": False,
|
|
2379
|
+
"message": "no safe code block repairs were required",
|
|
2380
|
+
"normalized_args": {"app_key": app_key, "field": requested_selector or None, "apply": True},
|
|
2381
|
+
"missing_fields": [],
|
|
2382
|
+
"allowed_values": {},
|
|
2383
|
+
"details": {},
|
|
2384
|
+
"request_id": None,
|
|
2385
|
+
"suggested_next_call": None,
|
|
2386
|
+
"noop": True,
|
|
2387
|
+
"warnings": [],
|
|
2388
|
+
"verification": {
|
|
2389
|
+
"app_exists": True,
|
|
2390
|
+
"code_block_fields_scanned": len(plans),
|
|
2391
|
+
"would_update": False,
|
|
2392
|
+
"applied": False,
|
|
2393
|
+
},
|
|
2394
|
+
"verified": True,
|
|
2395
|
+
"app_key": app_key,
|
|
2396
|
+
"apply": True,
|
|
2397
|
+
"repair_plan": plans,
|
|
2398
|
+
"applied_fields": [],
|
|
2399
|
+
}
|
|
2400
|
+
apply_result = self.app_schema_apply(
|
|
2401
|
+
profile=profile,
|
|
2402
|
+
app_key=app_key,
|
|
2403
|
+
package_tag_id=None,
|
|
2404
|
+
app_name="",
|
|
2405
|
+
publish=True,
|
|
2406
|
+
add_fields=[],
|
|
2407
|
+
update_fields=update_fields,
|
|
2408
|
+
remove_fields=[],
|
|
2409
|
+
)
|
|
2410
|
+
if apply_result.get("status") == "failed":
|
|
2411
|
+
return apply_result
|
|
2412
|
+
try:
|
|
2413
|
+
reread = self._load_base_schema_state(profile=profile, app_key=app_key)
|
|
2414
|
+
verified_fields = cast(list[dict[str, Any]], reread["parsed"].get("fields") or [])
|
|
2415
|
+
verified_lookup = _build_public_field_lookup(verified_fields)
|
|
2416
|
+
for plan in plans:
|
|
2417
|
+
selector = plan["field_id"] or plan["field_name"] or plan["que_id"]
|
|
2418
|
+
if selector is None:
|
|
2419
|
+
continue
|
|
2420
|
+
try:
|
|
2421
|
+
verified_field = _resolve_public_field(selector, field_lookup=verified_lookup)
|
|
2422
|
+
except ValueError:
|
|
2423
|
+
continue
|
|
2424
|
+
verified_binding = _normalize_code_block_binding(verified_field.get("code_block_binding"))
|
|
2425
|
+
verified_config = _normalize_code_block_config(verified_field.get("code_block_config") or verified_field.get("config") or {}) or {}
|
|
2426
|
+
verified_code = str((verified_binding or {}).get("code") or "")
|
|
2427
|
+
verified_content = str(verified_config.get("code_content") or "")
|
|
2428
|
+
if "const qf_output =" not in verified_code and "let qf_output =" not in verified_code and "const qf_output =" not in verified_content and "let qf_output =" not in verified_content:
|
|
2429
|
+
if plan["would_update"]:
|
|
2430
|
+
plan["applied"] = True
|
|
2431
|
+
applied_fields.append(plan["field_name"])
|
|
2432
|
+
except (QingflowApiError, RuntimeError):
|
|
2433
|
+
pass
|
|
2434
|
+
apply_result["message"] = "repaired code block fields"
|
|
2435
|
+
apply_result["apply"] = True
|
|
2436
|
+
apply_result["repair_plan"] = plans
|
|
2437
|
+
apply_result["applied_fields"] = applied_fields
|
|
2438
|
+
apply_result["verification"] = {
|
|
2439
|
+
**(apply_result.get("verification") if isinstance(apply_result.get("verification"), dict) else {}),
|
|
2440
|
+
"code_block_fields_scanned": len(plans),
|
|
2441
|
+
"would_update": bool(update_fields),
|
|
2442
|
+
"applied": bool(applied_fields),
|
|
2443
|
+
}
|
|
2444
|
+
return apply_result
|
|
2445
|
+
|
|
2091
2446
|
def app_get_layout(self, *, profile: str, app_key: str) -> JSONObject:
|
|
2092
2447
|
result = self.app_read_layout_summary(profile=profile, app_key=app_key)
|
|
2093
2448
|
if result.get("status") == "success":
|
|
@@ -3264,6 +3619,7 @@ class AiBuilderFacade:
|
|
|
3264
3619
|
existing_field = next((field for field in current_fields if str(field.get("name") or "") == patch.name), None)
|
|
3265
3620
|
if existing_field is not None:
|
|
3266
3621
|
if _field_matches_patch(existing_field, patch):
|
|
3622
|
+
_merge_existing_field_with_patch(existing_field, patch)
|
|
3267
3623
|
continue
|
|
3268
3624
|
return _failed(
|
|
3269
3625
|
"DUPLICATE_FIELD",
|
|
@@ -3338,18 +3694,33 @@ class AiBuilderFacade:
|
|
|
3338
3694
|
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
3339
3695
|
)
|
|
3340
3696
|
|
|
3697
|
+
normalized_code_block_fields: list[str] = []
|
|
3341
3698
|
try:
|
|
3699
|
+
current_fields, normalized_code_block_fields = _normalize_and_validate_code_block_fields_for_write(fields=current_fields)
|
|
3342
3700
|
current_fields, compiled_question_relations = _compile_code_block_binding_fields(
|
|
3343
3701
|
fields=current_fields,
|
|
3344
3702
|
current_schema=schema_result,
|
|
3345
3703
|
)
|
|
3704
|
+
_ensure_code_block_targets_compiled_as_relation_defaults(fields=current_fields)
|
|
3705
|
+
except _CodeBlockValidationError as error:
|
|
3706
|
+
return _failed(
|
|
3707
|
+
error.error_code,
|
|
3708
|
+
error.message,
|
|
3709
|
+
normalized_args=normalized_args,
|
|
3710
|
+
details={"app_key": target.app_key, **error.details},
|
|
3711
|
+
suggested_next_call=_code_block_repair_suggested_next_call(
|
|
3712
|
+
profile=profile,
|
|
3713
|
+
app_key=target.app_key,
|
|
3714
|
+
field_name=str(error.details.get("field_name") or "").strip() or None,
|
|
3715
|
+
),
|
|
3716
|
+
)
|
|
3346
3717
|
except ValueError as error:
|
|
3347
3718
|
return _failed(
|
|
3348
3719
|
"CODE_BLOCK_BINDING_INVALID",
|
|
3349
3720
|
str(error),
|
|
3350
3721
|
normalized_args=normalized_args,
|
|
3351
3722
|
details={"app_key": target.app_key},
|
|
3352
|
-
suggested_next_call=
|
|
3723
|
+
suggested_next_call=_code_block_repair_suggested_next_call(profile=profile, app_key=target.app_key),
|
|
3353
3724
|
)
|
|
3354
3725
|
|
|
3355
3726
|
q_linker_schema_context = deepcopy(schema_result)
|
|
@@ -3375,8 +3746,19 @@ class AiBuilderFacade:
|
|
|
3375
3746
|
if not relation_limit_verified
|
|
3376
3747
|
else []
|
|
3377
3748
|
)
|
|
3749
|
+
code_block_normalization_warnings = (
|
|
3750
|
+
[
|
|
3751
|
+
_warning(
|
|
3752
|
+
"CODE_BLOCK_OUTPUT_ASSIGNMENT_NORMALIZED",
|
|
3753
|
+
"normalized code block qf_output assignment before schema write",
|
|
3754
|
+
field_names=normalized_code_block_fields,
|
|
3755
|
+
)
|
|
3756
|
+
]
|
|
3757
|
+
if normalized_code_block_fields
|
|
3758
|
+
else []
|
|
3759
|
+
)
|
|
3378
3760
|
|
|
3379
|
-
if not added and not updated and not removed and not bool(resolved.get("created")):
|
|
3761
|
+
if not added and not updated and not removed and not normalized_code_block_fields and not bool(resolved.get("created")):
|
|
3380
3762
|
tag_ids_after = _coerce_int_list((self.apps.app_get_base(profile=profile, app_key=target.app_key, include_raw=True).get("result") or {}).get("tagIds"))
|
|
3381
3763
|
package_attached = None if package_tag_id is None else package_tag_id in tag_ids_after
|
|
3382
3764
|
response = {
|
|
@@ -3391,7 +3773,7 @@ class AiBuilderFacade:
|
|
|
3391
3773
|
"request_id": None,
|
|
3392
3774
|
"suggested_next_call": None if package_attached is not False else {"tool_name": "package_attach_app", "arguments": {"profile": profile, "tag_id": package_tag_id, "app_key": target.app_key}},
|
|
3393
3775
|
"noop": not bool(visual_result.get("updated")),
|
|
3394
|
-
"warnings": relation_warnings,
|
|
3776
|
+
"warnings": relation_warnings + code_block_normalization_warnings,
|
|
3395
3777
|
"verification": {
|
|
3396
3778
|
"fields_verified": True,
|
|
3397
3779
|
"relation_field_limit_verified": relation_limit_verified,
|
|
@@ -3407,6 +3789,9 @@ class AiBuilderFacade:
|
|
|
3407
3789
|
"package_attached": package_attached,
|
|
3408
3790
|
}
|
|
3409
3791
|
response["details"]["relation_field_count"] = relation_field_count
|
|
3792
|
+
if normalized_code_block_fields:
|
|
3793
|
+
response["normalized_code_block_output_assignment"] = True
|
|
3794
|
+
response["normalized_code_block_fields"] = normalized_code_block_fields
|
|
3410
3795
|
response = _apply_permission_outcomes(response, relation_permission_outcome)
|
|
3411
3796
|
return finalize(self._append_publish_result(profile=profile, app_key=target.app_key, publish=publish, response=response))
|
|
3412
3797
|
|
|
@@ -3473,16 +3858,30 @@ class AiBuilderFacade:
|
|
|
3473
3858
|
rebound_layout = cast(dict[str, Any], rebound_parsed.get("layout") or {"root_rows": [], "sections": []})
|
|
3474
3859
|
_overlay_code_block_binding_fields(target_fields=rebound_fields, source_fields=current_fields)
|
|
3475
3860
|
_overlay_q_linker_binding_fields(target_fields=rebound_fields, source_fields=current_fields)
|
|
3861
|
+
rebound_fields, _normalized_rebound_code_blocks = _normalize_and_validate_code_block_fields_for_write(fields=rebound_fields)
|
|
3476
3862
|
rebound_fields, compiled_question_relations = _compile_code_block_binding_fields(
|
|
3477
3863
|
fields=rebound_fields,
|
|
3478
3864
|
current_schema=rebound_schema,
|
|
3479
3865
|
)
|
|
3866
|
+
_ensure_code_block_targets_compiled_as_relation_defaults(fields=rebound_fields)
|
|
3480
3867
|
rebound_q_linker_schema = deepcopy(rebound_schema)
|
|
3481
3868
|
rebound_q_linker_schema["questionRelations"] = deepcopy(compiled_question_relations)
|
|
3482
3869
|
rebound_fields, compiled_question_relations = _compile_q_linker_binding_fields(
|
|
3483
3870
|
fields=rebound_fields,
|
|
3484
3871
|
current_schema=rebound_q_linker_schema,
|
|
3485
3872
|
)
|
|
3873
|
+
except _CodeBlockValidationError as error:
|
|
3874
|
+
return _failed(
|
|
3875
|
+
error.error_code,
|
|
3876
|
+
error.message,
|
|
3877
|
+
normalized_args=normalized_args,
|
|
3878
|
+
details={"app_key": target.app_key, **error.details},
|
|
3879
|
+
suggested_next_call=_code_block_repair_suggested_next_call(
|
|
3880
|
+
profile=profile,
|
|
3881
|
+
app_key=target.app_key,
|
|
3882
|
+
field_name=str(error.details.get("field_name") or "").strip() or None,
|
|
3883
|
+
),
|
|
3884
|
+
)
|
|
3486
3885
|
except ValueError as error:
|
|
3487
3886
|
return _failed(
|
|
3488
3887
|
"Q_LINKER_BINDING_INVALID" if "q_linker" in str(error) else "CODE_BLOCK_BINDING_INVALID",
|
|
@@ -3544,7 +3943,7 @@ class AiBuilderFacade:
|
|
|
3544
3943
|
"request_id": None,
|
|
3545
3944
|
"suggested_next_call": None,
|
|
3546
3945
|
"noop": False,
|
|
3547
|
-
"warnings": relation_warnings,
|
|
3946
|
+
"warnings": relation_warnings + code_block_normalization_warnings,
|
|
3548
3947
|
"verification": {
|
|
3549
3948
|
"fields_verified": False,
|
|
3550
3949
|
"package_attached": None,
|
|
@@ -3565,6 +3964,9 @@ class AiBuilderFacade:
|
|
|
3565
3964
|
"package_attached": None,
|
|
3566
3965
|
}
|
|
3567
3966
|
response["details"]["relation_field_count"] = relation_field_count
|
|
3967
|
+
if normalized_code_block_fields:
|
|
3968
|
+
response["normalized_code_block_output_assignment"] = True
|
|
3969
|
+
response["normalized_code_block_fields"] = normalized_code_block_fields
|
|
3568
3970
|
if schema_readback_delayed:
|
|
3569
3971
|
response["verification"]["schema_readback_delayed"] = True
|
|
3570
3972
|
response = _apply_permission_outcomes(response, relation_permission_outcome)
|
|
@@ -7770,6 +8172,133 @@ def _normalize_relation_mode(value: Any) -> str:
|
|
|
7770
8172
|
return _relation_mode_from_optional_data_num(value)
|
|
7771
8173
|
|
|
7772
8174
|
|
|
8175
|
+
def _normalize_department_scope_mode(value: Any) -> str | None:
|
|
8176
|
+
if value is None:
|
|
8177
|
+
return None
|
|
8178
|
+
if isinstance(value, int):
|
|
8179
|
+
if value == 1:
|
|
8180
|
+
return PublicDepartmentScopeMode.all.value
|
|
8181
|
+
if value == 2:
|
|
8182
|
+
return PublicDepartmentScopeMode.custom.value
|
|
8183
|
+
return None
|
|
8184
|
+
normalized = str(value).strip().lower()
|
|
8185
|
+
if normalized in {"all", "workspace_all", "workspace-all", "default", "default_all", "default-all", "1"}:
|
|
8186
|
+
return PublicDepartmentScopeMode.all.value
|
|
8187
|
+
if normalized in {"custom", "explicit", "selected", "2"}:
|
|
8188
|
+
return PublicDepartmentScopeMode.custom.value
|
|
8189
|
+
return normalized or None
|
|
8190
|
+
|
|
8191
|
+
|
|
8192
|
+
def _normalize_department_scope(value: Any) -> dict[str, Any] | None:
|
|
8193
|
+
if not isinstance(value, dict):
|
|
8194
|
+
return None
|
|
8195
|
+
raw_departments = value.get("departments", value.get("depart", value.get("departs")))
|
|
8196
|
+
departments: list[dict[str, Any]] = []
|
|
8197
|
+
if isinstance(raw_departments, list):
|
|
8198
|
+
for item in raw_departments:
|
|
8199
|
+
if isinstance(item, dict):
|
|
8200
|
+
dept_id = _coerce_positive_int(item.get("dept_id", item.get("deptId", item.get("id"))))
|
|
8201
|
+
dept_name = str(
|
|
8202
|
+
item.get("dept_name", item.get("deptName", item.get("name", item.get("value")))) or ""
|
|
8203
|
+
).strip() or None
|
|
8204
|
+
else:
|
|
8205
|
+
dept_id = _coerce_positive_int(item)
|
|
8206
|
+
dept_name = None
|
|
8207
|
+
if dept_id is None and dept_name is None:
|
|
8208
|
+
continue
|
|
8209
|
+
entry: dict[str, Any] = {}
|
|
8210
|
+
if dept_id is not None:
|
|
8211
|
+
entry["dept_id"] = dept_id
|
|
8212
|
+
if dept_name is not None:
|
|
8213
|
+
entry["dept_name"] = dept_name
|
|
8214
|
+
departments.append(entry)
|
|
8215
|
+
normalized_mode = _normalize_department_scope_mode(value.get("mode"))
|
|
8216
|
+
if normalized_mode is None:
|
|
8217
|
+
normalized_mode = PublicDepartmentScopeMode.custom.value if departments else PublicDepartmentScopeMode.all.value
|
|
8218
|
+
return {
|
|
8219
|
+
"mode": normalized_mode,
|
|
8220
|
+
"departments": departments,
|
|
8221
|
+
"include_sub_departs": (
|
|
8222
|
+
None
|
|
8223
|
+
if value.get("include_sub_departs", value.get("includeSubDeparts")) is None
|
|
8224
|
+
else bool(value.get("include_sub_departs", value.get("includeSubDeparts")))
|
|
8225
|
+
),
|
|
8226
|
+
}
|
|
8227
|
+
|
|
8228
|
+
|
|
8229
|
+
def _normalize_department_scope_from_question(question: dict[str, Any]) -> dict[str, Any] | None:
|
|
8230
|
+
scope_type = _coerce_positive_int(question.get("deptSelectScopeType"))
|
|
8231
|
+
scope = question.get("deptSelectScope") if isinstance(question.get("deptSelectScope"), dict) else {}
|
|
8232
|
+
if not isinstance(scope, dict):
|
|
8233
|
+
scope = {}
|
|
8234
|
+
if isinstance(scope.get("dynamic"), list) and scope.get("dynamic"):
|
|
8235
|
+
return None
|
|
8236
|
+
if isinstance(scope.get("externalDepartList"), list) and scope.get("externalDepartList"):
|
|
8237
|
+
return None
|
|
8238
|
+
if scope_type == 1:
|
|
8239
|
+
return {
|
|
8240
|
+
"mode": PublicDepartmentScopeMode.all.value,
|
|
8241
|
+
"departments": [],
|
|
8242
|
+
"include_sub_departs": None
|
|
8243
|
+
if scope.get("includeSubDeparts") is None
|
|
8244
|
+
else bool(scope.get("includeSubDeparts")),
|
|
8245
|
+
}
|
|
8246
|
+
if scope_type == 2:
|
|
8247
|
+
departments: list[dict[str, Any]] = []
|
|
8248
|
+
for item in cast(list[Any], scope.get("depart") or []):
|
|
8249
|
+
if not isinstance(item, dict):
|
|
8250
|
+
continue
|
|
8251
|
+
dept_id = _coerce_positive_int(item.get("deptId", item.get("id")))
|
|
8252
|
+
dept_name = str(item.get("deptName", item.get("name", item.get("value"))) or "").strip() or None
|
|
8253
|
+
if dept_id is None and dept_name is None:
|
|
8254
|
+
continue
|
|
8255
|
+
entry: dict[str, Any] = {}
|
|
8256
|
+
if dept_id is not None:
|
|
8257
|
+
entry["dept_id"] = dept_id
|
|
8258
|
+
if dept_name is not None:
|
|
8259
|
+
entry["dept_name"] = dept_name
|
|
8260
|
+
departments.append(entry)
|
|
8261
|
+
return {
|
|
8262
|
+
"mode": PublicDepartmentScopeMode.custom.value,
|
|
8263
|
+
"departments": departments,
|
|
8264
|
+
"include_sub_departs": None
|
|
8265
|
+
if scope.get("includeSubDeparts") is None
|
|
8266
|
+
else bool(scope.get("includeSubDeparts")),
|
|
8267
|
+
}
|
|
8268
|
+
return None
|
|
8269
|
+
|
|
8270
|
+
|
|
8271
|
+
def _serialize_department_scope_for_question(value: Any) -> tuple[int, dict[str, Any]]:
|
|
8272
|
+
normalized = _normalize_department_scope(value)
|
|
8273
|
+
if normalized is None or normalized.get("mode") == PublicDepartmentScopeMode.all.value:
|
|
8274
|
+
scope: dict[str, Any] = {"depart": [], "dynamic": []}
|
|
8275
|
+
if normalized is not None and normalized.get("include_sub_departs") is not None:
|
|
8276
|
+
scope["includeSubDeparts"] = bool(normalized.get("include_sub_departs"))
|
|
8277
|
+
return 1, scope
|
|
8278
|
+
departments = []
|
|
8279
|
+
for item in cast(list[Any], normalized.get("departments") or []):
|
|
8280
|
+
if not isinstance(item, dict):
|
|
8281
|
+
continue
|
|
8282
|
+
dept_id = _coerce_positive_int(item.get("dept_id"))
|
|
8283
|
+
dept_name = str(item.get("dept_name") or "").strip() or None
|
|
8284
|
+
if dept_id is None and dept_name is None:
|
|
8285
|
+
continue
|
|
8286
|
+
entry: dict[str, Any] = {}
|
|
8287
|
+
if dept_id is not None:
|
|
8288
|
+
entry["deptId"] = dept_id
|
|
8289
|
+
if dept_name is not None:
|
|
8290
|
+
entry["deptName"] = dept_name
|
|
8291
|
+
departments.append(entry)
|
|
8292
|
+
scope = {"depart": departments, "dynamic": []}
|
|
8293
|
+
if normalized.get("include_sub_departs") is not None:
|
|
8294
|
+
scope["includeSubDeparts"] = bool(normalized.get("include_sub_departs"))
|
|
8295
|
+
return 2, scope
|
|
8296
|
+
|
|
8297
|
+
|
|
8298
|
+
def _department_scope_equal(left: Any, right: Any) -> bool:
|
|
8299
|
+
return _normalize_department_scope(left) == _normalize_department_scope(right)
|
|
8300
|
+
|
|
8301
|
+
|
|
7773
8302
|
def _is_relation_target_metadata_read_restricted_api_error(error: QingflowApiError) -> bool:
|
|
7774
8303
|
return error.backend_code in {40002, 40027, 40161}
|
|
7775
8304
|
|
|
@@ -7936,8 +8465,10 @@ def _hydrate_relation_field_configs(
|
|
|
7936
8465
|
field["config"] = config
|
|
7937
8466
|
display_selector = field.get("display_field") if isinstance(field.get("display_field"), dict) else None
|
|
7938
8467
|
visible_selector_payloads = [item for item in cast(list[Any], field.get("visible_fields") or []) if isinstance(item, dict)]
|
|
7939
|
-
if display_selector is None
|
|
7940
|
-
|
|
8468
|
+
if display_selector is None:
|
|
8469
|
+
raise ValueError("relation field requires display_field")
|
|
8470
|
+
if not visible_selector_payloads:
|
|
8471
|
+
raise ValueError("relation field requires visible_fields")
|
|
7941
8472
|
target_fields = target_field_cache.get(target_app_key)
|
|
7942
8473
|
if target_fields is None:
|
|
7943
8474
|
try:
|
|
@@ -7974,7 +8505,31 @@ def _hydrate_relation_field_configs(
|
|
|
7974
8505
|
)
|
|
7975
8506
|
continue
|
|
7976
8507
|
if not target_fields:
|
|
7977
|
-
|
|
8508
|
+
display_field = _normalize_relation_target_stub(
|
|
8509
|
+
display_selector,
|
|
8510
|
+
fallback_name=str((display_selector or {}).get("name") or "").strip() or None,
|
|
8511
|
+
)
|
|
8512
|
+
visible_fields = [
|
|
8513
|
+
_normalize_relation_target_stub(item, fallback_name=str(item.get("name") or "").strip() or None)
|
|
8514
|
+
for item in visible_selector_payloads
|
|
8515
|
+
]
|
|
8516
|
+
_apply_relation_target_selection(
|
|
8517
|
+
field=field,
|
|
8518
|
+
config=config,
|
|
8519
|
+
display_field=display_field,
|
|
8520
|
+
visible_fields=visible_fields,
|
|
8521
|
+
)
|
|
8522
|
+
degraded_entries.append(
|
|
8523
|
+
{
|
|
8524
|
+
"field_name": field.get("name"),
|
|
8525
|
+
"target_app_key": target_app_key,
|
|
8526
|
+
"display_field": deepcopy(field.get("display_field") or {}),
|
|
8527
|
+
"visible_fields": deepcopy(field.get("visible_fields") or []),
|
|
8528
|
+
"relation_mode": field.get("relation_mode"),
|
|
8529
|
+
"transport_error": None,
|
|
8530
|
+
}
|
|
8531
|
+
)
|
|
8532
|
+
continue
|
|
7978
8533
|
display_field = _resolve_relation_target_field(
|
|
7979
8534
|
target_fields=target_fields,
|
|
7980
8535
|
selector_payload=display_selector,
|
|
@@ -8079,6 +8634,10 @@ def _parse_field(question: dict[str, Any], *, field_id_hint: str | None = None)
|
|
|
8079
8634
|
}
|
|
8080
8635
|
field["visible_fields"] = visible_fields
|
|
8081
8636
|
field["field_name_show"] = bool(reference.get("fieldNameShow", True))
|
|
8637
|
+
if field_type == FieldType.department:
|
|
8638
|
+
department_scope = _normalize_department_scope_from_question(question)
|
|
8639
|
+
if department_scope is not None:
|
|
8640
|
+
field["department_scope"] = department_scope
|
|
8082
8641
|
if field_type == FieldType.code_block:
|
|
8083
8642
|
code_block_config = question.get("codeBlockConfig") if isinstance(question.get("codeBlockConfig"), dict) else {}
|
|
8084
8643
|
field["code_block_config"] = {
|
|
@@ -8700,6 +9259,8 @@ def _compact_public_field_read(*, field: dict[str, Any], layout: dict[str, Any])
|
|
|
8700
9259
|
payload["relation_mode"] = _normalize_relation_mode(field.get("relation_mode"))
|
|
8701
9260
|
payload["display_field"] = deepcopy(field.get("display_field"))
|
|
8702
9261
|
payload["visible_fields"] = deepcopy(field.get("visible_fields") or [])
|
|
9262
|
+
if field.get("type") == FieldType.department.value and field.get("department_scope") is not None:
|
|
9263
|
+
payload["department_scope"] = deepcopy(field.get("department_scope"))
|
|
8703
9264
|
if field.get("type") == FieldType.code_block.value:
|
|
8704
9265
|
payload["code_block_config"] = deepcopy(field.get("code_block_config") or {})
|
|
8705
9266
|
if field.get("auto_trigger") is not None:
|
|
@@ -8780,11 +9341,13 @@ def _field_patch_to_internal(patch: FieldPatch) -> dict[str, Any]:
|
|
|
8780
9341
|
"display_field": patch.display_field.model_dump(mode="json", exclude_none=True) if patch.display_field is not None else None,
|
|
8781
9342
|
"visible_fields": [selector.model_dump(mode="json", exclude_none=True) for selector in patch.visible_fields],
|
|
8782
9343
|
"relation_mode": patch.relation_mode.value if patch.relation_mode is not None else None,
|
|
9344
|
+
"department_scope": patch.department_scope.model_dump(mode="json", exclude_none=True) if patch.department_scope is not None else None,
|
|
8783
9345
|
"remote_lookup_config": remote_lookup_config,
|
|
8784
9346
|
"_explicit_remote_lookup_config": remote_lookup_config is not None,
|
|
8785
9347
|
"q_linker_binding": q_linker_binding,
|
|
8786
9348
|
"code_block_config": code_block_config,
|
|
8787
9349
|
"code_block_binding": code_block_binding,
|
|
9350
|
+
"_explicit_code_block_binding": code_block_binding is not None,
|
|
8788
9351
|
"config": deepcopy(remote_lookup_config) if remote_lookup_config is not None else (deepcopy(code_block_config) if code_block_config is not None else {}),
|
|
8789
9352
|
"auto_trigger": patch.auto_trigger,
|
|
8790
9353
|
"custom_button_text_enabled": patch.custom_button_text_enabled,
|
|
@@ -8797,6 +9360,23 @@ def _field_patch_to_internal(patch: FieldPatch) -> dict[str, Any]:
|
|
|
8797
9360
|
|
|
8798
9361
|
|
|
8799
9362
|
def _field_matches_patch(field: dict[str, Any], patch: FieldPatch) -> bool:
|
|
9363
|
+
field_display_selector = field.get("display_field")
|
|
9364
|
+
patch_display_selector = patch.display_field.model_dump(mode="json", exclude_none=True) if patch.display_field is not None else None
|
|
9365
|
+
field_visible_selectors = field.get("visible_fields")
|
|
9366
|
+
patch_visible_selectors = [selector.model_dump(mode="json", exclude_none=True) for selector in patch.visible_fields]
|
|
9367
|
+
if (
|
|
9368
|
+
str(field.get("type") or "") == FieldType.relation.value
|
|
9369
|
+
and patch.type == PublicFieldType.relation
|
|
9370
|
+
and (field.get("target_app_key") or None) == patch.target_app_key
|
|
9371
|
+
and field_display_selector is None
|
|
9372
|
+
and not list(field_visible_selectors or [])
|
|
9373
|
+
):
|
|
9374
|
+
relation_selector_match = True
|
|
9375
|
+
else:
|
|
9376
|
+
relation_selector_match = (
|
|
9377
|
+
_field_selector_payload_equal(field_display_selector, patch_display_selector)
|
|
9378
|
+
and _field_selector_list_equal(field_visible_selectors, patch_visible_selectors)
|
|
9379
|
+
)
|
|
8800
9380
|
return (
|
|
8801
9381
|
str(field.get("name") or "") == patch.name
|
|
8802
9382
|
and str(field.get("type") or "") == patch.type.value
|
|
@@ -8804,9 +9384,12 @@ def _field_matches_patch(field: dict[str, Any], patch: FieldPatch) -> bool:
|
|
|
8804
9384
|
and (field.get("description") or None) == patch.description
|
|
8805
9385
|
and list(field.get("options") or []) == list(patch.options)
|
|
8806
9386
|
and (field.get("target_app_key") or None) == patch.target_app_key
|
|
8807
|
-
and
|
|
8808
|
-
and _field_selector_list_equal(field.get("visible_fields"), [selector.model_dump(mode="json", exclude_none=True) for selector in patch.visible_fields])
|
|
9387
|
+
and relation_selector_match
|
|
8809
9388
|
and _normalize_relation_mode(field.get("relation_mode")) == _normalize_relation_mode(patch.relation_mode.value if patch.relation_mode is not None else None)
|
|
9389
|
+
and (
|
|
9390
|
+
patch.department_scope is None
|
|
9391
|
+
or _department_scope_equal(field.get("department_scope"), patch.department_scope.model_dump(mode="json", exclude_none=True))
|
|
9392
|
+
)
|
|
8810
9393
|
and _remote_lookup_config_equal(field.get("remote_lookup_config"), patch.remote_lookup_config.model_dump(mode="json", exclude_none=True) if patch.remote_lookup_config is not None else None)
|
|
8811
9394
|
and _q_linker_binding_equal(field.get("q_linker_binding"), patch.q_linker_binding.model_dump(mode="json", exclude_none=True) if patch.q_linker_binding is not None else None)
|
|
8812
9395
|
and _code_block_config_equal(field.get("code_block_config"), patch.code_block_config.model_dump(mode="json", exclude_none=True) if patch.code_block_config is not None else None)
|
|
@@ -8818,6 +9401,24 @@ def _field_matches_patch(field: dict[str, Any], patch: FieldPatch) -> bool:
|
|
|
8818
9401
|
)
|
|
8819
9402
|
|
|
8820
9403
|
|
|
9404
|
+
def _merge_existing_field_with_patch(field: dict[str, Any], patch: FieldPatch) -> None:
|
|
9405
|
+
if str(field.get("type") or "") == FieldType.relation.value and patch.type == PublicFieldType.relation:
|
|
9406
|
+
if not str(field.get("target_app_key") or "").strip() and patch.target_app_key:
|
|
9407
|
+
field["target_app_key"] = patch.target_app_key
|
|
9408
|
+
if not isinstance(field.get("display_field"), dict) and patch.display_field is not None:
|
|
9409
|
+
field["display_field"] = patch.display_field.model_dump(mode="json", exclude_none=True)
|
|
9410
|
+
if not list(field.get("visible_fields") or []) and patch.visible_fields:
|
|
9411
|
+
field["visible_fields"] = [
|
|
9412
|
+
selector.model_dump(mode="json", exclude_none=True)
|
|
9413
|
+
for selector in patch.visible_fields
|
|
9414
|
+
]
|
|
9415
|
+
if field.get("relation_mode") is None and patch.relation_mode is not None:
|
|
9416
|
+
field["relation_mode"] = patch.relation_mode.value
|
|
9417
|
+
if str(field.get("type") or "") == FieldType.department.value and patch.type == PublicFieldType.department:
|
|
9418
|
+
if field.get("department_scope") is None and patch.department_scope is not None:
|
|
9419
|
+
field["department_scope"] = patch.department_scope.model_dump(mode="json", exclude_none=True)
|
|
9420
|
+
|
|
9421
|
+
|
|
8821
9422
|
def _field_selector_payload_equal(left: Any, right: Any) -> bool:
|
|
8822
9423
|
if left is None and right is None:
|
|
8823
9424
|
return True
|
|
@@ -8998,6 +9599,8 @@ def _apply_field_mutation(field: dict[str, Any], mutation: Any) -> None:
|
|
|
8998
9599
|
field["visible_fields"] = list(payload["visible_fields"])
|
|
8999
9600
|
if "relation_mode" in payload:
|
|
9000
9601
|
field["relation_mode"] = payload["relation_mode"]
|
|
9602
|
+
if "department_scope" in payload:
|
|
9603
|
+
field["department_scope"] = payload["department_scope"]
|
|
9001
9604
|
if "remote_lookup_config" in payload:
|
|
9002
9605
|
field["remote_lookup_config"] = payload["remote_lookup_config"]
|
|
9003
9606
|
field["config"] = deepcopy(payload["remote_lookup_config"])
|
|
@@ -9011,6 +9614,7 @@ def _apply_field_mutation(field: dict[str, Any], mutation: Any) -> None:
|
|
|
9011
9614
|
field["config"] = deepcopy(payload["code_block_config"])
|
|
9012
9615
|
if "code_block_binding" in payload:
|
|
9013
9616
|
field["code_block_binding"] = payload["code_block_binding"]
|
|
9617
|
+
field["_explicit_code_block_binding"] = True
|
|
9014
9618
|
if "auto_trigger" in payload:
|
|
9015
9619
|
field["auto_trigger"] = payload["auto_trigger"]
|
|
9016
9620
|
if "custom_button_text_enabled" in payload:
|
|
@@ -9087,7 +9691,225 @@ def _strip_code_block_generated_input_prelude(code_content: str) -> str:
|
|
|
9087
9691
|
|
|
9088
9692
|
|
|
9089
9693
|
def _normalize_code_block_output_assignment(code_content: str) -> str:
|
|
9090
|
-
return re.sub(r"(
|
|
9694
|
+
return re.sub(r"(?<![A-Za-z0-9_$])(?:const|let)\s+qf_output\s*=", "qf_output =", code_content)
|
|
9695
|
+
|
|
9696
|
+
|
|
9697
|
+
@dataclass
|
|
9698
|
+
class _CodeBlockValidationError(ValueError):
|
|
9699
|
+
error_code: str
|
|
9700
|
+
message: str
|
|
9701
|
+
details: JSONObject = field(default_factory=dict)
|
|
9702
|
+
|
|
9703
|
+
def __str__(self) -> str:
|
|
9704
|
+
return self.message
|
|
9705
|
+
|
|
9706
|
+
|
|
9707
|
+
def _code_block_has_effective_output_assignment(code_content: str) -> bool:
|
|
9708
|
+
return bool(re.search(r"(?<![A-Za-z0-9_$])qf_output\s*=", str(code_content or "")))
|
|
9709
|
+
|
|
9710
|
+
|
|
9711
|
+
def _field_selector_payload_for_field(field: dict[str, Any]) -> dict[str, Any]:
|
|
9712
|
+
payload: dict[str, Any] = {}
|
|
9713
|
+
field_id = str(field.get("field_id") or "").strip()
|
|
9714
|
+
que_id = _coerce_positive_int(field.get("que_id"))
|
|
9715
|
+
name = str(field.get("name") or "").strip()
|
|
9716
|
+
if field_id:
|
|
9717
|
+
payload["field_id"] = field_id
|
|
9718
|
+
elif que_id is not None:
|
|
9719
|
+
payload["que_id"] = que_id
|
|
9720
|
+
elif name:
|
|
9721
|
+
payload["name"] = name
|
|
9722
|
+
return payload
|
|
9723
|
+
|
|
9724
|
+
|
|
9725
|
+
def _field_selector_payload_for_selector(value: Any) -> dict[str, Any]:
|
|
9726
|
+
if isinstance(value, dict):
|
|
9727
|
+
payload = {
|
|
9728
|
+
"field_id": str(value.get("field_id") or "").strip() or None,
|
|
9729
|
+
"que_id": _coerce_positive_int(value.get("que_id")),
|
|
9730
|
+
"name": str(value.get("name") or "").strip() or None,
|
|
9731
|
+
}
|
|
9732
|
+
return {key: item for key, item in payload.items() if item is not None}
|
|
9733
|
+
if isinstance(value, str) and value.strip():
|
|
9734
|
+
return {"name": value.strip()}
|
|
9735
|
+
return {}
|
|
9736
|
+
|
|
9737
|
+
|
|
9738
|
+
def _code_block_field_display_name(field: dict[str, Any]) -> str:
|
|
9739
|
+
name = str(field.get("name") or "").strip()
|
|
9740
|
+
if name:
|
|
9741
|
+
return name
|
|
9742
|
+
field_id = str(field.get("field_id") or "").strip()
|
|
9743
|
+
if field_id:
|
|
9744
|
+
return field_id
|
|
9745
|
+
que_id = _coerce_positive_int(field.get("que_id"))
|
|
9746
|
+
if que_id is not None:
|
|
9747
|
+
return str(que_id)
|
|
9748
|
+
return "未命名代码块"
|
|
9749
|
+
|
|
9750
|
+
|
|
9751
|
+
def _public_code_block_binding_payload(binding: dict[str, Any]) -> dict[str, Any]:
|
|
9752
|
+
return {
|
|
9753
|
+
"inputs": [
|
|
9754
|
+
{
|
|
9755
|
+
"field": _field_selector_payload_for_selector(input_item.get("field")),
|
|
9756
|
+
"var": input_item.get("var"),
|
|
9757
|
+
}
|
|
9758
|
+
for input_item in cast(list[dict[str, Any]], binding.get("inputs") or [])
|
|
9759
|
+
if _field_selector_payload_for_selector(input_item.get("field"))
|
|
9760
|
+
],
|
|
9761
|
+
"code": str(binding.get("code") or ""),
|
|
9762
|
+
"auto_trigger": binding.get("auto_trigger"),
|
|
9763
|
+
"custom_button_text_enabled": binding.get("custom_button_text_enabled"),
|
|
9764
|
+
"custom_button_text": binding.get("custom_button_text"),
|
|
9765
|
+
"outputs": [
|
|
9766
|
+
{
|
|
9767
|
+
"alias": str(output.get("alias") or "").strip(),
|
|
9768
|
+
"path": str(output.get("path") or "").strip(),
|
|
9769
|
+
"target_field": _field_selector_payload_for_selector(output.get("target_field")),
|
|
9770
|
+
}
|
|
9771
|
+
for output in cast(list[dict[str, Any]], binding.get("outputs") or [])
|
|
9772
|
+
if str(output.get("alias") or "").strip()
|
|
9773
|
+
and str(output.get("path") or "").strip()
|
|
9774
|
+
and _field_selector_payload_for_selector(output.get("target_field"))
|
|
9775
|
+
],
|
|
9776
|
+
}
|
|
9777
|
+
|
|
9778
|
+
|
|
9779
|
+
def _raw_field_selector_present(value: Any) -> bool:
|
|
9780
|
+
if isinstance(value, str):
|
|
9781
|
+
return bool(value.strip())
|
|
9782
|
+
if not isinstance(value, dict):
|
|
9783
|
+
return False
|
|
9784
|
+
return bool(
|
|
9785
|
+
str(value.get("field_id") or "").strip()
|
|
9786
|
+
or _coerce_positive_int(value.get("que_id"))
|
|
9787
|
+
or str(value.get("name") or value.get("title") or value.get("label") or "").strip()
|
|
9788
|
+
)
|
|
9789
|
+
|
|
9790
|
+
|
|
9791
|
+
def _validate_code_block_alias_config(
|
|
9792
|
+
*,
|
|
9793
|
+
field_name: str,
|
|
9794
|
+
raw_binding: dict[str, Any] | None,
|
|
9795
|
+
normalized_config: dict[str, Any] | None,
|
|
9796
|
+
) -> None:
|
|
9797
|
+
raw_outputs = raw_binding.get("outputs") if isinstance(raw_binding, dict) and isinstance(raw_binding.get("outputs"), list) else []
|
|
9798
|
+
for index, raw_output in enumerate(cast(list[Any], raw_outputs)):
|
|
9799
|
+
if not isinstance(raw_output, dict):
|
|
9800
|
+
continue
|
|
9801
|
+
raw_target = raw_output.get("target_field", raw_output.get("targetField"))
|
|
9802
|
+
if not _raw_field_selector_present(raw_target):
|
|
9803
|
+
continue
|
|
9804
|
+
alias = str(raw_output.get("alias", raw_output.get("alias_name", raw_output.get("aliasName", ""))) or "").strip()
|
|
9805
|
+
path = str(raw_output.get("path", raw_output.get("alias_path", raw_output.get("aliasPath", ""))) or "").strip()
|
|
9806
|
+
if alias and path:
|
|
9807
|
+
continue
|
|
9808
|
+
raise _CodeBlockValidationError(
|
|
9809
|
+
"CODE_BLOCK_ALIAS_REQUIRED",
|
|
9810
|
+
f"code_block field '{field_name}' requires non-empty alias and path for output target binding",
|
|
9811
|
+
details={"field_name": field_name, "output_index": index},
|
|
9812
|
+
)
|
|
9813
|
+
normalized_aliases = cast(list[dict[str, Any]], (normalized_config or {}).get("result_alias_path") or [])
|
|
9814
|
+
for index, alias_item in enumerate(normalized_aliases):
|
|
9815
|
+
alias_name = str(alias_item.get("alias_name") or "").strip()
|
|
9816
|
+
alias_path = str(alias_item.get("alias_path") or "").strip()
|
|
9817
|
+
if alias_name and alias_path:
|
|
9818
|
+
continue
|
|
9819
|
+
raise _CodeBlockValidationError(
|
|
9820
|
+
"CODE_BLOCK_ALIAS_REQUIRED",
|
|
9821
|
+
f"code_block field '{field_name}' contains an empty output alias configuration",
|
|
9822
|
+
details={"field_name": field_name, "alias_index": index},
|
|
9823
|
+
)
|
|
9824
|
+
|
|
9825
|
+
|
|
9826
|
+
def _normalize_and_validate_code_block_fields_for_write(
|
|
9827
|
+
*,
|
|
9828
|
+
fields: list[dict[str, Any]],
|
|
9829
|
+
) -> tuple[list[dict[str, Any]], list[str]]:
|
|
9830
|
+
next_fields = deepcopy(fields)
|
|
9831
|
+
normalized_fields: list[str] = []
|
|
9832
|
+
for field in next_fields:
|
|
9833
|
+
if not isinstance(field, dict) or field.get("type") != FieldType.code_block.value:
|
|
9834
|
+
continue
|
|
9835
|
+
field_name = _code_block_field_display_name(field)
|
|
9836
|
+
raw_binding = field.get("code_block_binding") if isinstance(field.get("code_block_binding"), dict) else None
|
|
9837
|
+
binding = _normalize_code_block_binding(raw_binding)
|
|
9838
|
+
current_config = _normalize_code_block_config(field.get("code_block_config") or field.get("config") or {}) or None
|
|
9839
|
+
_validate_code_block_alias_config(field_name=field_name, raw_binding=raw_binding, normalized_config=current_config)
|
|
9840
|
+
normalized = False
|
|
9841
|
+
if binding is not None:
|
|
9842
|
+
raw_code = str(binding.get("code") or "")
|
|
9843
|
+
normalized_code = _normalize_code_block_output_assignment(raw_code)
|
|
9844
|
+
if normalized_code != raw_code:
|
|
9845
|
+
normalized = True
|
|
9846
|
+
binding["code"] = normalized_code
|
|
9847
|
+
field["code_block_binding"] = binding
|
|
9848
|
+
if current_config is not None:
|
|
9849
|
+
raw_code_content = str(current_config.get("code_content") or "")
|
|
9850
|
+
normalized_code_content = _normalize_code_block_output_assignment(raw_code_content)
|
|
9851
|
+
if normalized_code_content != raw_code_content:
|
|
9852
|
+
normalized = True
|
|
9853
|
+
current_config["code_content"] = normalized_code_content
|
|
9854
|
+
field["code_block_config"] = current_config
|
|
9855
|
+
field["config"] = deepcopy(current_config)
|
|
9856
|
+
conflicting_binding_and_config = False
|
|
9857
|
+
if binding is not None and current_config is not None:
|
|
9858
|
+
current_config_code = str(current_config.get("code_content") or "")
|
|
9859
|
+
compiled_body = _strip_code_block_generated_input_prelude(current_config_code)
|
|
9860
|
+
if current_config_code.strip() and compiled_body != str(binding.get("code") or ""):
|
|
9861
|
+
conflicting_binding_and_config = True
|
|
9862
|
+
has_outputs = bool((binding or {}).get("outputs")) or bool((current_config or {}).get("result_alias_path"))
|
|
9863
|
+
effective_code = ""
|
|
9864
|
+
if binding is not None:
|
|
9865
|
+
effective_code = str(binding.get("code") or "")
|
|
9866
|
+
elif current_config is not None:
|
|
9867
|
+
effective_code = _strip_code_block_generated_input_prelude(str(current_config.get("code_content") or ""))
|
|
9868
|
+
if has_outputs and not conflicting_binding_and_config and not _code_block_has_effective_output_assignment(effective_code):
|
|
9869
|
+
raise _CodeBlockValidationError(
|
|
9870
|
+
"CODE_BLOCK_OUTPUT_ASSIGNMENT_MISSING",
|
|
9871
|
+
f"code_block field '{field_name}' must assign to qf_output when outputs are configured",
|
|
9872
|
+
details={"field_name": field_name},
|
|
9873
|
+
)
|
|
9874
|
+
if normalized:
|
|
9875
|
+
normalized_fields.append(field_name)
|
|
9876
|
+
return next_fields, sorted(set(normalized_fields))
|
|
9877
|
+
|
|
9878
|
+
|
|
9879
|
+
def _ensure_code_block_targets_compiled_as_relation_defaults(*, fields: list[dict[str, Any]]) -> None:
|
|
9880
|
+
for field in fields:
|
|
9881
|
+
if not isinstance(field, dict) or field.get("type") != FieldType.code_block.value:
|
|
9882
|
+
continue
|
|
9883
|
+
field_name = _code_block_field_display_name(field)
|
|
9884
|
+
binding = _normalize_code_block_binding(field.get("code_block_binding"))
|
|
9885
|
+
if binding is None:
|
|
9886
|
+
continue
|
|
9887
|
+
for output_index, output in enumerate(cast(list[dict[str, Any]], binding.get("outputs") or [])):
|
|
9888
|
+
target_payload = output.get("target_field")
|
|
9889
|
+
if not isinstance(target_payload, dict):
|
|
9890
|
+
continue
|
|
9891
|
+
target_field = _resolve_field_selector_with_uniqueness(
|
|
9892
|
+
fields=fields,
|
|
9893
|
+
selector_payload=target_payload,
|
|
9894
|
+
location=f"code_block_binding.outputs[{output_index}].target_field",
|
|
9895
|
+
)
|
|
9896
|
+
if _coerce_any_int(target_field.get("default_type")) == DEFAULT_TYPE_RELATION:
|
|
9897
|
+
continue
|
|
9898
|
+
raise _CodeBlockValidationError(
|
|
9899
|
+
"CODE_BLOCK_TARGET_DEFAULT_INVALID",
|
|
9900
|
+
f"code_block field '{field_name}' output target '{_code_block_field_display_name(target_field)}' is not compiled as relation default",
|
|
9901
|
+
details={
|
|
9902
|
+
"field_name": field_name,
|
|
9903
|
+
"target_field_name": _code_block_field_display_name(target_field),
|
|
9904
|
+
},
|
|
9905
|
+
)
|
|
9906
|
+
|
|
9907
|
+
|
|
9908
|
+
def _code_block_repair_suggested_next_call(*, profile: str, app_key: str, field_name: str | None = None) -> JSONObject:
|
|
9909
|
+
arguments: JSONObject = {"profile": profile, "app_key": app_key}
|
|
9910
|
+
if field_name:
|
|
9911
|
+
arguments["field"] = field_name
|
|
9912
|
+
return {"tool_name": "app_repair_code_blocks", "arguments": arguments}
|
|
9091
9913
|
|
|
9092
9914
|
|
|
9093
9915
|
def _ensure_field_temp_ids(fields: list[dict[str, Any]]) -> None:
|
|
@@ -9122,12 +9944,21 @@ def _compile_code_block_binding_fields(
|
|
|
9122
9944
|
binding = _normalize_code_block_binding(field.get("code_block_binding"))
|
|
9123
9945
|
if binding is None:
|
|
9124
9946
|
continue
|
|
9947
|
+
explicit_binding = bool(field.get("_explicit_code_block_binding"))
|
|
9948
|
+
binding_outputs = cast(list[dict[str, Any]], binding.get("outputs") or [])
|
|
9949
|
+
binding_has_target_bindings = any(
|
|
9950
|
+
isinstance(output.get("target_field"), dict) and any(cast(dict[str, Any], output.get("target_field") or {}).values())
|
|
9951
|
+
for output in binding_outputs
|
|
9952
|
+
)
|
|
9125
9953
|
current_config = _normalize_code_block_config(field.get("code_block_config") or field.get("config") or {}) or {
|
|
9126
9954
|
"config_mode": 1,
|
|
9127
9955
|
"code_content": "",
|
|
9128
9956
|
"being_hide_on_form": False,
|
|
9129
9957
|
"result_alias_path": [],
|
|
9130
9958
|
}
|
|
9959
|
+
if binding_outputs and not binding_has_target_bindings and not explicit_binding:
|
|
9960
|
+
field["code_block_binding"] = None
|
|
9961
|
+
continue
|
|
9131
9962
|
low_level_code = _strip_code_block_generated_input_prelude(str(current_config.get("code_content") or ""))
|
|
9132
9963
|
if str(current_config.get("code_content") or "").strip() and low_level_code != str(binding.get("code") or ""):
|
|
9133
9964
|
raise ValueError(f"code_block field '{field.get('name')}' has conflicting code_block_config.code_content and code_block_binding.code")
|
|
@@ -10280,6 +11111,10 @@ def _field_to_question(field: dict[str, Any], *, temp_id: int) -> dict[str, Any]
|
|
|
10280
11111
|
reference["referQueId"] = field.get("target_field_que_id")
|
|
10281
11112
|
reference["optionalDataNum"] = _relation_mode_to_optional_data_num(field.get("relation_mode"))
|
|
10282
11113
|
question["referenceConfig"] = reference
|
|
11114
|
+
if field.get("type") == FieldType.department.value:
|
|
11115
|
+
scope_type, scope_payload = _serialize_department_scope_for_question(field.get("department_scope"))
|
|
11116
|
+
question["deptSelectScopeType"] = scope_type
|
|
11117
|
+
question["deptSelectScope"] = scope_payload
|
|
10283
11118
|
if field.get("type") == FieldType.code_block.value:
|
|
10284
11119
|
code_block_config = _normalize_code_block_config(field.get("code_block_config") or field.get("config") or {}) or {
|
|
10285
11120
|
"config_mode": 1,
|