@qingflow-tech/qingflow-app-user-mcp 1.0.5 → 1.0.7

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 CHANGED
@@ -3,13 +3,13 @@
3
3
  Install:
4
4
 
5
5
  ```bash
6
- npm install @qingflow-tech/qingflow-app-user-mcp@1.0.5
6
+ npm install @qingflow-tech/qingflow-app-user-mcp@1.0.7
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @qingflow-tech/qingflow-app-user-mcp@1.0.5 qingflow-app-user-mcp
12
+ npx -y -p @qingflow-tech/qingflow-app-user-mcp@1.0.7 qingflow-app-user-mcp
13
13
  ```
14
14
 
15
15
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qingflow-tech/qingflow-app-user-mcp",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Operational end-user MCP for Qingflow records, tasks, comments, and directory workflows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qingflow-mcp"
7
- version = "1.0.5"
7
+ version = "1.0.7"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -13,10 +13,11 @@ For final statistics, grouped distributions, rankings, trends, or insight-style
13
13
 
14
14
  ## Direct Writes
15
15
 
16
- - `record_insert` is schema-first through `record_insert_schema_get`
16
+ - `record_insert` is schema-first through `record_insert_schema_get`; default to `items=[{"fields": {...}}]`
17
17
  - `record_update` is schema-first through `record_update_schema_get`
18
18
  - `record_delete` does not need a schema-get step
19
- - If a direct-write tool returns `ok=false`, the write was blocked and not executed
19
+ - For batch insert, `partial_success` means some rows were created; use `created_record_ids`, failed `row_number`, and `failed_fields` to repair only failed rows
20
+ - If a direct-write tool returns `write_executed=false`, the write was blocked and not executed for that item
20
21
  - Prefer `verify_write=true` for complex, relation-heavy, subtable, or production writes
21
22
 
22
23
  ## Lookup Fields
@@ -13,7 +13,7 @@ It is not a user-facing product spec. It exists to prevent skill drift.
13
13
  - analyze: `app_get -> record_browse_schema_get -> record_access -> Python`
14
14
  - browse detail: `app_get -> record_browse_schema_get -> record_list / record_get`
15
15
  - explicit export/download/Excel: `view_get -> record_export_*` or `record_export_direct`
16
- - insert: `record_insert_schema_get -> record_insert`
16
+ - insert: `record_insert_schema_get -> record_insert(items)`
17
17
  - update: `record_update_schema_get -> record_update`
18
18
 
19
19
  ### Tasks
@@ -30,13 +30,14 @@ Use `record_browse_schema_get -> record_get` when:
30
30
 
31
31
  ## Insert Pattern
32
32
 
33
- Use `record_insert_schema_get -> record_insert`.
33
+ Use `record_insert_schema_get -> record_insert(items)`.
34
34
 
35
35
  1. Confirm the target app
36
36
  2. Read `required_fields`, `optional_fields`, `runtime_linked_required_fields`, and `payload_template`
37
- 3. Build a field-title keyed `fields` map
38
- 4. If lookup fields are ambiguous, stop and ask for confirmation
39
- 5. Run `record_insert`
37
+ 3. Build `items` as `[{"fields": {...}}]`; a single record is one item
38
+ 4. Write member, department, and relation fields with natural strings first when the user provided names
39
+ 5. If lookup fields are ambiguous, stop and ask for confirmation
40
+ 6. On `partial_success`, keep `created_record_ids` and only repair failed `row_number` / `failed_fields`
40
41
 
41
42
  ## Update Pattern
42
43
 
@@ -9,7 +9,9 @@ metadata:
9
9
 
10
10
  ## Default Path
11
11
 
12
- `record_insert_schema_get -> (optional candidate lookup / confirmation) -> record_insert`
12
+ `record_insert_schema_get -> record_insert(items) -> optional record_get/readback`
13
+
14
+ Default to batch-shaped insert. A single new record is `items` with one row.
13
15
 
14
16
  ## Core Tools
15
17
 
