@josephyan/qingflow-app-user-mcp 0.2.0-beta.94 → 0.2.0-beta.96
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/__init__.py +1 -1
- package/src/qingflow_mcp/builder_facade/models.py +12 -2
- package/src/qingflow_mcp/builder_facade/service.py +134 -7
- package/src/qingflow_mcp/cli/commands/builder.py +1 -1
- package/src/qingflow_mcp/tools/ai_builder_tools.py +25 -0
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.
|
|
6
|
+
npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.96
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.
|
|
12
|
+
npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.96 qingflow-app-user-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -819,6 +819,10 @@ class FieldMutation(StrictModel):
|
|
|
819
819
|
validation_alias=AliasChoices("custom_button_text", "customButtonText", "custom_btn_text", "customBtnText"),
|
|
820
820
|
)
|
|
821
821
|
subfields: list[FieldPatch] | None = None
|
|
822
|
+
subfield_updates: list["FieldUpdatePatch"] | None = Field(
|
|
823
|
+
default=None,
|
|
824
|
+
validation_alias=AliasChoices("subfield_updates", "subfieldUpdates"),
|
|
825
|
+
)
|
|
822
826
|
|
|
823
827
|
@model_validator(mode="after")
|
|
824
828
|
def validate_shape(self) -> "FieldMutation":
|
|
@@ -848,8 +852,12 @@ class FieldMutation(StrictModel):
|
|
|
848
852
|
or self.custom_button_text is not None
|
|
849
853
|
):
|
|
850
854
|
raise ValueError("code_block_config, code_block_binding, auto_trigger, custom_button_text_enabled, and custom_button_text are only allowed for code_block fields")
|
|
851
|
-
if self.type == PublicFieldType.subtable and not self.subfields:
|
|
852
|
-
raise ValueError("subtable field requires subfields")
|
|
855
|
+
if self.type == PublicFieldType.subtable and not self.subfields and not self.subfield_updates:
|
|
856
|
+
raise ValueError("subtable field requires subfields or subfield_updates")
|
|
857
|
+
if self.type is not None and self.type != PublicFieldType.subtable and self.subfield_updates:
|
|
858
|
+
raise ValueError("subfield_updates are only allowed for subtable fields")
|
|
859
|
+
if self.subfields and self.subfield_updates:
|
|
860
|
+
raise ValueError("subfields and subfield_updates cannot be used together")
|
|
853
861
|
return self
|
|
854
862
|
|
|
855
863
|
@model_validator(mode="before")
|
|
@@ -1536,6 +1544,8 @@ class PortalApplyRequest(StrictModel):
|
|
|
1536
1544
|
|
|
1537
1545
|
|
|
1538
1546
|
FieldPatch.model_rebuild()
|
|
1547
|
+
FieldMutation.model_rebuild()
|
|
1548
|
+
FieldUpdatePatch.model_rebuild()
|
|
1539
1549
|
|
|
1540
1550
|
|
|
1541
1551
|
class AppGetResponse(StrictModel):
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import base64
|
|
3
4
|
from copy import deepcopy
|
|
4
5
|
from dataclasses import dataclass, field
|
|
5
6
|
import json
|
|
6
7
|
import os
|
|
8
|
+
import random
|
|
7
9
|
import re
|
|
10
|
+
import string
|
|
8
11
|
import tempfile
|
|
9
12
|
from typing import Any, cast
|
|
13
|
+
from urllib.parse import quote_plus, unquote_plus
|
|
10
14
|
from uuid import uuid4
|
|
11
15
|
|
|
12
16
|
from ..backend_client import BackendRequestContext
|
|
@@ -143,6 +147,7 @@ JUDGE_EQUAL_ANY = 9
|
|
|
143
147
|
JUDGE_FUZZY_MATCH = 19
|
|
144
148
|
JUDGE_INCLUDE_ANY = 20
|
|
145
149
|
DEFAULT_TYPE_RELATION = 2
|
|
150
|
+
DEFAULT_TYPE_FORMULA = 3
|
|
146
151
|
RELATION_TYPE_Q_LINKER = 2
|
|
147
152
|
RELATION_TYPE_CODE_BLOCK = 3
|
|
148
153
|
|
|
@@ -4630,7 +4635,19 @@ class AiBuilderFacade:
|
|
|
4630
4635
|
)
|
|
4631
4636
|
field = current_fields[matched]
|
|
4632
4637
|
previous_name = field["name"]
|
|
4633
|
-
|
|
4638
|
+
try:
|
|
4639
|
+
_apply_field_mutation(field, patch.set)
|
|
4640
|
+
except ValueError as error:
|
|
4641
|
+
return _failed(
|
|
4642
|
+
"VALIDATION_ERROR",
|
|
4643
|
+
str(error),
|
|
4644
|
+
normalized_args=normalized_args,
|
|
4645
|
+
details={
|
|
4646
|
+
"selector": patch.selector.model_dump(mode="json"),
|
|
4647
|
+
"app_key": target.app_key,
|
|
4648
|
+
},
|
|
4649
|
+
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
4650
|
+
)
|
|
4634
4651
|
current_fields[matched] = field
|
|
4635
4652
|
layout = _rename_field_in_layout(layout, previous_name, field["name"])
|
|
4636
4653
|
updated.append(field["name"])
|
|
@@ -10527,14 +10544,14 @@ def _parse_code_block_inputs_and_body(code_content: str) -> tuple[list[dict[str,
|
|
|
10527
10544
|
return inputs, body
|
|
10528
10545
|
|
|
10529
10546
|
|
|
10530
|
-
def _compact_public_field_read(*, field: dict[str, Any], layout: dict[str, Any]) -> dict[str, Any]:
|
|
10547
|
+
def _compact_public_field_read(*, field: dict[str, Any], layout: dict[str, Any], nested: bool = False) -> dict[str, Any]:
|
|
10531
10548
|
payload = {
|
|
10532
10549
|
"field_id": field.get("field_id"),
|
|
10533
10550
|
"que_id": field.get("que_id"),
|
|
10534
10551
|
"name": field.get("name"),
|
|
10535
10552
|
"type": field.get("type"),
|
|
10536
10553
|
"required": bool(field.get("required")),
|
|
10537
|
-
"section_id": _find_field_section_id(layout, str(field.get("name") or "")),
|
|
10554
|
+
"section_id": None if nested else _find_field_section_id(layout, str(field.get("name") or "")),
|
|
10538
10555
|
}
|
|
10539
10556
|
if field.get("type") == FieldType.relation.value:
|
|
10540
10557
|
payload["target_app_key"] = field.get("target_app_key")
|
|
@@ -10567,6 +10584,12 @@ def _compact_public_field_read(*, field: dict[str, Any], layout: dict[str, Any])
|
|
|
10567
10584
|
payload["custom_button_text"] = field.get("custom_button_text")
|
|
10568
10585
|
if field.get("metadata_unverified") is not None:
|
|
10569
10586
|
payload["metadata_unverified"] = bool(field.get("metadata_unverified"))
|
|
10587
|
+
if field.get("type") == FieldType.subtable.value:
|
|
10588
|
+
payload["subfields"] = [
|
|
10589
|
+
_compact_public_field_read(field=subfield, layout=layout, nested=True)
|
|
10590
|
+
for subfield in cast(list[dict[str, Any]], field.get("subfields") or [])
|
|
10591
|
+
if isinstance(subfield, dict)
|
|
10592
|
+
]
|
|
10570
10593
|
return payload
|
|
10571
10594
|
|
|
10572
10595
|
|
|
@@ -10862,6 +10885,35 @@ def _code_block_binding_equal(left: Any, right: Any) -> bool:
|
|
|
10862
10885
|
return _normalize_code_block_binding(left) == _normalize_code_block_binding(right)
|
|
10863
10886
|
|
|
10864
10887
|
|
|
10888
|
+
_SAFE_SUBFIELD_MUTATION_KEYS = frozenset({"name", "required", "description", "subfield_updates"})
|
|
10889
|
+
|
|
10890
|
+
|
|
10891
|
+
def _validate_safe_subfield_mutation(*, payload: dict[str, Any], location: str) -> None:
|
|
10892
|
+
unsupported = sorted(key for key in payload if key not in _SAFE_SUBFIELD_MUTATION_KEYS)
|
|
10893
|
+
if unsupported:
|
|
10894
|
+
raise ValueError(
|
|
10895
|
+
f"{location} only supports safe overlay keys: name, required, description, subfield_updates; "
|
|
10896
|
+
f"unsupported keys: {', '.join(unsupported)}"
|
|
10897
|
+
)
|
|
10898
|
+
|
|
10899
|
+
|
|
10900
|
+
def _apply_subfield_updates(field: dict[str, Any], raw_updates: list[Any]) -> None:
|
|
10901
|
+
if str(field.get("type") or "") != FieldType.subtable.value:
|
|
10902
|
+
raise ValueError("subfield_updates can only target subtable fields")
|
|
10903
|
+
subfields = [subfield for subfield in cast(list[dict[str, Any]], field.get("subfields") or []) if isinstance(subfield, dict)]
|
|
10904
|
+
for index, raw_item in enumerate(raw_updates):
|
|
10905
|
+
patch = FieldUpdatePatch.model_validate(raw_item)
|
|
10906
|
+
payload = patch.set.model_dump(mode="json", exclude_none=True)
|
|
10907
|
+
_validate_safe_subfield_mutation(payload=payload, location=f"subfield_updates[{index}].set")
|
|
10908
|
+
target = _resolve_field_selector_with_uniqueness(
|
|
10909
|
+
fields=subfields,
|
|
10910
|
+
selector_payload=patch.selector.model_dump(mode="json", exclude_none=True),
|
|
10911
|
+
location=f"subfield_updates[{index}].selector",
|
|
10912
|
+
)
|
|
10913
|
+
_apply_field_mutation(target, patch.set)
|
|
10914
|
+
field["subfields"] = subfields
|
|
10915
|
+
|
|
10916
|
+
|
|
10865
10917
|
def _apply_field_mutation(field: dict[str, Any], mutation: Any) -> None:
|
|
10866
10918
|
payload = mutation.model_dump(mode="json", exclude_none=True)
|
|
10867
10919
|
relation_config_explicit = (
|
|
@@ -10930,6 +10982,8 @@ def _apply_field_mutation(field: dict[str, Any], mutation: Any) -> None:
|
|
|
10930
10982
|
if "subfields" in payload:
|
|
10931
10983
|
field["subfields"] = [_field_patch_to_internal(item) for item in payload["subfields"]]
|
|
10932
10984
|
question_rebuild_required = True
|
|
10985
|
+
if "subfield_updates" in payload:
|
|
10986
|
+
_apply_subfield_updates(field, payload["subfield_updates"])
|
|
10933
10987
|
if relation_config_explicit:
|
|
10934
10988
|
field["_relation_config_explicit"] = True
|
|
10935
10989
|
question_rebuild_required = True
|
|
@@ -12927,6 +12981,48 @@ def _copy_present_keys(
|
|
|
12927
12981
|
return payload
|
|
12928
12982
|
|
|
12929
12983
|
|
|
12984
|
+
def _looks_like_backend_encoded_formula(value: str) -> bool:
|
|
12985
|
+
if len(value) <= 32:
|
|
12986
|
+
return False
|
|
12987
|
+
encoded = value[16:-16]
|
|
12988
|
+
if not encoded:
|
|
12989
|
+
return False
|
|
12990
|
+
try:
|
|
12991
|
+
decoded = base64.b64decode(encoded, validate=True).decode("utf-8")
|
|
12992
|
+
unquote_plus(decoded)
|
|
12993
|
+
except Exception:
|
|
12994
|
+
return False
|
|
12995
|
+
return True
|
|
12996
|
+
|
|
12997
|
+
|
|
12998
|
+
def _encode_formula_for_backend_save(value: Any) -> Any:
|
|
12999
|
+
if not isinstance(value, str) or not value:
|
|
13000
|
+
return value
|
|
13001
|
+
if _looks_like_backend_encoded_formula(value):
|
|
13002
|
+
return value
|
|
13003
|
+
encoded = quote_plus(value, encoding="utf-8")
|
|
13004
|
+
b64_value = base64.b64encode(encoded.encode("utf-8")).decode("ascii")
|
|
13005
|
+
alphabet = string.ascii_letters + string.digits
|
|
13006
|
+
prefix = "".join(random.choice(alphabet) for _ in range(16))
|
|
13007
|
+
suffix = "".join(random.choice(alphabet) for _ in range(16))
|
|
13008
|
+
return f"{prefix}{b64_value}{suffix}"
|
|
13009
|
+
|
|
13010
|
+
|
|
13011
|
+
def _normalize_formula_defaults_for_save(value: Any) -> None:
|
|
13012
|
+
if isinstance(value, list):
|
|
13013
|
+
for item in value:
|
|
13014
|
+
_normalize_formula_defaults_for_save(item)
|
|
13015
|
+
return
|
|
13016
|
+
if not isinstance(value, dict):
|
|
13017
|
+
return
|
|
13018
|
+
if _coerce_any_int(value.get("queDefaultType")) == DEFAULT_TYPE_FORMULA and value.get("queDefaultValue"):
|
|
13019
|
+
value["queDefaultValue"] = _encode_formula_for_backend_save(value.get("queDefaultValue"))
|
|
13020
|
+
for key in ("subQuestions", "innerQuestions"):
|
|
13021
|
+
nested = value.get(key)
|
|
13022
|
+
if isinstance(nested, (list, dict)):
|
|
13023
|
+
_normalize_formula_defaults_for_save(nested)
|
|
13024
|
+
|
|
13025
|
+
|
|
12930
13026
|
def _normalize_reference_question_for_save(value: Any, *, ordinal: int) -> dict[str, Any] | None:
|
|
12931
13027
|
if not isinstance(value, dict):
|
|
12932
13028
|
return None
|
|
@@ -13090,6 +13186,8 @@ def _normalize_question_relations_for_save(question_relations: list[dict[str, An
|
|
|
13090
13186
|
value = relation.get(key)
|
|
13091
13187
|
if value is None:
|
|
13092
13188
|
continue
|
|
13189
|
+
if key == "matchRuleFormula":
|
|
13190
|
+
value = _encode_formula_for_backend_save(value)
|
|
13093
13191
|
item[key] = deepcopy(value)
|
|
13094
13192
|
if item:
|
|
13095
13193
|
normalized.append(item)
|
|
@@ -13099,22 +13197,32 @@ def _normalize_question_relations_for_save(question_relations: list[dict[str, An
|
|
|
13099
13197
|
def _field_rename_maps(fields: list[dict[str, Any]]) -> tuple[dict[int, str], dict[str, str]]:
|
|
13100
13198
|
by_que_id: dict[int, str] = {}
|
|
13101
13199
|
by_title: dict[str, str] = {}
|
|
13102
|
-
|
|
13200
|
+
|
|
13201
|
+
def visit(field: dict[str, Any]) -> None:
|
|
13103
13202
|
if not isinstance(field, dict):
|
|
13104
|
-
|
|
13203
|
+
return
|
|
13105
13204
|
template = field.get("_question_template")
|
|
13106
13205
|
if not isinstance(template, dict):
|
|
13107
|
-
|
|
13206
|
+
for subfield in cast(list[dict[str, Any]], field.get("subfields") or []):
|
|
13207
|
+
visit(subfield)
|
|
13208
|
+
return
|
|
13108
13209
|
old_title = str(template.get("queTitle") or "").strip()
|
|
13109
13210
|
new_title = str(field.get("name") or "").strip()
|
|
13110
13211
|
if not old_title or not new_title or old_title == new_title:
|
|
13111
|
-
|
|
13212
|
+
for subfield in cast(list[dict[str, Any]], field.get("subfields") or []):
|
|
13213
|
+
visit(subfield)
|
|
13214
|
+
return
|
|
13112
13215
|
que_id = _coerce_nonnegative_int(field.get("que_id"))
|
|
13113
13216
|
if que_id is None:
|
|
13114
13217
|
que_id = _coerce_nonnegative_int(template.get("queId"))
|
|
13115
13218
|
if que_id is not None:
|
|
13116
13219
|
by_que_id[que_id] = new_title
|
|
13117
13220
|
by_title[old_title] = new_title
|
|
13221
|
+
for subfield in cast(list[dict[str, Any]], field.get("subfields") or []):
|
|
13222
|
+
visit(subfield)
|
|
13223
|
+
|
|
13224
|
+
for field in fields:
|
|
13225
|
+
visit(field)
|
|
13118
13226
|
return by_que_id, by_title
|
|
13119
13227
|
|
|
13120
13228
|
|
|
@@ -13151,6 +13259,20 @@ def _sync_question_title_references(value: Any, *, by_que_id: dict[int, str], by
|
|
|
13151
13259
|
_sync_question_title_references(child_value, by_que_id=by_que_id, by_title=by_title)
|
|
13152
13260
|
|
|
13153
13261
|
|
|
13262
|
+
def _materialize_preserved_subtable_question(field: dict[str, Any], *, template: dict[str, Any]) -> dict[str, Any] | None:
|
|
13263
|
+
materialized_subquestions: list[dict[str, Any]] = []
|
|
13264
|
+
for subfield in cast(list[dict[str, Any]], field.get("subfields") or []):
|
|
13265
|
+
if not isinstance(subfield, dict):
|
|
13266
|
+
continue
|
|
13267
|
+
materialized = _materialize_preserved_question(subfield)
|
|
13268
|
+
if materialized is None:
|
|
13269
|
+
return None
|
|
13270
|
+
materialized_subquestions.append(materialized)
|
|
13271
|
+
template["subQuestions"] = materialized_subquestions
|
|
13272
|
+
template["innerQuestions"] = [deepcopy(materialized_subquestions)]
|
|
13273
|
+
return template
|
|
13274
|
+
|
|
13275
|
+
|
|
13154
13276
|
def _materialize_preserved_question(field: dict[str, Any]) -> dict[str, Any] | None:
|
|
13155
13277
|
template = deepcopy(field.get("_question_template"))
|
|
13156
13278
|
if not isinstance(template, dict):
|
|
@@ -13163,6 +13285,8 @@ def _materialize_preserved_question(field: dict[str, Any]) -> dict[str, Any] | N
|
|
|
13163
13285
|
if "description" in overlay_keys:
|
|
13164
13286
|
description = field.get("description")
|
|
13165
13287
|
template["queHint"] = "" if description is None else str(description)
|
|
13288
|
+
if str(field.get("type") or "") == FieldType.subtable.value:
|
|
13289
|
+
return _materialize_preserved_subtable_question(field, template=template)
|
|
13166
13290
|
if str(field.get("type") or "") == FieldType.relation.value:
|
|
13167
13291
|
return _normalize_relation_question_for_save(template, field=field)
|
|
13168
13292
|
return template
|
|
@@ -13412,6 +13536,7 @@ def _build_form_payload_from_fields(
|
|
|
13412
13536
|
for row in form_rows:
|
|
13413
13537
|
_apply_row_widths(row)
|
|
13414
13538
|
payload = default_form_payload(title, form_rows)
|
|
13539
|
+
_normalize_formula_defaults_for_save(payload.get("formQues"))
|
|
13415
13540
|
payload["editVersionNo"] = int(current_schema.get("editVersionNo") or 1)
|
|
13416
13541
|
payload["questionRelations"] = _normalize_question_relations_for_save(
|
|
13417
13542
|
question_relations if question_relations is not None else (current_schema.get("questionRelations") or [])
|
|
@@ -13529,6 +13654,7 @@ def _build_form_payload_for_edit_fields(
|
|
|
13529
13654
|
|
|
13530
13655
|
payload = _build_form_save_base_payload(current_schema, title)
|
|
13531
13656
|
payload["formQues"] = form_rows
|
|
13657
|
+
_normalize_formula_defaults_for_save(payload.get("formQues"))
|
|
13532
13658
|
payload["questionRelations"] = normalized_relations
|
|
13533
13659
|
payload["editVersionNo"] = int(current_schema.get("editVersionNo") or 1)
|
|
13534
13660
|
return payload
|
|
@@ -14945,6 +15071,7 @@ def _build_form_payload_from_existing_schema(
|
|
|
14945
15071
|
|
|
14946
15072
|
payload = _build_form_save_base_payload(current_schema, str(current_schema.get("formTitle") or "未命名应用"))
|
|
14947
15073
|
payload["formQues"] = form_rows
|
|
15074
|
+
_normalize_formula_defaults_for_save(payload.get("formQues"))
|
|
14948
15075
|
payload["questionRelations"] = _normalize_question_relations_for_save(current_schema.get("questionRelations") or [])
|
|
14949
15076
|
payload["editVersionNo"] = int(current_schema.get("editVersionNo") or 1)
|
|
14950
15077
|
payload.setdefault("formTitle", current_schema.get("formTitle") or "未命名应用")
|
|
@@ -101,7 +101,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
101
101
|
app_release_lock.add_argument("--lock-owner-name", required=True)
|
|
102
102
|
app_release_lock.set_defaults(handler=_handle_app_release_edit_lock_if_mine, format_hint="builder_summary")
|
|
103
103
|
|
|
104
|
-
app_get = app_subparsers.add_parser("get", help="
|
|
104
|
+
app_get = app_subparsers.add_parser("get", help="读取应用配置(字段请使用: builder app get --app-key APP fields)")
|
|
105
105
|
app_get.add_argument(
|
|
106
106
|
"builder_app_get_section",
|
|
107
107
|
nargs="?",
|
|
@@ -2550,6 +2550,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2550
2550
|
"field.autoTrigger": "field.auto_trigger",
|
|
2551
2551
|
"field.customBtnTextStatus": "field.custom_button_text_enabled",
|
|
2552
2552
|
"field.customBtnText": "field.custom_button_text",
|
|
2553
|
+
"field.subfieldUpdates": "field.subfield_updates",
|
|
2553
2554
|
},
|
|
2554
2555
|
"allowed_values": {
|
|
2555
2556
|
"field.type": [member.value for member in PublicFieldType],
|
|
@@ -2636,6 +2637,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2636
2637
|
"relation_mode=multiple maps to referenceConfig.optionalDataNum=0",
|
|
2637
2638
|
"relation fields now require both display_field and visible_fields in MCP/CLI payloads",
|
|
2638
2639
|
"if relation target metadata lookup is blocked by 40161/40002/40027, explicit display_field.name and visible_fields[].name let builder degrade verification and still continue schema write",
|
|
2640
|
+
"update_fields[].set.subfield_updates is the safe patch path for editing existing subtable child fields without rebuilding the entire subtable",
|
|
2641
|
+
"subfield_updates only supports safe child overlays: name, required, description, and nested subfield_updates",
|
|
2642
|
+
"set.subfields remains the full replace/rebuild path for a subtable and is higher risk when hidden relation/reference children exist",
|
|
2639
2643
|
"department fields accept department_scope with mode=all or mode=custom; custom scope requires explicit departments[].dept_id and optional include_sub_departs",
|
|
2640
2644
|
"q_linker_binding lets you declare request config, dynamic inputs, alias parsing, and target-field bindings in one step; builder writes remoteLookupConfig plus the existing backend relation-default and questionRelations structures",
|
|
2641
2645
|
"code_block_binding lets you declare inputs, code, alias parsing, and target-field bindings in one step; builder writes codeBlockConfig plus the existing backend relation-default and questionRelations structures",
|
|
@@ -2683,6 +2687,26 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2683
2687
|
"update_fields": [],
|
|
2684
2688
|
"remove_fields": [],
|
|
2685
2689
|
},
|
|
2690
|
+
"subfield_update_example": {
|
|
2691
|
+
"profile": "default",
|
|
2692
|
+
"app_key": "APP_REWARD",
|
|
2693
|
+
"publish": True,
|
|
2694
|
+
"add_fields": [],
|
|
2695
|
+
"update_fields": [
|
|
2696
|
+
{
|
|
2697
|
+
"selector": {"que_id": 441305044},
|
|
2698
|
+
"set": {
|
|
2699
|
+
"subfield_updates": [
|
|
2700
|
+
{
|
|
2701
|
+
"selector": {"que_id": 441305045},
|
|
2702
|
+
"set": {"name": "景品名"},
|
|
2703
|
+
}
|
|
2704
|
+
]
|
|
2705
|
+
},
|
|
2706
|
+
}
|
|
2707
|
+
],
|
|
2708
|
+
"remove_fields": [],
|
|
2709
|
+
},
|
|
2686
2710
|
"department_scope_example": {
|
|
2687
2711
|
"profile": "default",
|
|
2688
2712
|
"app_key": "APP_LEAD",
|
|
@@ -3049,6 +3073,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3049
3073
|
"execution_notes": [
|
|
3050
3074
|
"returns compact current field configuration for one app",
|
|
3051
3075
|
"use this before app_schema_apply when you need exact field definitions",
|
|
3076
|
+
"subtable fields include nested subfields using the same compact field shape",
|
|
3052
3077
|
],
|
|
3053
3078
|
"minimal_example": {
|
|
3054
3079
|
"profile": "default",
|