@josephyan/qingflow-app-builder-mcp 0.2.0-beta.7 → 0.2.0-beta.71

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.
Files changed (70) hide show
  1. package/README.md +5 -3
  2. package/docs/local-agent-install.md +21 -5
  3. package/npm/bin/qingflow-app-builder-mcp.mjs +1 -1
  4. package/npm/lib/runtime.mjs +168 -12
  5. package/package.json +1 -1
  6. package/pyproject.toml +4 -1
  7. package/skills/qingflow-app-builder/SKILL.md +155 -22
  8. package/skills/qingflow-app-builder/references/create-app.md +51 -21
  9. package/skills/qingflow-app-builder/references/environments.md +1 -1
  10. package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
  11. package/skills/qingflow-app-builder/references/gotchas.md +28 -1
  12. package/skills/qingflow-app-builder/references/solution-playbooks.md +14 -12
  13. package/skills/qingflow-app-builder/references/tool-selection.md +47 -19
  14. package/skills/qingflow-app-builder/references/update-flow.md +112 -25
  15. package/skills/qingflow-app-builder/references/update-layout.md +11 -24
  16. package/skills/qingflow-app-builder/references/update-schema.md +1 -23
  17. package/skills/qingflow-app-builder/references/update-views.md +87 -21
  18. package/skills/qingflow-app-builder-code-integrations/SKILL.md +137 -0
  19. package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +4 -0
  20. package/skills/qingflow-app-builder-code-integrations/references/code-block.md +66 -0
  21. package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +77 -0
  22. package/src/qingflow_mcp/__init__.py +1 -1
  23. package/src/qingflow_mcp/backend_client.py +210 -0
  24. package/src/qingflow_mcp/builder_facade/models.py +1252 -3
  25. package/src/qingflow_mcp/builder_facade/service.py +11367 -2389
  26. package/src/qingflow_mcp/cli/__init__.py +1 -0
  27. package/src/qingflow_mcp/cli/commands/__init__.py +15 -0
  28. package/src/qingflow_mcp/cli/commands/app.py +40 -0
  29. package/src/qingflow_mcp/cli/commands/auth.py +78 -0
  30. package/src/qingflow_mcp/cli/commands/builder.py +515 -0
  31. package/src/qingflow_mcp/cli/commands/common.py +62 -0
  32. package/src/qingflow_mcp/cli/commands/imports.py +96 -0
  33. package/src/qingflow_mcp/cli/commands/record.py +304 -0
  34. package/src/qingflow_mcp/cli/commands/task.py +89 -0
  35. package/src/qingflow_mcp/cli/commands/workspace.py +33 -0
  36. package/src/qingflow_mcp/cli/context.py +48 -0
  37. package/src/qingflow_mcp/cli/formatters.py +355 -0
  38. package/src/qingflow_mcp/cli/json_io.py +50 -0
  39. package/src/qingflow_mcp/cli/main.py +149 -0
  40. package/src/qingflow_mcp/config.py +39 -0
  41. package/src/qingflow_mcp/import_store.py +121 -0
  42. package/src/qingflow_mcp/list_type_labels.py +24 -0
  43. package/src/qingflow_mcp/response_trim.py +668 -0
  44. package/src/qingflow_mcp/server.py +160 -18
  45. package/src/qingflow_mcp/server_app_builder.py +275 -68
  46. package/src/qingflow_mcp/server_app_user.py +219 -191
  47. package/src/qingflow_mcp/session_store.py +41 -1
  48. package/src/qingflow_mcp/solution/compiler/form_compiler.py +43 -4
  49. package/src/qingflow_mcp/solution/compiler/icon_utils.py +119 -45
  50. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +41 -2
  51. package/src/qingflow_mcp/solution/executor.py +107 -11
  52. package/src/qingflow_mcp/solution/spec_models.py +2 -0
  53. package/src/qingflow_mcp/tools/ai_builder_tools.py +2032 -127
  54. package/src/qingflow_mcp/tools/app_tools.py +419 -12
  55. package/src/qingflow_mcp/tools/approval_tools.py +571 -72
  56. package/src/qingflow_mcp/tools/auth_tools.py +398 -2
  57. package/src/qingflow_mcp/tools/code_block_tools.py +756 -0
  58. package/src/qingflow_mcp/tools/custom_button_tools.py +179 -0
  59. package/src/qingflow_mcp/tools/directory_tools.py +203 -31
  60. package/src/qingflow_mcp/tools/feedback_tools.py +230 -0
  61. package/src/qingflow_mcp/tools/file_tools.py +1 -0
  62. package/src/qingflow_mcp/tools/import_tools.py +2150 -0
  63. package/src/qingflow_mcp/tools/package_tools.py +18 -4
  64. package/src/qingflow_mcp/tools/portal_tools.py +31 -0
  65. package/src/qingflow_mcp/tools/qingbi_report_tools.py +109 -7
  66. package/src/qingflow_mcp/tools/record_tools.py +9894 -1104
  67. package/src/qingflow_mcp/tools/solution_tools.py +115 -3
  68. package/src/qingflow_mcp/tools/task_context_tools.py +2040 -0
  69. package/src/qingflow_mcp/tools/task_tools.py +376 -225
  70. package/src/qingflow_mcp/tools/workspace_tools.py +163 -19