@@ -25,19 +27,20 @@ metadata:
25
27
  2. Read `required_fields`, `optional_fields`, `runtime_linked_required_fields`, and `payload_template`
26
28
  3. Inside every field bucket, read field-level `linkage` first when present; it is the canonical static hint for linked visibility, reference-driven auto fill, or formula-driven fields
27
29
  4. Inside `optional_fields`, pay special attention to any field with `may_become_required=true`; these are writable fields that can become required when linked visibility or option-driven rules activate
28
- 5. Build `fields` from the manually-supplied required/optional fields in that schema
30
+ 5. Build `items` as `[{"fields": {...}}]`, where each `fields` map uses field titles from the insert schema
29
31
  6. Treat `runtime_linked_required_fields` as required-but-not-directly-writable runtime/upstream dependencies, not as fields to hand-fill blindly
30
32
  7. For `linkage.kind=logic_visibility`, read `sources` as upstream trigger fields and treat `role=manual_input_after_activation` as "fill this only after the upstream condition is satisfied"
31
33
  8. For `linkage.kind=reference_fill`, prefer filling the source field first; treat target fields with `role=auto_fill_preferred` or `auto_fill_only` as reference-driven outputs rather than blind manual inputs
32
34
  9. For `linkage.kind=formula_fill`, treat the field as formula/default-auto-fill driven unless the user explicitly asks to override it and the field is still writable
33
35
  10. If insert succeeds and single-record detail/readback matters, prefer `record_get`; use `record_list(..., output_profile="normalized")` only for batch row-shaped normalized readback
34
36
  11. Keep subtable payloads under the parent field as a row array
35
- 12. Member / department / relation fields may accept natural strings directly
37
+ 12. Member / department / relation fields may accept natural strings directly, such as `"张三"`, `"直销部"`, or `"海军军医大学"`; do not pre-query ids by default
36
38
  13. If the write returns `status="needs_confirmation"`, stop and surface the candidates
37
- 14. Retry with explicit ids / objects only after the user confirms
39
+ 14. Retry failed rows only with explicit ids / objects after the user confirms
38
40
  15. Keep `verify_write=true` for production inserts
39
41
  16. If post-write detail context matters, read `record_get.fields[]`, `media_assets.items[].local_path`, `file_assets.items[].local_path`, `file_assets.items[].extraction.text_path`, and `semantic_context`; `record_get` follows the frontend storage cookie redirect path for Qingflow attachments, so prefer local paths over remote URLs and do not expect legacy `data.normalized_record`
40
42
  17. Treat nested schema shape as guidance, not a brittle contract; do not hard-code transient implementation details like optional nested `field_id` shape when composing inserts
43
+ 18. For `partial_success`, read `created_record_ids`, then repair only the failed `items[].row_number` using `failed_fields`; never retry the whole batch after any row has `write_executed=true`
41
44
 
42
45
  ## Field Notes
43
46
 
@@ -51,11 +54,29 @@ metadata:
51
54
  - `linkage.affects_fields` lists downstream field titles that may change when this field changes
52
55
  - `linkage.role=auto_fill_only` means "normally do not hand-fill this unless the product explicitly requires it"
53
56
  - `requires_upload=true` means upload the file first, then write the returned value
57
+ - `failed_fields[].next_action` tells the next repair step for that row
58
+
59
+ ## CLI Pattern
60
+
61
+ Use a JSON array file:
62
+
63
+ ```bash
64
+ qingflow record insert --app-key APP_KEY --items-file records.json --json
65
+ ```
66
+
67
+ `records.json`:
68
+
69
+ ```json
70
+ [
71
+ { "fields": { "客户名称": "测试客户", "负责人": "张三" } }
72
+ ]
73
+ ```
54
74
 
55
75
  ## Do Not
56
76
 
57
77
  - Do not skip `record_insert_schema_get`
58
78
  - Do not invent missing required fields
59
79
  - Do not flatten subtable leaf fields to the top level
