@josephyan/qingflow-app-user-mcp 0.2.0-beta.95 → 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/builder_facade/models.py +12 -2
- package/src/qingflow_mcp/builder_facade/service.py +82 -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):
|
|
@@ -4635,7 +4635,19 @@ class AiBuilderFacade:
|
|
|
4635
4635
|
)
|
|
4636
4636
|
field = current_fields[matched]
|
|
4637
4637
|
previous_name = field["name"]
|
|
4638
|
-
|
|
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
|
+
)
|
|
4639
4651
|
current_fields[matched] = field
|
|
4640
4652
|
layout = _rename_field_in_layout(layout, previous_name, field["name"])
|
|
4641
4653
|
updated.append(field["name"])
|
|
@@ -10532,14 +10544,14 @@ def _parse_code_block_inputs_and_body(code_content: str) -> tuple[list[dict[str,
|
|
|
10532
10544
|
return inputs, body
|
|
10533
10545
|
|
|
10534
10546
|
|
|
10535
|
-
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]:
|
|
10536
10548
|
payload = {
|
|
10537
10549
|
"field_id": field.get("field_id"),
|
|
10538
10550
|
"que_id": field.get("que_id"),
|
|
10539
10551
|
"name": field.get("name"),
|
|
10540
10552
|
"type": field.get("type"),
|
|
10541
10553
|
"required": bool(field.get("required")),
|
|
10542
|
-
"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 "")),
|
|
10543
10555
|
}
|
|
10544
10556
|
if field.get("type") == FieldType.relation.value:
|
|
10545
10557
|
payload["target_app_key"] = field.get("target_app_key")
|
|
@@ -10572,6 +10584,12 @@ def _compact_public_field_read(*, field: dict[str, Any], layout: dict[str, Any])
|
|
|
10572
10584
|
payload["custom_button_text"] = field.get("custom_button_text")
|
|
10573
10585
|
if field.get("metadata_unverified") is not None:
|
|
10574
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
|
+
]
|
|
10575
10593
|
return payload
|
|
10576
10594
|
|
|
10577
10595
|
|
|
@@ -10867,6 +10885,35 @@ def _code_block_binding_equal(left: Any, right: Any) -> bool:
|
|
|
10867
10885
|
return _normalize_code_block_binding(left) == _normalize_code_block_binding(right)
|
|
10868
10886
|
|
|
10869
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
|
+
|
|
10870
10917
|
def _apply_field_mutation(field: dict[str, Any], mutation: Any) -> None:
|
|
10871
10918
|
payload = mutation.model_dump(mode="json", exclude_none=True)
|
|
10872
10919
|
relation_config_explicit = (
|
|
@@ -10935,6 +10982,8 @@ def _apply_field_mutation(field: dict[str, Any], mutation: Any) -> None:
|
|
|
10935
10982
|
if "subfields" in payload:
|
|
10936
10983
|
field["subfields"] = [_field_patch_to_internal(item) for item in payload["subfields"]]
|
|
10937
10984
|
question_rebuild_required = True
|
|
10985
|
+
if "subfield_updates" in payload:
|
|
10986
|
+
_apply_subfield_updates(field, payload["subfield_updates"])
|
|
10938
10987
|
if relation_config_explicit:
|
|
10939
10988
|
field["_relation_config_explicit"] = True
|
|
10940
10989
|
question_rebuild_required = True
|
|
@@ -13148,22 +13197,32 @@ def _normalize_question_relations_for_save(question_relations: list[dict[str, An
|
|
|
13148
13197
|
def _field_rename_maps(fields: list[dict[str, Any]]) -> tuple[dict[int, str], dict[str, str]]:
|
|
13149
13198
|
by_que_id: dict[int, str] = {}
|
|
13150
13199
|
by_title: dict[str, str] = {}
|
|
13151
|
-
|
|
13200
|
+
|
|
13201
|
+
def visit(field: dict[str, Any]) -> None:
|
|
13152
13202
|
if not isinstance(field, dict):
|
|
13153
|
-
|
|
13203
|
+
return
|
|
13154
13204
|
template = field.get("_question_template")
|
|
13155
13205
|
if not isinstance(template, dict):
|
|
13156
|
-
|
|
13206
|
+
for subfield in cast(list[dict[str, Any]], field.get("subfields") or []):
|
|
13207
|
+
visit(subfield)
|
|
13208
|
+
return
|
|
13157
13209
|
old_title = str(template.get("queTitle") or "").strip()
|
|
13158
13210
|
new_title = str(field.get("name") or "").strip()
|
|
13159
13211
|
if not old_title or not new_title or old_title == new_title:
|
|
13160
|
-
|
|
13212
|
+
for subfield in cast(list[dict[str, Any]], field.get("subfields") or []):
|
|
13213
|
+
visit(subfield)
|
|
13214
|
+
return
|
|
13161
13215
|
que_id = _coerce_nonnegative_int(field.get("que_id"))
|
|
13162
13216
|
if que_id is None:
|
|
13163
13217
|
que_id = _coerce_nonnegative_int(template.get("queId"))
|
|
13164
13218
|
if que_id is not None:
|
|
13165
13219
|
by_que_id[que_id] = new_title
|
|
13166
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)
|
|
13167
13226
|
return by_que_id, by_title
|
|
13168
13227
|
|
|
13169
13228
|
|
|
@@ -13200,6 +13259,20 @@ def _sync_question_title_references(value: Any, *, by_que_id: dict[int, str], by
|
|
|
13200
13259
|
_sync_question_title_references(child_value, by_que_id=by_que_id, by_title=by_title)
|
|
13201
13260
|
|
|
13202
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
|
+
|
|
13203
13276
|
def _materialize_preserved_question(field: dict[str, Any]) -> dict[str, Any] | None:
|
|
13204
13277
|
template = deepcopy(field.get("_question_template"))
|
|
13205
13278
|
if not isinstance(template, dict):
|
|
@@ -13212,6 +13285,8 @@ def _materialize_preserved_question(field: dict[str, Any]) -> dict[str, Any] | N
|
|
|
13212
13285
|
if "description" in overlay_keys:
|
|
13213
13286
|
description = field.get("description")
|
|
13214
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)
|
|
13215
13290
|
if str(field.get("type") or "") == FieldType.relation.value:
|
|
13216
13291
|
return _normalize_relation_question_for_save(template, field=field)
|
|
13217
13292
|
return template
|
|
@@ -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",
|