@@ -9,7 +9,7 @@ from ..backend_client import BackendRequestContext
9
9
  from ..config import DEFAULT_PROFILE
10
10
  from ..errors import QingflowApiError, raise_tool_error
11
11
  from ..json_types import JSONObject
12
- from ..list_type_labels import get_app_publish_status_label
12
+ from ..list_type_labels import SYSTEM_VIEW_DEFINITIONS, get_app_publish_status_label
13
13
  from .base import ToolBase
14
14
 
15
15
 
@@ -23,6 +23,10 @@ class AppTools(ToolBase):
23
23
  def app_search(profile: str = DEFAULT_PROFILE, keyword: str = "", page_num: int = 1, page_size: int = 50) -> JSONObject:
24
24
  return self.app_search(profile=profile, keyword=keyword, page_num=page_num, page_size=page_size)
25
25
 
26
+ @mcp.tool()
27
+ def app_get(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
28
+ return self.app_get(profile=profile, app_key=app_key)
29
+
26
30
  @mcp.tool()
27
31
  def app_get_base(profile: str = DEFAULT_PROFILE, app_key: str = "", include_raw: bool = False) -> JSONObject:
28
32
  return self.app_get_base(profile=profile, app_key=app_key, include_raw=include_raw)
@@ -76,14 +80,20 @@ class AppTools(ToolBase):
76
80
  return self.app_publish(profile=profile, app_key=app_key, payload=payload or {})
77
81
 
78
82
  def app_list(self, *, profile: str, ship_auth: bool = False) -> JSONObject:
79
- """Get all apps with full hierarchy from tag/apps endpoint."""
83
+ """List current-user visible apps in the selected workspace."""
80
84
  def runner(session_profile, context):
81
85
  result = self.backend.request("GET", context, "/tag/apps")
82
- return {
86
+ items, source_shape = self._extract_visible_apps(result)
87
+ response = {
83
88
  "profile": profile,
84
89
  "ws_id": session_profile.selected_ws_id,
85
- "items": result,
90
+ "items": items,
91
+ "count": len(items),
92
+ "source_shape": source_shape,
86
93
  }
94
+ if ship_auth:
95
+ response["raw"] = result
96
+ return response
87
97
 
88
98
  return self._run(profile, runner)
89
99
 
@@ -98,19 +108,20 @@ class AppTools(ToolBase):
98
108
 
99
109
  result = self.backend.request("GET", context, "/app/item", params=params)
100
110
 
101
- # Extract app list from the response
102
111
  apps = []
103
112
  if isinstance(result, dict):
104
113
  items = result.get("list", [])
105
114
  for item in items:
106
115
  if isinstance(item, dict):
107
- apps.append({
108
- "app_key": item.get("appKey"),
109
- "title": item.get("title") or item.get("formTitle"),
110
- "form_id": item.get("formId"),
111
- "tag_id": item.get("tagId"),
112
- "group_id": item.get("groupId"),
113
- })
116
+ normalized = self._normalize_visible_app(
117
+ item,
118
+ package_tag_id=_coerce_positive_int(item.get("tagId")),
119
+ package_name=str(item.get("tagName") or "").strip() or None,
120
+ group_id=_coerce_positive_int(item.get("groupId")),
121
+ group_name=str(item.get("groupName") or "").strip() or None,
122
+ )
123
+ if normalized is not None:
124
+ apps.append(normalized)
114
125
 
115
126
  return {
116
127
  "profile": profile,
@@ -119,11 +130,76 @@ class AppTools(ToolBase):
119
130
  "page_num": page_num,
120
131
  "page_size": page_size,
121
132
  "total": result.get("total") if isinstance(result, dict) else len(apps),
133
+ "items": apps,
122
134
  "apps": apps,
123
135
  }
124
136
 
125
137
  return self._run(profile, runner)
126
138
 
139
+ def app_get(self, *, profile: str, app_key: str) -> JSONObject:
140
+ self._require_app_key(app_key)
141
+
142
+ def runner(session_profile, context):
143
+ warnings: list[JSONObject] = []
144
+ app_name = app_key
145
+ base_info: JSONObject | None = None
146
+
147
+ try:
148
+ base_info = self.backend.request("GET", context, f"/app/{app_key}/baseInfo")
149
+ if isinstance(base_info, dict):
150
+ app_name = (
151
+ str(base_info.get("formTitle") or base_info.get("title") or base_info.get("appName") or app_key).strip()
152
+ or app_key
153
+ )
154
+ except QingflowApiError as exc:
155
+ if exc.backend_code not in {40002, 40027}:
156
+ raise
157
+ warnings.append(
158
+ {
159
+ "code": "APP_BASE_INFO_UNAVAILABLE",
160
+ "message": f"app_get could not load base info for {app_key}; using app_key as app_name.",
161
+ }
162
+ )
163
+
164
+ try:
165
+ can_create = self._probe_create_access(context, app_key)
166
+ except QingflowApiError as exc:
167
+ can_create = False
168
+ warnings.append(
169
+ {
170
+ "code": "APP_CREATE_PROBE_UNVERIFIED",
171
+ "message": (
172
+ f"app_get could not fully verify create access for {app_key}: "
173
+ f"{exc.message or 'probe failed'}"
174
+ ),
175
+ }
176
+ )
177
+ accessible_views, system_view_warnings = self._resolve_accessible_system_views(context, app_key)
178
+ warnings.extend(system_view_warnings)
179
+ accessible_views.extend(self._resolve_accessible_custom_views(context, app_key))
180
+ import_capability, import_warnings = _derive_import_capability(base_info)
181
+ warnings.extend(import_warnings)
182
+ editability, editability_warnings = _derive_editability(base_info)
183
+ warnings.extend(editability_warnings)
184
+
185
+ return {
186
+ "profile": profile,
187
+ "ws_id": session_profile.selected_ws_id,
188
+ "ok": True,
189
+ "request_route": {"base_url": context.base_url, "qf_version": context.qf_version},
190
+ "warnings": warnings,
191
+ "data": {
192
+ "app_key": app_key,
193
+ "app_name": app_name,
194
+ "can_create": can_create,
195
+ "import_capability": import_capability,
196
+ "editability": editability,
197
+ "accessible_views": accessible_views,
198
+ },
199
+ }
200
+
201
+ return self._run(profile, runner)
202
+
127
203
  def app_get_base(self, *, profile: str, app_key: str, include_raw: bool = False) -> JSONObject:
128
204
  self._require_app_key(app_key)
129
205
 
@@ -208,6 +284,42 @@ class AppTools(ToolBase):
208
284
 
209
285
  return self._run(profile, runner)
210
286
 
287
+ def app_get_apply_base_info(self, *, profile: str, app_key: str, list_type: int) -> JSONObject:
288
+ self._require_app_key(app_key)
289
+ if not isinstance(list_type, int) or isinstance(list_type, bool) or list_type <= 0:
290
+ raise_tool_error(QingflowApiError.config_error("list_type must be a positive integer"))
291
+
292
+ def runner(session_profile, context):
293
+ result = self.backend.request("GET", context, f"/app/{app_key}/apply/baseInfo", params={"type": list_type})
294
+ return {
295
+ "profile": profile,
296
+ "ws_id": session_profile.selected_ws_id,
297
+ "app_key": app_key,
298
+ "list_type": list_type,
299
+ "result": result,
300
+ }
301
+
302
+ return self._run(profile, runner)
303
+
304
+ def app_update_apply_config(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
305
+ self._require_app_key(app_key)
306
+ body = self._require_dict(payload)
307
+
308
+ def runner(session_profile, context):
309
+ result = self.backend.request("POST", context, f"/app/{app_key}/apply/config", json_body=body)
310
+ return self._attach_human_review_notice(
311
+ {
312
+ "profile": profile,
313
+ "ws_id": session_profile.selected_ws_id,
314
+ "app_key": app_key,
315
+ "result": result,
316
+ },
317
+ operation="update",
318
+ target="app apply list configuration",
319
+ )
320
+
321
+ return self._run(profile, runner)
322
+
211
323
  def app_update_form_schema(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
212
324
  self._require_app_key(app_key)
213
325
  body = self._require_dict(payload)
@@ -305,6 +417,83 @@ class AppTools(ToolBase):
305
417
  if not app_key:
306
418
  raise_tool_error(QingflowApiError.config_error("app_key is required"))
307
419
 
420
+ def _probe_create_access(self, context: BackendRequestContext, app_key: str) -> bool:
421
+ try:
422
+ self.backend.request(
423
+ "GET",
424
+ context,
425
+ f"/app/{app_key}/form",
426
+ params={"type": 2, "beingApply": True},
427
+ )
428
+ return True
429
+ except QingflowApiError as exc:
430
+ if exc.backend_code in {40002, 40027}:
431
+ return False
432
+ raise
433
+
434
+ def _probe_list_type_access(self, context: BackendRequestContext, app_key: str, list_type: int) -> bool:
435
+ try:
436
+ self.backend.request(
437
+ "POST",
438
+ context,
439
+ f"/app/{app_key}/apply/filter",
440
+ json_body={"type": list_type, "pageNum": 1, "pageSize": 1},
441
+ )
442
+ return True
443
+ except QingflowApiError as exc:
444
+ if exc.backend_code in {40002, 40027}:
445
+ return False
446
+ raise
447
+
448
+ def _resolve_accessible_system_views(self, context: BackendRequestContext, app_key: str) -> tuple[list[JSONObject], list[JSONObject]]:
449
+ items: list[JSONObject] = []
450
+ warnings: list[JSONObject] = []
451
+ for view_id, list_type, name in SYSTEM_VIEW_DEFINITIONS:
452
+ try:
453
+ can_access = self._probe_list_type_access(context, app_key, list_type)
454
+ except QingflowApiError as exc:
455
+ warnings.append(
456
+ {
457
+ "code": "SYSTEM_VIEW_PROBE_UNVERIFIED",
458
+ "message": (
459
+ f"app_get skipped system view '{name}' because access probing failed: "
460
+ f"{exc.message or 'probe failed'}"
461
+ ),
462
+ "view_id": view_id,
463
+ "list_type": list_type,
464
+ }
465
+ )
466
+ continue
467
+ if not can_access:
468
+ continue
469
+ items.append({"view_id": view_id, "name": name, "kind": "system", "analysis_supported": True})
470
+ return items, warnings
471
+
472
+ def _resolve_accessible_custom_views(self, context: BackendRequestContext, app_key: str) -> list[JSONObject]:
473
+ try:
474
+ payload = self.backend.request("GET", context, f"/app/{app_key}/view/viewList")
475
+ except QingflowApiError as exc:
476
+ if exc.backend_code in {40002, 40027}:
477
+ return []
478
+ raise
479
+
480
+ items: list[JSONObject] = []
481
+ for item in _normalize_view_list(payload):
482
+ view_key = str(item.get("viewKey") or "").strip()
483
+ if not view_key:
484
+ continue
485
+ normalized: JSONObject = {
486
+ "view_id": f"custom:{view_key}",
487
+ "name": str(item.get("viewName") or view_key).strip() or view_key,
488
+ "kind": "custom",
489
+ }
490
+ view_type = str(item.get("viewType") or item.get("viewgraphType") or "").strip()
491
+ if view_type:
492
+ normalized["view_type"] = view_type
493
+ normalized["analysis_supported"] = _analysis_supported_for_view_type(view_type or None)
494
+ items.append(normalized)
495
+ return items
496
+
308
497
  def _compact_base_info(self, result: dict[str, Any]) -> JSONObject:
309
498
  publish_status = result.get("appPublishStatus")
310
499
  auth = result.get("auth") if isinstance(result.get("auth"), dict) else {}
@@ -424,6 +613,88 @@ class AppTools(ToolBase):
424
613
  }
425
614
  return {key: value for key, value in compact.items() if value is not None}
426
615
 
616
+ def _extract_visible_apps(self, result: Any) -> tuple[list[JSONObject], str]:
617
+ apps: list[JSONObject] = []
618
+ seen: set[str] = set()
619
+
620
+ def walk(
621
+ node: Any,
622
+ *,
623
+ package_tag_id: int | None = None,
624
+ package_name: str | None = None,
625
+ group_id: int | None = None,
626
+ group_name: str | None = None,
627
+ ) -> None:
628
+ if isinstance(node, list):
629
+ for item in node:
630
+ walk(
631
+ item,
632
+ package_tag_id=package_tag_id,
633
+ package_name=package_name,
634
+ group_id=group_id,
635
+ group_name=group_name,
636
+ )
637
+ return
638
+ if not isinstance(node, dict):
639
+ return
640
+
641
+ next_package_tag_id = _coerce_positive_int(node.get("tagId")) or package_tag_id
642
+ next_package_name = str(node.get("tagName") or "").strip() or package_name
643
+ next_group_id = _coerce_positive_int(node.get("groupId")) or group_id
644
+ next_group_name = str(node.get("groupName") or node.get("groupTitle") or "").strip() or group_name
645
+
646
+ normalized = self._normalize_visible_app(
647
+ node,
648
+ package_tag_id=next_package_tag_id,
649
+ package_name=next_package_name,
650
+ group_id=next_group_id,
651
+ group_name=next_group_name,
652
+ )
653
+ if normalized is not None:
654
+ app_key = str(normalized.get("app_key") or "").strip()
655
+ if app_key and app_key not in seen:
656
+ seen.add(app_key)
657
+ apps.append(normalized)
658
+
659
+ for value in node.values():
660
+ if isinstance(value, (list, dict)):
661
+ walk(
662
+ value,
663
+ package_tag_id=next_package_tag_id,
664
+ package_name=next_package_name,
665
+ group_id=next_group_id,
666
+ group_name=next_group_name,
667
+ )
668
+
669
+ walk(result)
670
+ return apps, type(result).__name__
671
+
672
+ def _normalize_visible_app(
673
+ self,
674
+ item: dict[str, Any],
675
+ *,
676
+ package_tag_id: int | None,
677
+ package_name: str | None,
678
+ group_id: int | None,
679
+ group_name: str | None,
680
+ ) -> JSONObject | None:
681
+ app_key = str(item.get("appKey") or item.get("app_key") or "").strip()
682
+ if not app_key:
683
+ return None
684
+ title = str(item.get("title") or item.get("formTitle") or item.get("appName") or item.get("name") or app_key).strip() or app_key
685
+ tag_ids = item.get("tagIds") if isinstance(item.get("tagIds"), list) else []
686
+ compact = {
687
+ "app_key": app_key,
688
+ "title": title,
689
+ "form_id": item.get("formId"),
690
+ "tag_id": package_tag_id,
691
+ "package_name": package_name,
692
+ "group_id": group_id,
693
+ "group_name": group_name,
694
+ "tag_ids": [value for value in (_coerce_positive_int(tag_id) for tag_id in tag_ids) if value is not None],
695
+ }
696
+ return {key: value for key, value in compact.items() if value not in (None, [], "", {})}
697
+
427
698
  def _count_auth_members(self, auth_payload: Any, member_key: str) -> int:
428
699
  if not isinstance(auth_payload, dict):
429
700
  return 0
@@ -470,3 +741,139 @@ def _normalize_form_type(value: int | str) -> int:
470
741
  if text in FORM_TYPE_ALIASES:
471
742
  return FORM_TYPE_ALIASES[text]
472
743
  raise_tool_error(QingflowApiError.config_error("form_type must be a positive integer or one of: default, form, schema, new, draft, edit"))
744
+
745
+
746
+ def _normalize_view_list(payload: Any) -> list[JSONObject]:
747
+ if not isinstance(payload, list):
748
+ return []
749
+ flattened: list[JSONObject] = []
750
+ for group in payload:
751
+ if not isinstance(group, dict):
752
+ continue
753
+ view_list = group.get("viewList")
754
+ if not isinstance(view_list, list):
755
+ continue
756
+ for item in view_list:
757
+ if isinstance(item, dict) and item.get("viewKey"):
758
+ flattened.append(item)
759
+ return flattened
760
+
761
+
762
+ def _analysis_supported_for_view_type(view_type: str | None) -> bool:
763
+ normalized = str(view_type or "").strip().lower()
764
+ if not normalized:
765
+ return True
766
+ return normalized not in {"boardview", "ganttview"}
767
+
768
+
769
+ def _derive_import_capability(base_info: Any) -> tuple[JSONObject, list[JSONObject]]:
770
+ warnings: list[JSONObject] = []
771
+ if not isinstance(base_info, dict):
772
+ warnings.append(
773
+ {
774
+ "code": "IMPORT_CAPABILITY_UNAVAILABLE",
775
+ "message": "app_get could not determine import capability because baseInfo was unavailable.",
776
+ }
777
+ )
778
+ return _unknown_import_capability(), warnings
779
+
780
+ has_data_import_status = "dataImportStatus" in base_info
781
+ has_data_manage_status = "dataManageStatus" in base_info
782
+ applicant_import_enabled = _coerce_optional_bool(base_info.get("dataImportStatus")) if has_data_import_status else None
783
+ data_manage_status = _coerce_optional_bool(base_info.get("dataManageStatus")) if has_data_manage_status else None
784
+
785
+ if applicant_import_enabled is True:
786
+ return {
787
+ "can_import": True,
788
+ "auth_source": "apply_auth",
789
+ "applicant_import_enabled": True,
790
+ "data_manage_status": data_manage_status,
791
+ "runtime_checks_required": ["user_disabled", "function_demoted"],
792
+ "confidence": "preflight",
793
+ }, warnings
794
+
795
+ if data_manage_status is True:
796
+ return {
797
+ "can_import": True,
798
+ "auth_source": "data_manage_auth",
799
+ "applicant_import_enabled": applicant_import_enabled,
800
+ "data_manage_status": True,
801
+ "runtime_checks_required": ["user_disabled", "function_demoted"],
802
+ "confidence": "preflight",
803
+ }, warnings
804
+
805
+ if applicant_import_enabled is False and data_manage_status is False:
806
+ return {
807
+ "can_import": False,
808
+ "auth_source": "none",
809
+ "applicant_import_enabled": False,
810
+ "data_manage_status": False,
811
+ "runtime_checks_required": [],
812
+ "confidence": "preflight",
813
+ }, warnings
814
+
815
+ warnings.append(
816
+ {
817
+ "code": "IMPORT_CAPABILITY_UNAVAILABLE",
818
+ "message": "app_get could not fully determine import capability because baseInfo did not include a complete import permission summary.",
819
+ }
820
+ )
821
+ return _unknown_import_capability(
822
+ applicant_import_enabled=applicant_import_enabled,
823
+ data_manage_status=data_manage_status,
824
+ ), warnings
825
+
826
+
827
+ def _unknown_import_capability(
828
+ *,
829
+ applicant_import_enabled: bool | None = None,
830
+ data_manage_status: bool | None = None,
831
+ ) -> JSONObject:
832
+ return {
833
+ "can_import": None,
834
+ "auth_source": "unknown",
835
+ "applicant_import_enabled": applicant_import_enabled,
836
+ "data_manage_status": data_manage_status,
837
+ "runtime_checks_required": [],
838
+ "confidence": "unknown",
839
+ }
840
+
841
+
842
+ def _derive_editability(base_info: Any) -> tuple[JSONObject, list[JSONObject]]:
843
+ warnings: list[JSONObject] = []
844
+ if not isinstance(base_info, dict):
845
+ warnings.append(
846
+ {
847
+ "code": "APP_EDITABILITY_UNAVAILABLE",
848
+ "message": "app_get could not determine editability because baseInfo was unavailable.",
849
+ }
850
+ )
851
+ return {
852
+ "can_edit_form": None,
853
+ "can_edit_flow": None,
854
+ "can_edit_views": None,
855
+ "can_edit_charts": None,
856
+ }, warnings
857
+
858
+ edit_item_status = _coerce_optional_bool(base_info.get("editItemStatus"))
859
+ data_manage_status = _coerce_optional_bool(base_info.get("dataManageStatus"))
860
+ return {
861
+ "can_edit_form": edit_item_status,
862
+ "can_edit_flow": edit_item_status,
863
+ "can_edit_views": data_manage_status,
864
+ "can_edit_charts": data_manage_status,
865
+ }, warnings
866
+
867
+
868
+ def _coerce_positive_int(value: Any) -> int | None:
869
+ try:
870
+ number = int(value)
871
+ except (TypeError, ValueError):
872
+ return None
873
+ return number if number > 0 else None
874
+
875
+
876
+ def _coerce_optional_bool(value: Any) -> bool | None:
877
+ if isinstance(value, bool):
878
+ return value
879
+ return None