60
- - Do not silently guess member / department / relation ids
80
+ - Do not pre-query or silently guess member / department / relation ids when a natural string is enough
81
+ - Do not retry a whole batch after `created_record_ids` is non-empty
61
82
  - Do not bind logic to a transient nested schema serialization detail when the field title and parent table already identify the legal payload shape
@@ -115,6 +115,13 @@ class PublicViewButtonType(str, Enum):
115
115
  class PublicViewButtonConfigType(str, Enum):
116
116
  top = "TOP"
117
117
  detail = "DETAIL"
118
+ list = "LIST"
119
+
120
+
121
+ class PublicButtonPlacement(str, Enum):
122
+ header = "header"
123
+ detail = "detail"
124
+ list = "list"
118
125
 
119
126
 
120
127
  class PublicChartType(str, Enum):
@@ -739,6 +746,8 @@ class FieldPatch(StrictModel):
739
746
  default=None,
740
747
  validation_alias=AliasChoices("custom_button_text", "customButtonText", "custom_btn_text", "customBtnText"),
741
748
  )
749
+ as_data_title: bool | None = Field(default=None, validation_alias=AliasChoices("as_data_title", "asDataTitle"))
750
+ as_data_cover: bool | None = Field(default=None, validation_alias=AliasChoices("as_data_cover", "asDataCover"))
742
751
  subfields: list["FieldPatch"] = Field(default_factory=list)
743
752
 
744
753
  @model_validator(mode="after")
@@ -819,6 +828,8 @@ class FieldMutation(StrictModel):
819
828
  default=None,
820
829
  validation_alias=AliasChoices("custom_button_text", "customButtonText", "custom_btn_text", "customBtnText"),
821
830
  )
831
+ as_data_title: bool | None = Field(default=None, validation_alias=AliasChoices("as_data_title", "asDataTitle"))
832
+ as_data_cover: bool | None = Field(default=None, validation_alias=AliasChoices("as_data_cover", "asDataCover"))
822
833
  subfields: list[FieldPatch] | None = None
