@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.
- package/README.md +5 -3
- package/docs/local-agent-install.md +21 -5
- package/npm/bin/qingflow-app-builder-mcp.mjs +1 -1
- package/npm/lib/runtime.mjs +168 -12
- package/package.json +1 -1
- package/pyproject.toml +4 -1
- package/skills/qingflow-app-builder/SKILL.md +155 -22
- package/skills/qingflow-app-builder/references/create-app.md +51 -21
- package/skills/qingflow-app-builder/references/environments.md +1 -1
- package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
- package/skills/qingflow-app-builder/references/gotchas.md +28 -1
- package/skills/qingflow-app-builder/references/solution-playbooks.md +14 -12
- package/skills/qingflow-app-builder/references/tool-selection.md +47 -19
- package/skills/qingflow-app-builder/references/update-flow.md +112 -25
- package/skills/qingflow-app-builder/references/update-layout.md +11 -24
- package/skills/qingflow-app-builder/references/update-schema.md +1 -23
- package/skills/qingflow-app-builder/references/update-views.md +87 -21
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +137 -0
- package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +4 -0
- package/skills/qingflow-app-builder-code-integrations/references/code-block.md +66 -0
- package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +77 -0
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/backend_client.py +210 -0
- package/src/qingflow_mcp/builder_facade/models.py +1252 -3
- package/src/qingflow_mcp/builder_facade/service.py +11367 -2389
- package/src/qingflow_mcp/cli/__init__.py +1 -0
- package/src/qingflow_mcp/cli/commands/__init__.py +15 -0
- package/src/qingflow_mcp/cli/commands/app.py +40 -0
- package/src/qingflow_mcp/cli/commands/auth.py +78 -0
- package/src/qingflow_mcp/cli/commands/builder.py +515 -0
- package/src/qingflow_mcp/cli/commands/common.py +62 -0
- package/src/qingflow_mcp/cli/commands/imports.py +96 -0
- package/src/qingflow_mcp/cli/commands/record.py +304 -0
- package/src/qingflow_mcp/cli/commands/task.py +89 -0
- package/src/qingflow_mcp/cli/commands/workspace.py +33 -0
- package/src/qingflow_mcp/cli/context.py +48 -0
- package/src/qingflow_mcp/cli/formatters.py +355 -0
- package/src/qingflow_mcp/cli/json_io.py +50 -0
- package/src/qingflow_mcp/cli/main.py +149 -0
- package/src/qingflow_mcp/config.py +39 -0
- package/src/qingflow_mcp/import_store.py +121 -0
- package/src/qingflow_mcp/list_type_labels.py +24 -0
- package/src/qingflow_mcp/response_trim.py +668 -0
- package/src/qingflow_mcp/server.py +160 -18
- package/src/qingflow_mcp/server_app_builder.py +275 -68
- package/src/qingflow_mcp/server_app_user.py +219 -191
- package/src/qingflow_mcp/session_store.py +41 -1
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +43 -4
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +119 -45
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +41 -2
- package/src/qingflow_mcp/solution/executor.py +107 -11
- package/src/qingflow_mcp/solution/spec_models.py +2 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +2032 -127
- package/src/qingflow_mcp/tools/app_tools.py +419 -12
- package/src/qingflow_mcp/tools/approval_tools.py +571 -72
- package/src/qingflow_mcp/tools/auth_tools.py +398 -2
- package/src/qingflow_mcp/tools/code_block_tools.py +756 -0
- package/src/qingflow_mcp/tools/custom_button_tools.py +179 -0
- package/src/qingflow_mcp/tools/directory_tools.py +203 -31
- package/src/qingflow_mcp/tools/feedback_tools.py +230 -0
- package/src/qingflow_mcp/tools/file_tools.py +1 -0
- package/src/qingflow_mcp/tools/import_tools.py +2150 -0
- package/src/qingflow_mcp/tools/package_tools.py +18 -4
- package/src/qingflow_mcp/tools/portal_tools.py +31 -0
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +109 -7
- package/src/qingflow_mcp/tools/record_tools.py +9894 -1104
- package/src/qingflow_mcp/tools/solution_tools.py +115 -3
- package/src/qingflow_mcp/tools/task_context_tools.py +2040 -0
- package/src/qingflow_mcp/tools/task_tools.py +376 -225
- 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
|
-
"""
|
|
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
|
-
|
|
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":
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|