823
834
  subfield_updates: list["FieldUpdatePatch"] | None = Field(
824
835
  default=None,
@@ -1013,6 +1024,27 @@ class FlowTransitionPatch(StrictModel):
1013
1024
  target: str = Field(alias="to")
1014
1025
 
1015
1026
 
1027
+ class ViewQueryConditionsPatch(StrictModel):
1028
+ enabled: bool = Field(default=True, validation_alias=AliasChoices("enabled", "queryConditionStatus", "status"))
1029
+ exact: bool = Field(default=False, validation_alias=AliasChoices("exact", "queryConditionExact"))
1030
+ hide_before_query: bool = Field(default=False, validation_alias=AliasChoices("hide_before_query", "hideBeforeQuery", "hideBeforeQueryCondition"))
1031
+ rows: list[list[str | int]] = Field(default_factory=list, validation_alias=AliasChoices("rows", "fields", "queryCondition"))
1032
+
1033
+
1034
+ class ViewAssociatedResourcesPatch(StrictModel):
1035
+ visible: bool = Field(default=True, validation_alias=AliasChoices("visible", "enabled", "asosChartVisible"))
1036
+ limit_type: str | None = Field(default=None, validation_alias=AliasChoices("limit_type", "limitType"))
1037
+ associated_item_ids: list[Any] = Field(
1038
+ default_factory=list,
1039
+ validation_alias=AliasChoices(
1040
+ "associated_item_ids",
1041
+ "associatedItemIds",
1042
+ "asosChartIdList",
1043
+ "items",
1044
+ ),
1045
+ )
1046
+
1047
+
1016
1048
  class ViewUpsertPatch(StrictModel):
1017
1049
  name: str
1018
1050
  view_key: str | None = Field(default=None, validation_alias=AliasChoices("view_key", "viewKey"))
@@ -1025,6 +1057,17 @@ class ViewUpsertPatch(StrictModel):
1025
1057
  title_field: str | None = Field(default=None, validation_alias=AliasChoices("title_field", "titleField"))
1026
1058
  buttons: list["ViewButtonBindingPatch"] | None = None
1027
1059
  visibility: VisibilityPatch | None = None
1060
+ query_conditions: ViewQueryConditionsPatch | None = Field(default=None, validation_alias=AliasChoices("query_conditions", "queryConditions", "query_condition", "queryCondition"))
1061
+ associated_resources: ViewAssociatedResourcesPatch | None = Field(
1062
+ default=None,
1063
+ validation_alias=AliasChoices(
1064
+ "associated_resources",
1065
+ "associatedResources",
1066
+ "associated_reports",
1067
+ "associatedReports",
1068
+ "asosChartConfig",
1069
+ ),
1070
+ )
1028
1071
 
1029
1072
  @model_validator(mode="before")
1030
1073
  @classmethod
@@ -1042,6 +1085,20 @@ class ViewUpsertPatch(StrictModel):
1042
1085
  payload["filters"] = payload.pop("filter_rules")
1043
1086
  if "filterRules" in payload and "filters" not in payload:
1044
1087
  payload["filters"] = payload.pop("filterRules")
1088
+ if "queryConditions" in payload and "query_conditions" not in payload:
1089
+ payload["query_conditions"] = payload.pop("queryConditions")
1090
+ if "query_condition" in payload and "query_conditions" not in payload:
1091
+ payload["query_conditions"] = payload.pop("query_condition")
1092
+ if "queryCondition" in payload and "query_conditions" not in payload:
1093
+ payload["query_conditions"] = payload.pop("queryCondition")
1094
+ if "associatedResources" in payload and "associated_resources" not in payload:
1095
+ payload["associated_resources"] = payload.pop("associatedResources")
1096
+ if "associated_reports" in payload and "associated_resources" not in payload:
1097
+ payload["associated_resources"] = payload.pop("associated_reports")
1098
+ if "associatedReports" in payload and "associated_resources" not in payload:
1099
+ payload["associated_resources"] = payload.pop("associatedReports")
1100
+ if "asosChartConfig" in payload and "associated_resources" not in payload:
1101
+ payload["associated_resources"] = payload.pop("asosChartConfig")
1045
1102
  raw_type = payload.get("type")
1046
1103
  if isinstance(raw_type, str):
1047
1104
  normalized = raw_type.strip().lower()
@@ -1128,13 +1185,29 @@ class CustomButtonMatchRulePatch(StrictModel):
1128
1185
  return payload
1129
1186
 
1130
1187
 
1188
+ class CustomButtonFieldMappingPatch(StrictModel):
1189
+ source_field: Any = Field(validation_alias=AliasChoices("source_field", "sourceField", "source"))
1190
+ target_field: Any = Field(validation_alias=AliasChoices("target_field", "targetField", "target"))
1191
+
1192
+
1131
1193
  class CustomButtonAddDataConfigPatch(StrictModel):
1132
- related_app_key: str | None = Field(default=None, validation_alias=AliasChoices("related_app_key", "relatedAppKey"))
1194
+ related_app_key: str | None = Field(
1195
+ default=None,
1196
+ validation_alias=AliasChoices("related_app_key", "relatedAppKey", "target_app_key", "targetAppKey"),
1197
+ )
1133
1198
  related_app_name: str | None = Field(default=None, validation_alias=AliasChoices("related_app_name", "relatedAppName"))
1134
1199
  que_relation: list[CustomButtonMatchRulePatch] = Field(
1135
1200
  default_factory=list,
1136
1201
  validation_alias=AliasChoices("que_relation", "queRelation"),
1137
1202
  )
1203
+ field_mappings: list[CustomButtonFieldMappingPatch] = Field(
1204
+ default_factory=list,
1205
+ validation_alias=AliasChoices("field_mappings", "fieldMappings", "mappings"),
1206
+ )
1207
+ default_values: dict[str, Any] = Field(
1208
+ default_factory=dict,
1209
+ validation_alias=AliasChoices("default_values", "defaultValues", "defaults"),
1210
+ )
1138
1211
 
1139
1212
 
1140
1213
  class CustomButtonExternalQRobotConfigPatch(StrictModel):
@@ -1241,6 +1314,184 @@ class CustomButtonPatch(StrictModel):
1241
1314
  return self
1242
1315
 
1243
1316
 
1317
+ class CustomButtonUpsertPatch(CustomButtonPatch):
1318
+ button_id: int | None = Field(default=None, validation_alias=AliasChoices("button_id", "buttonId", "id"))
1319
+ client_key: str | None = Field(default=None, validation_alias=AliasChoices("client_key", "clientKey"))
1320
+
1321
+
1322
+ class CustomButtonRemovePatch(StrictModel):
1323
+ button_id: int | None = Field(default=None, validation_alias=AliasChoices("button_id", "buttonId", "id"))
1324
+ button_text: str | None = Field(default=None, validation_alias=AliasChoices("button_text", "buttonText", "name"))
1325
+
1326
+ @model_validator(mode="after")
1327
+ def validate_selector(self) -> "CustomButtonRemovePatch":
1328
+ if self.button_id is None and not str(self.button_text or "").strip():
1329
+ raise ValueError("remove_buttons[] requires button_id or button_text")
1330
+ return self
1331
+
1332
+
1333
+ class CustomButtonViewButtonBindingPatch(StrictModel):
1334
+ button_ref: Any = Field(validation_alias=AliasChoices("button_ref", "buttonRef", "button_id", "buttonId", "id"))
1335
+ placement: PublicButtonPlacement = Field(default=PublicButtonPlacement.detail, validation_alias=AliasChoices("placement", "position"))
1336
+ primary: bool = Field(default=False, validation_alias=AliasChoices("primary", "being_main", "beingMain"))
1337
+ button_limit: list[list[ViewFilterRulePatch]] = Field(
1338
+ default_factory=list,
1339
+ validation_alias=AliasChoices("button_limit", "buttonLimit", "visible_when", "visibleWhen"),
1340
+ )
1341
+ button_formula: str | None = Field(default=None, validation_alias=AliasChoices("button_formula", "buttonFormula"))
1342
+ button_formula_type: int = Field(default=1, validation_alias=AliasChoices("button_formula_type", "buttonFormulaType"))
1343
+ print_tpls: list[Any] = Field(default_factory=list, validation_alias=AliasChoices("print_tpls", "printTpls"))
1344
+
1345
+ @model_validator(mode="before")
1346
+ @classmethod
1347
+ def normalize_aliases(cls, value: Any) -> Any:
1348
+ if not isinstance(value, dict):
1349
+ return value
1350
+ payload = dict(value)
1351
+ raw_placement = payload.get("placement", payload.get("position", payload.get("config_type", payload.get("configType"))))
1352
+ if isinstance(raw_placement, str):
1353
+ normalized = raw_placement.strip().lower()
1354
+ if normalized in {"top", "header"}:
1355
+ payload["placement"] = PublicButtonPlacement.header.value
1356
+ elif normalized in {"detail", "data_detail"}:
1357
+ payload["placement"] = PublicButtonPlacement.detail.value
1358
+ elif normalized in {"list", "row", "row_action"}:
1359
+ payload["placement"] = PublicButtonPlacement.list.value
1360
+ raw_limits = payload.get("button_limit", payload.get("buttonLimit", payload.get("visible_when", payload.get("visibleWhen"))))
1361
+ if isinstance(raw_limits, list) and raw_limits and all(isinstance(item, dict) for item in raw_limits):
1362
+ payload["button_limit"] = [raw_limits]
1363
+ return payload
1364
+
1365
+
1366
+ class CustomButtonViewConfigPatch(StrictModel):
1367
+ view_key: str = Field(validation_alias=AliasChoices("view_key", "viewKey", "viewgraphKey", "viewGraphKey"))
1368
+ mode: str = Field(default="merge", validation_alias=AliasChoices("mode", "apply_mode", "applyMode"))
1369
+ buttons: list[CustomButtonViewButtonBindingPatch] = Field(default_factory=list)
1370
+
1371
+ @model_validator(mode="after")
1372
+ def validate_mode(self) -> "CustomButtonViewConfigPatch":
1373
+ normalized = str(self.mode or "").strip().lower()
1374
+ if normalized not in {"merge", "replace"}:
1375
+ raise ValueError("view_configs[].mode must be merge or replace")
1376
+ self.mode = normalized
1377
+ if normalized == "merge" and "buttons" not in getattr(self, "model_fields_set", set()):
1378
+ raise ValueError("view_configs[] in merge mode requires buttons; pass buttons: [] or mode=replace to clear existing bindings")
1379
+ return self
1380
+
1381
+
1382
+ class CustomButtonsApplyRequest(StrictModel):
1383
+ app_key: str
1384
+ upsert_buttons: list[CustomButtonUpsertPatch] = Field(default_factory=list)
1385
+ remove_buttons: list[CustomButtonRemovePatch] = Field(default_factory=list)
1386
+ view_configs: list[CustomButtonViewConfigPatch] = Field(default_factory=list)
1387
+
1388
+ @model_validator(mode="before")
1389
+ @classmethod
1390
+ def normalize_aliases(cls, value: Any) -> Any:
1391
+ if not isinstance(value, dict):
1392
+ return value
1393
+ payload = dict(value)
1394
+ if "buttons" in payload and "upsert_buttons" not in payload:
1395
+ payload["upsert_buttons"] = payload.pop("buttons")
1396
+ if "upsertButtons" in payload and "upsert_buttons" not in payload:
1397
+ payload["upsert_buttons"] = payload.pop("upsertButtons")
1398
+ if "removeButtons" in payload and "remove_buttons" not in payload:
1399
+ payload["remove_buttons"] = payload.pop("removeButtons")
1400
+ if "viewConfigs" in payload and "view_configs" not in payload:
1401
+ payload["view_configs"] = payload.pop("viewConfigs")
1402
+ return payload
1403
+
1404
+ @model_validator(mode="after")
1405
+ def validate_shape(self) -> "CustomButtonsApplyRequest":
1406
+ if not self.upsert_buttons and not self.remove_buttons and not self.view_configs:
1407
+ raise ValueError("custom button apply requires at least one upsert, remove, or view config operation")
1408
+ return self
1409
+
1410
+
1411
+ class AssociatedResourceUpsertPatch(StrictModel):
1412
+ client_key: str | None = Field(default=None, validation_alias=AliasChoices("client_key", "clientKey"))
1413
+ associated_item_id: int | None = Field(
1414
+ default=None,
1415
+ validation_alias=AliasChoices("associated_item_id", "associatedItemId", "asosChartId", "id"),
1416
+ )
1417
+ graph_type: str = Field(validation_alias=AliasChoices("graph_type", "graphType"))
1418
+ target_app_key: str = Field(validation_alias=AliasChoices("target_app_key", "targetAppKey", "app_key", "appKey"))
1419
+ chart_key: str | None = Field(default=None, validation_alias=AliasChoices("chart_key", "chartKey"))
1420
+ view_key: str | None = Field(default=None, validation_alias=AliasChoices("view_key", "viewKey", "viewgraphKey", "viewGraphKey"))
1421
+ report_source: str | None = Field(default=None, validation_alias=AliasChoices("report_source", "reportSource"))
1422
+ source_type: str | None = Field(default=None, validation_alias=AliasChoices("source_type", "sourceType"))
1423
+ match_rules: list[CustomButtonMatchRulePatch] = Field(default_factory=list, validation_alias=AliasChoices("match_rules", "matchRules"))
1424
+
1425
+ @model_validator(mode="before")
1426
+ @classmethod
1427
+ def normalize_aliases(cls, value: Any) -> Any:
1428
+ if not isinstance(value, dict):
1429
+ return value
1430
+ payload = dict(value)
1431
+ if "chart_id" in payload and "chart_key" not in payload and "chartKey" not in payload:
1432
+ payload["chart_key"] = str(payload.pop("chart_id"))
1433
+ raw_graph_type = str(payload.get("graph_type", payload.get("graphType", "")) or "").strip().lower()
1434
+ raw_source_type = payload.get("source_type", payload.get("sourceType"))
1435
+ has_report_source = "report_source" in payload or "reportSource" in payload
1436
+ if isinstance(raw_source_type, str):
1437
+ normalized_source = raw_source_type.strip().upper()
1438
+ if normalized_source == "BI_QINGFLOW" and not has_report_source:
1439
+ payload["report_source"] = "app"
1440
+ payload.pop("source_type", None)
1441
+ payload.pop("sourceType", None)
1442
+ elif normalized_source == "BI_DATASET" and not has_report_source:
1443
+ payload["report_source"] = "dataset"
1444
+ payload.pop("source_type", None)
1445
+ payload.pop("sourceType", None)
1446
+ elif normalized_source == "QINGFLOW" and raw_graph_type in {"view", "viewgraph"}:
1447
+ payload.pop("source_type", None)
1448
+ payload.pop("sourceType", None)
1449
+ return payload
1450
+
1451
+
1452
+ class AssociatedResourceViewConfigPatch(StrictModel):
1453
+ view_key: str = Field(validation_alias=AliasChoices("view_key", "viewKey", "viewgraphKey", "viewGraphKey"))
1454
+ visible: bool = Field(default=True, validation_alias=AliasChoices("visible", "enabled", "asosChartVisible"))
1455
+ limit_type: str | None = Field(default=None, validation_alias=AliasChoices("limit_type", "limitType"))
1456
+ associated_item_ids: list[Any] = Field(
1457
+ default_factory=list,
1458
+ validation_alias=AliasChoices("associated_item_ids", "associatedItemIds", "asosChartIdList"),
1459
+ )
1460
+ associated_item_refs: list[str] = Field(default_factory=list, validation_alias=AliasChoices("associated_item_refs", "associatedItemRefs", "refs"))
1461
+
1462
+
1463
+ class AssociatedResourcesApplyRequest(StrictModel):
1464
+ app_key: str
1465
+ upsert_resources: list[AssociatedResourceUpsertPatch] = Field(default_factory=list)
1466
+ remove_associated_item_ids: list[int] = Field(default_factory=list)
1467
+ reorder_associated_item_ids: list[int] = Field(default_factory=list)
1468
+ view_configs: list[AssociatedResourceViewConfigPatch] = Field(default_factory=list)
1469
+
1470
+ @model_validator(mode="before")
1471
+ @classmethod
1472
+ def normalize_aliases(cls, value: Any) -> Any:
1473
+ if not isinstance(value, dict):
1474
+ return value
1475
+ payload = dict(value)
1476
+ if "upsertResources" in payload and "upsert_resources" not in payload:
1477
+ payload["upsert_resources"] = payload.pop("upsertResources")
1478
+ if "resources" in payload and "upsert_resources" not in payload:
1479
+ payload["upsert_resources"] = payload.pop("resources")
1480
+ if "removeAssociatedItemIds" in payload and "remove_associated_item_ids" not in payload:
1481
+ payload["remove_associated_item_ids"] = payload.pop("removeAssociatedItemIds")
1482
+ if "reorderAssociatedItemIds" in payload and "reorder_associated_item_ids" not in payload:
1483
+ payload["reorder_associated_item_ids"] = payload.pop("reorderAssociatedItemIds")
1484
+ if "viewConfigs" in payload and "view_configs" not in payload:
1485
+ payload["view_configs"] = payload.pop("viewConfigs")
1486
+ return payload
1487
+
1488
+ @model_validator(mode="after")
1489
+ def validate_shape(self) -> "AssociatedResourcesApplyRequest":
1490
+ if not self.upsert_resources and not self.remove_associated_item_ids and not self.reorder_associated_item_ids and not self.view_configs:
1491
+ raise ValueError("associated resources apply requires at least one resource or view config operation")
1492
+ return self
1493
+
1494
+
1244
1495
  class ViewButtonBindingPatch(StrictModel):
1245
1496
  button_type: PublicViewButtonType = Field(validation_alias=AliasChoices("button_type", "buttonType"))
1246
1497
  config_type: PublicViewButtonConfigType = Field(validation_alias=AliasChoices("config_type", "configType"))
@@ -1278,10 +1529,12 @@ class ViewButtonBindingPatch(StrictModel):
1278
1529
  raw_config_type = payload.get("config_type", payload.get("configType"))
1279
1530
  if isinstance(raw_config_type, str):
1280
1531
  normalized_config = raw_config_type.strip().lower()
1281
- if normalized_config == "top":
1532
+ if normalized_config in {"top", "header"}:
1282
1533
  payload["config_type"] = "TOP"
1283
1534
  elif normalized_config == "detail":
1284
1535
  payload["config_type"] = "DETAIL"
1536
+ elif normalized_config in {"list", "row", "row_action"}:
1537
+ payload["config_type"] = "LIST"
1285
1538
  raw_limits = payload.get("button_limit", payload.get("buttonLimit"))
1286
1539
  if isinstance(raw_limits, list) and raw_limits and all(isinstance(item, dict) for item in raw_limits):
1287
1540
  payload["button_limit"] = [raw_limits]
@@ -1597,15 +1850,25 @@ class AppGetResponse(StrictModel):
1597
1850
  field_count: int = 0
1598
1851
  layout_section_count: int = 0
1599
1852
  view_count: int = 0
1853
+ chart_count: int = 0
1854
+ associated_resource_count: int = 0
1855
+ custom_button_count: int = 0
1600
1856
  workflow_enabled: bool = False
1857
+ counts: dict[str, int] = Field(default_factory=dict)
1858
+ views: list[dict[str, Any]] = Field(default_factory=list)
1859
+ charts: list[dict[str, Any]] = Field(default_factory=list)
1860
+ associated_resources: list[dict[str, Any]] = Field(default_factory=list)
1861
+ custom_buttons: list[dict[str, Any]] = Field(default_factory=list)
1601
1862
  verification_hints: list[str] = Field(default_factory=list)
1602
1863
  editability: dict[str, bool | None] = Field(default_factory=dict)
1864
+ form_settings: dict[str, Any] = Field(default_factory=dict)
1603
1865
 
1604
1866
 
1605
1867
  class AppGetFieldsResponse(StrictModel):
1606
1868
  app_key: str
1607
1869
  fields: list[dict[str, Any]] = Field(default_factory=list)
1608
1870
  field_count: int = 0
1871
+ form_settings: dict[str, Any] = Field(default_factory=dict)
1609
1872
 
1610
1873
 
1611
1874
  class AppGetLayoutResponse(StrictModel):
@@ -1681,6 +1944,7 @@ class ViewGetResponse(StrictModel):
1681
1944
  config: dict[str, Any] = Field(default_factory=dict)
1682
1945
  questions: list[dict[str, Any]] = Field(default_factory=list)
1683
1946
  associations: list[dict[str, Any]] = Field(default_factory=list)
1947
+ associated_resources_config: dict[str, Any] = Field(default_factory=dict)
1684
1948
 
1685
1949
 
1686
1950
  class ChartGetResponse(StrictModel):
@@ -1879,7 +2143,10 @@ def _normalize_public_department_scope_mode(value: Any) -> str | None:
1879
2143
 
1880
2144
 
1881
2145
  CustomButtonMatchRulePatch.model_rebuild()
2146
+ CustomButtonFieldMappingPatch.model_rebuild()
1882
2147
  CustomButtonAddDataConfigPatch.model_rebuild()
2148
+ CustomButtonViewButtonBindingPatch.model_rebuild()
2149
+ CustomButtonViewConfigPatch.model_rebuild()
1883
2150
  CodeBlockAliasPathPatch.model_rebuild()
1884
2151
  ViewButtonBindingPatch.model_rebuild()
1885
2152
  ViewUpsertPatch.model_rebuild()