@josephyan/qingflow-cli 0.2.0-beta.55
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 +30 -0
- package/docs/local-agent-install.md +235 -0
- package/entry_point.py +13 -0
- package/npm/bin/qingflow.mjs +5 -0
- package/npm/lib/runtime.mjs +204 -0
- package/npm/scripts/postinstall.mjs +16 -0
- package/package.json +34 -0
- package/pyproject.toml +67 -0
- package/qingflow +15 -0
- package/src/qingflow_mcp/__init__.py +5 -0
- package/src/qingflow_mcp/__main__.py +5 -0
- package/src/qingflow_mcp/backend_client.py +547 -0
- package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
- package/src/qingflow_mcp/builder_facade/models.py +985 -0
- package/src/qingflow_mcp/builder_facade/service.py +8243 -0
- 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 +184 -0
- package/src/qingflow_mcp/cli/commands/common.py +47 -0
- package/src/qingflow_mcp/cli/commands/imports.py +86 -0
- package/src/qingflow_mcp/cli/commands/record.py +202 -0
- package/src/qingflow_mcp/cli/commands/task.py +87 -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 +269 -0
- package/src/qingflow_mcp/cli/json_io.py +50 -0
- package/src/qingflow_mcp/cli/main.py +147 -0
- package/src/qingflow_mcp/config.py +221 -0
- package/src/qingflow_mcp/errors.py +66 -0
- package/src/qingflow_mcp/import_store.py +121 -0
- package/src/qingflow_mcp/json_types.py +18 -0
- package/src/qingflow_mcp/list_type_labels.py +76 -0
- package/src/qingflow_mcp/server.py +211 -0
- package/src/qingflow_mcp/server_app_builder.py +387 -0
- package/src/qingflow_mcp/server_app_user.py +317 -0
- package/src/qingflow_mcp/session_store.py +289 -0
- package/src/qingflow_mcp/solution/__init__.py +6 -0
- package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
- package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
- package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +466 -0
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +113 -0
- package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
- package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
- package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
- package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
- package/src/qingflow_mcp/solution/design_session.py +222 -0
- package/src/qingflow_mcp/solution/design_store.py +100 -0
- package/src/qingflow_mcp/solution/executor.py +2339 -0
- package/src/qingflow_mcp/solution/normalizer.py +23 -0
- package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
- package/src/qingflow_mcp/solution/run_store.py +244 -0
- package/src/qingflow_mcp/solution/spec_models.py +853 -0
- package/src/qingflow_mcp/tools/__init__.py +1 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +2063 -0
- package/src/qingflow_mcp/tools/app_tools.py +850 -0
- package/src/qingflow_mcp/tools/approval_tools.py +833 -0
- package/src/qingflow_mcp/tools/auth_tools.py +697 -0
- package/src/qingflow_mcp/tools/base.py +81 -0
- package/src/qingflow_mcp/tools/code_block_tools.py +679 -0
- package/src/qingflow_mcp/tools/directory_tools.py +648 -0
- package/src/qingflow_mcp/tools/feedback_tools.py +230 -0
- package/src/qingflow_mcp/tools/file_tools.py +385 -0
- package/src/qingflow_mcp/tools/import_tools.py +1971 -0
- package/src/qingflow_mcp/tools/navigation_tools.py +177 -0
- package/src/qingflow_mcp/tools/package_tools.py +240 -0
- package/src/qingflow_mcp/tools/portal_tools.py +131 -0
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +269 -0
- package/src/qingflow_mcp/tools/record_tools.py +12739 -0
- package/src/qingflow_mcp/tools/role_tools.py +94 -0
- package/src/qingflow_mcp/tools/solution_tools.py +3887 -0
- package/src/qingflow_mcp/tools/task_context_tools.py +1423 -0
- package/src/qingflow_mcp/tools/task_tools.py +843 -0
- package/src/qingflow_mcp/tools/view_tools.py +280 -0
- package/src/qingflow_mcp/tools/workflow_tools.py +312 -0
- package/src/qingflow_mcp/tools/workspace_tools.py +219 -0
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import Counter
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from mcp.server.fastmcp import FastMCP
|
|
7
|
+
|
|
8
|
+
from ..backend_client import BackendRequestContext
|
|
9
|
+
from ..config import DEFAULT_PROFILE
|
|
10
|
+
from ..errors import QingflowApiError, raise_tool_error
|
|
11
|
+
from ..json_types import JSONObject
|
|
12
|
+
from ..list_type_labels import SYSTEM_VIEW_DEFINITIONS, get_app_publish_status_label
|
|
13
|
+
from .base import ToolBase
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AppTools(ToolBase):
|
|
17
|
+
def register(self, mcp: FastMCP) -> None:
|
|
18
|
+
@mcp.tool()
|
|
19
|
+
def app_list(profile: str = DEFAULT_PROFILE, ship_auth: bool = False) -> JSONObject:
|
|
20
|
+
return self.app_list(profile=profile, ship_auth=ship_auth)
|
|
21
|
+
|
|
22
|
+
@mcp.tool()
|
|
23
|
+
def app_search(profile: str = DEFAULT_PROFILE, keyword: str = "", page_num: int = 1, page_size: int = 50) -> JSONObject:
|
|
24
|
+
return self.app_search(profile=profile, keyword=keyword, page_num=page_num, page_size=page_size)
|
|
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
|
+
|
|
30
|
+
@mcp.tool()
|
|
31
|
+
def app_get_base(profile: str = DEFAULT_PROFILE, app_key: str = "", include_raw: bool = False) -> JSONObject:
|
|
32
|
+
return self.app_get_base(profile=profile, app_key=app_key, include_raw=include_raw)
|
|
33
|
+
|
|
34
|
+
@mcp.tool(description=self._high_risk_tool_description(operation="update", target="app base settings"))
|
|
35
|
+
def app_update_base(profile: str = DEFAULT_PROFILE, app_key: str = "", payload: JSONObject | None = None) -> JSONObject:
|
|
36
|
+
return self.app_update_base(profile=profile, app_key=app_key, payload=payload or {})
|
|
37
|
+
|
|
38
|
+
@mcp.tool()
|
|
39
|
+
def app_get_form_schema(
|
|
40
|
+
profile: str = DEFAULT_PROFILE,
|
|
41
|
+
app_key: str = "",
|
|
42
|
+
form_type: int | str = 1,
|
|
43
|
+
being_draft: bool | None = None,
|
|
44
|
+
being_apply: bool | None = None,
|
|
45
|
+
audit_node_id: int | None = None,
|
|
46
|
+
include_raw: bool = False,
|
|
47
|
+
) -> JSONObject:
|
|
48
|
+
return self.app_get_form_schema(
|
|
49
|
+
profile=profile,
|
|
50
|
+
app_key=app_key,
|
|
51
|
+
form_type=form_type,
|
|
52
|
+
being_draft=being_draft,
|
|
53
|
+
being_apply=being_apply,
|
|
54
|
+
audit_node_id=audit_node_id,
|
|
55
|
+
include_raw=include_raw,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
@mcp.tool()
|
|
59
|
+
def app_get_edit_version_no(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
|
|
60
|
+
return self.app_get_edit_version_no(profile=profile, app_key=app_key)
|
|
61
|
+
|
|
62
|
+
@mcp.tool()
|
|
63
|
+
def app_edit_finished(profile: str = DEFAULT_PROFILE, app_key: str = "", payload: JSONObject | None = None) -> JSONObject:
|
|
64
|
+
return self.app_edit_finished(profile=profile, app_key=app_key, payload=payload or {})
|
|
65
|
+
|
|
66
|
+
@mcp.tool()
|
|
67
|
+
def app_create(profile: str = DEFAULT_PROFILE, payload: JSONObject | None = None) -> JSONObject:
|
|
68
|
+
return self.app_create(profile=profile, payload=payload or {})
|
|
69
|
+
|
|
70
|
+
@mcp.tool(description=self._high_risk_tool_description(operation="delete", target="app configuration"))
|
|
71
|
+
def app_delete(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
|
|
72
|
+
return self.app_delete(profile=profile, app_key=app_key)
|
|
73
|
+
|
|
74
|
+
@mcp.tool()
|
|
75
|
+
def app_publish(
|
|
76
|
+
profile: str = DEFAULT_PROFILE,
|
|
77
|
+
app_key: str = "",
|
|
78
|
+
payload: JSONObject | None = None,
|
|
79
|
+
) -> JSONObject:
|
|
80
|
+
return self.app_publish(profile=profile, app_key=app_key, payload=payload or {})
|
|
81
|
+
|
|
82
|
+
def app_list(self, *, profile: str, ship_auth: bool = False) -> JSONObject:
|
|
83
|
+
"""List current-user visible apps in the selected workspace."""
|
|
84
|
+
def runner(session_profile, context):
|
|
85
|
+
result = self.backend.request("GET", context, "/tag/apps")
|
|
86
|
+
items, source_shape = self._extract_visible_apps(result)
|
|
87
|
+
response = {
|
|
88
|
+
"profile": profile,
|
|
89
|
+
"ws_id": session_profile.selected_ws_id,
|
|
90
|
+
"items": items,
|
|
91
|
+
"count": len(items),
|
|
92
|
+
"source_shape": source_shape,
|
|
93
|
+
}
|
|
94
|
+
if ship_auth:
|
|
95
|
+
response["raw"] = result
|
|
96
|
+
return response
|
|
97
|
+
|
|
98
|
+
return self._run(profile, runner)
|
|
99
|
+
|
|
100
|
+
def app_search(self, *, profile: str, keyword: str = "", page_num: int = 1, page_size: int = 50) -> JSONObject:
|
|
101
|
+
"""Search apps by keyword in name/title using backend search API.
|
|
102
|
+
Useful for finding BUG-related apps across all packages."""
|
|
103
|
+
def runner(session_profile, context):
|
|
104
|
+
# Use GET /app/item which supports queryKey search across all apps
|
|
105
|
+
params: JSONObject = {"pageNum": page_num, "pageSize": page_size}
|
|
106
|
+
if keyword:
|
|
107
|
+
params["queryKey"] = keyword
|
|
108
|
+
|
|
109
|
+
result = self.backend.request("GET", context, "/app/item", params=params)
|
|
110
|
+
|
|
111
|
+
apps = []
|
|
112
|
+
if isinstance(result, dict):
|
|
113
|
+
items = result.get("list", [])
|
|
114
|
+
for item in items:
|
|
115
|
+
if isinstance(item, dict):
|
|
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)
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
"profile": profile,
|
|
128
|
+
"ws_id": session_profile.selected_ws_id,
|
|
129
|
+
"keyword": keyword,
|
|
130
|
+
"page_num": page_num,
|
|
131
|
+
"page_size": page_size,
|
|
132
|
+
"total": result.get("total") if isinstance(result, dict) else len(apps),
|
|
133
|
+
"items": apps,
|
|
134
|
+
"apps": apps,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return self._run(profile, runner)
|
|
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
|
+
|
|
183
|
+
return {
|
|
184
|
+
"profile": profile,
|
|
185
|
+
"ws_id": session_profile.selected_ws_id,
|
|
186
|
+
"ok": True,
|
|
187
|
+
"request_route": {"base_url": context.base_url, "qf_version": context.qf_version},
|
|
188
|
+
"warnings": warnings,
|
|
189
|
+
"data": {
|
|
190
|
+
"app_key": app_key,
|
|
191
|
+
"app_name": app_name,
|
|
192
|
+
"can_create": can_create,
|
|
193
|
+
"import_capability": import_capability,
|
|
194
|
+
"accessible_views": accessible_views,
|
|
195
|
+
},
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return self._run(profile, runner)
|
|
199
|
+
|
|
200
|
+
def app_get_base(self, *, profile: str, app_key: str, include_raw: bool = False) -> JSONObject:
|
|
201
|
+
self._require_app_key(app_key)
|
|
202
|
+
|
|
203
|
+
def runner(session_profile, context):
|
|
204
|
+
result = self.backend.request("GET", context, f"/app/{app_key}/baseInfo")
|
|
205
|
+
publish_status = result.get("appPublishStatus") if isinstance(result, dict) else None
|
|
206
|
+
compact = self._compact_base_info(result if isinstance(result, dict) else {})
|
|
207
|
+
response = {
|
|
208
|
+
"profile": profile,
|
|
209
|
+
"ws_id": session_profile.selected_ws_id,
|
|
210
|
+
"app_key": app_key,
|
|
211
|
+
"result": result if include_raw else compact,
|
|
212
|
+
"app_publish_status": publish_status,
|
|
213
|
+
"app_publish_status_label": get_app_publish_status_label(publish_status if isinstance(publish_status, int) else None),
|
|
214
|
+
"compact": not include_raw,
|
|
215
|
+
}
|
|
216
|
+
if include_raw:
|
|
217
|
+
response["summary"] = compact
|
|
218
|
+
return response
|
|
219
|
+
|
|
220
|
+
return self._run(profile, runner)
|
|
221
|
+
|
|
222
|
+
def app_update_base(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
|
|
223
|
+
self._require_app_key(app_key)
|
|
224
|
+
body = self._require_dict(payload)
|
|
225
|
+
|
|
226
|
+
def runner(session_profile, context):
|
|
227
|
+
result = self.backend.request("POST", context, f"/app/{app_key}/baseInfo", json_body=body)
|
|
228
|
+
return self._attach_human_review_notice(
|
|
229
|
+
{"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "result": result},
|
|
230
|
+
operation="update",
|
|
231
|
+
target="app base settings",
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
return self._run(profile, runner)
|
|
235
|
+
|
|
236
|
+
def app_get_form_schema(
|
|
237
|
+
self,
|
|
238
|
+
*,
|
|
239
|
+
profile: str,
|
|
240
|
+
app_key: str,
|
|
241
|
+
form_type: int | str,
|
|
242
|
+
being_draft: bool | None,
|
|
243
|
+
being_apply: bool | None,
|
|
244
|
+
audit_node_id: int | None,
|
|
245
|
+
include_raw: bool = False,
|
|
246
|
+
) -> JSONObject:
|
|
247
|
+
self._require_app_key(app_key)
|
|
248
|
+
resolved_form_type = _normalize_form_type(form_type)
|
|
249
|
+
|
|
250
|
+
def runner(session_profile, context):
|
|
251
|
+
params: JSONObject = {"type": resolved_form_type}
|
|
252
|
+
if being_draft is not None:
|
|
253
|
+
params["beingDraft"] = being_draft
|
|
254
|
+
if being_apply is not None:
|
|
255
|
+
params["beingApply"] = being_apply
|
|
256
|
+
if audit_node_id is not None:
|
|
257
|
+
params["auditNodeId"] = audit_node_id
|
|
258
|
+
result = self.backend.request("GET", context, f"/app/{app_key}/form", params=params)
|
|
259
|
+
compact = self._compact_form_schema(result if isinstance(result, dict) else {})
|
|
260
|
+
response = {
|
|
261
|
+
"profile": profile,
|
|
262
|
+
"ws_id": session_profile.selected_ws_id,
|
|
263
|
+
"app_key": app_key,
|
|
264
|
+
"result": result if include_raw else compact,
|
|
265
|
+
"compact": not include_raw,
|
|
266
|
+
"requested_form_type": form_type,
|
|
267
|
+
"resolved_form_type": resolved_form_type,
|
|
268
|
+
}
|
|
269
|
+
if include_raw:
|
|
270
|
+
response["summary"] = compact
|
|
271
|
+
return response
|
|
272
|
+
|
|
273
|
+
return self._run(profile, runner)
|
|
274
|
+
|
|
275
|
+
def app_get_edit_version_no(self, *, profile: str, app_key: str) -> JSONObject:
|
|
276
|
+
self._require_app_key(app_key)
|
|
277
|
+
|
|
278
|
+
def runner(session_profile, context):
|
|
279
|
+
result = self.backend.request("GET", context, f"/app/{app_key}/editVersionNo")
|
|
280
|
+
return {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "result": result}
|
|
281
|
+
|
|
282
|
+
return self._run(profile, runner)
|
|
283
|
+
|
|
284
|
+
def app_get_apply_base_info(self, *, profile: str, app_key: str, list_type: int) -> JSONObject:
|
|
285
|
+
self._require_app_key(app_key)
|
|
286
|
+
if not isinstance(list_type, int) or isinstance(list_type, bool) or list_type <= 0:
|
|
287
|
+
raise_tool_error(QingflowApiError.config_error("list_type must be a positive integer"))
|
|
288
|
+
|
|
289
|
+
def runner(session_profile, context):
|
|
290
|
+
result = self.backend.request("GET", context, f"/app/{app_key}/apply/baseInfo", params={"type": list_type})
|
|
291
|
+
return {
|
|
292
|
+
"profile": profile,
|
|
293
|
+
"ws_id": session_profile.selected_ws_id,
|
|
294
|
+
"app_key": app_key,
|
|
295
|
+
"list_type": list_type,
|
|
296
|
+
"result": result,
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return self._run(profile, runner)
|
|
300
|
+
|
|
301
|
+
def app_update_apply_config(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
|
|
302
|
+
self._require_app_key(app_key)
|
|
303
|
+
body = self._require_dict(payload)
|
|
304
|
+
|
|
305
|
+
def runner(session_profile, context):
|
|
306
|
+
result = self.backend.request("POST", context, f"/app/{app_key}/apply/config", json_body=body)
|
|
307
|
+
return self._attach_human_review_notice(
|
|
308
|
+
{
|
|
309
|
+
"profile": profile,
|
|
310
|
+
"ws_id": session_profile.selected_ws_id,
|
|
311
|
+
"app_key": app_key,
|
|
312
|
+
"result": result,
|
|
313
|
+
},
|
|
314
|
+
operation="update",
|
|
315
|
+
target="app apply list configuration",
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
return self._run(profile, runner)
|
|
319
|
+
|
|
320
|
+
def app_update_form_schema(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
|
|
321
|
+
self._require_app_key(app_key)
|
|
322
|
+
body = self._require_dict(payload)
|
|
323
|
+
|
|
324
|
+
def runner(session_profile, context):
|
|
325
|
+
result = self.backend.request("POST", context, f"/app/{app_key}/form", json_body=body)
|
|
326
|
+
return self._attach_human_review_notice(
|
|
327
|
+
{"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "result": result},
|
|
328
|
+
operation="update",
|
|
329
|
+
target="app form schema",
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
return self._run(profile, runner)
|
|
333
|
+
|
|
334
|
+
def app_edit_finished(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
|
|
335
|
+
self._require_app_key(app_key)
|
|
336
|
+
body = self._require_dict(payload)
|
|
337
|
+
|
|
338
|
+
def runner(session_profile, context):
|
|
339
|
+
result = self.backend.request("POST", context, f"/app/{app_key}/editFinished", json_body=body)
|
|
340
|
+
return {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "result": result}
|
|
341
|
+
|
|
342
|
+
return self._run(profile, runner)
|
|
343
|
+
|
|
344
|
+
def app_create(self, *, profile: str, payload: JSONObject) -> JSONObject:
|
|
345
|
+
body = self._require_dict(payload)
|
|
346
|
+
|
|
347
|
+
def runner(session_profile, context):
|
|
348
|
+
attempted_contexts = [context]
|
|
349
|
+
if context.qf_version is not None:
|
|
350
|
+
attempted_contexts.append(
|
|
351
|
+
BackendRequestContext(
|
|
352
|
+
base_url=context.base_url,
|
|
353
|
+
token=context.token,
|
|
354
|
+
ws_id=context.ws_id,
|
|
355
|
+
qf_version=None,
|
|
356
|
+
qf_version_source="app_create_retry_without_qf_version",
|
|
357
|
+
)
|
|
358
|
+
)
|
|
359
|
+
last_error: QingflowApiError | None = None
|
|
360
|
+
request_with_meta = getattr(self.backend, "request_with_meta", None)
|
|
361
|
+
for call_context in attempted_contexts:
|
|
362
|
+
try:
|
|
363
|
+
if callable(request_with_meta):
|
|
364
|
+
response = request_with_meta("POST", call_context, "/app", json_body=body)
|
|
365
|
+
result = response.data
|
|
366
|
+
request_id = response.request_id
|
|
367
|
+
else:
|
|
368
|
+
result = self.backend.request("POST", call_context, "/app", json_body=body)
|
|
369
|
+
request_id = None
|
|
370
|
+
return {
|
|
371
|
+
"profile": profile,
|
|
372
|
+
"ws_id": session_profile.selected_ws_id,
|
|
373
|
+
"result": result,
|
|
374
|
+
"request_route": self.backend.describe_route(call_context),
|
|
375
|
+
"request_id": request_id,
|
|
376
|
+
}
|
|
377
|
+
except QingflowApiError as error:
|
|
378
|
+
last_error = error
|
|
379
|
+
is_retryable_404 = (
|
|
380
|
+
error.http_status == 404
|
|
381
|
+
and call_context.qf_version is not None
|
|
382
|
+
)
|
|
383
|
+
if not is_retryable_404:
|
|
384
|
+
raise
|
|
385
|
+
assert last_error is not None
|
|
386
|
+
raise last_error
|
|
387
|
+
|
|
388
|
+
return self._run(profile, runner)
|
|
389
|
+
|
|
390
|
+
def app_delete(self, *, profile: str, app_key: str) -> JSONObject:
|
|
391
|
+
self._require_app_key(app_key)
|
|
392
|
+
|
|
393
|
+
def runner(session_profile, context):
|
|
394
|
+
result = self.backend.request("DELETE", context, f"/app/{app_key}")
|
|
395
|
+
return self._attach_human_review_notice(
|
|
396
|
+
{"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "result": result},
|
|
397
|
+
operation="delete",
|
|
398
|
+
target="app configuration",
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
return self._run(profile, runner)
|
|
402
|
+
|
|
403
|
+
def app_publish(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
|
|
404
|
+
self._require_app_key(app_key)
|
|
405
|
+
body = self._require_dict(payload)
|
|
406
|
+
|
|
407
|
+
def runner(session_profile, context):
|
|
408
|
+
result = self.backend.request("POST", context, f"/app/{app_key}/publish", json_body=body)
|
|
409
|
+
return {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "result": result}
|
|
410
|
+
|
|
411
|
+
return self._run(profile, runner)
|
|
412
|
+
|
|
413
|
+
def _require_app_key(self, app_key: str) -> None:
|
|
414
|
+
if not app_key:
|
|
415
|
+
raise_tool_error(QingflowApiError.config_error("app_key is required"))
|
|
416
|
+
|
|
417
|
+
def _probe_create_access(self, context: BackendRequestContext, app_key: str) -> bool:
|
|
418
|
+
try:
|
|
419
|
+
self.backend.request(
|
|
420
|
+
"GET",
|
|
421
|
+
context,
|
|
422
|
+
f"/app/{app_key}/form",
|
|
423
|
+
params={"type": 2, "beingApply": True},
|
|
424
|
+
)
|
|
425
|
+
return True
|
|
426
|
+
except QingflowApiError as exc:
|
|
427
|
+
if exc.backend_code in {40002, 40027}:
|
|
428
|
+
return False
|
|
429
|
+
raise
|
|
430
|
+
|
|
431
|
+
def _probe_list_type_access(self, context: BackendRequestContext, app_key: str, list_type: int) -> bool:
|
|
432
|
+
try:
|
|
433
|
+
self.backend.request(
|
|
434
|
+
"POST",
|
|
435
|
+
context,
|
|
436
|
+
f"/app/{app_key}/apply/filter",
|
|
437
|
+
json_body={"type": list_type, "pageNum": 1, "pageSize": 1},
|
|
438
|
+
)
|
|
439
|
+
return True
|
|
440
|
+
except QingflowApiError as exc:
|
|
441
|
+
if exc.backend_code in {40002, 40027}:
|
|
442
|
+
return False
|
|
443
|
+
raise
|
|
444
|
+
|
|
445
|
+
def _resolve_accessible_system_views(self, context: BackendRequestContext, app_key: str) -> tuple[list[JSONObject], list[JSONObject]]:
|
|
446
|
+
items: list[JSONObject] = []
|
|
447
|
+
warnings: list[JSONObject] = []
|
|
448
|
+
for view_id, list_type, name in SYSTEM_VIEW_DEFINITIONS:
|
|
449
|
+
try:
|
|
450
|
+
can_access = self._probe_list_type_access(context, app_key, list_type)
|
|
451
|
+
except QingflowApiError as exc:
|
|
452
|
+
warnings.append(
|
|
453
|
+
{
|
|
454
|
+
"code": "SYSTEM_VIEW_PROBE_UNVERIFIED",
|
|
455
|
+
"message": (
|
|
456
|
+
f"app_get skipped system view '{name}' because access probing failed: "
|
|
457
|
+
f"{exc.message or 'probe failed'}"
|
|
458
|
+
),
|
|
459
|
+
"view_id": view_id,
|
|
460
|
+
"list_type": list_type,
|
|
461
|
+
}
|
|
462
|
+
)
|
|
463
|
+
continue
|
|
464
|
+
if not can_access:
|
|
465
|
+
continue
|
|
466
|
+
items.append({"view_id": view_id, "name": name, "kind": "system", "analysis_supported": True})
|
|
467
|
+
return items, warnings
|
|
468
|
+
|
|
469
|
+
def _resolve_accessible_custom_views(self, context: BackendRequestContext, app_key: str) -> list[JSONObject]:
|
|
470
|
+
try:
|
|
471
|
+
payload = self.backend.request("GET", context, f"/app/{app_key}/view/viewList")
|
|
472
|
+
except QingflowApiError as exc:
|
|
473
|
+
if exc.backend_code in {40002, 40027}:
|
|
474
|
+
return []
|
|
475
|
+
raise
|
|
476
|
+
|
|
477
|
+
items: list[JSONObject] = []
|
|
478
|
+
for item in _normalize_view_list(payload):
|
|
479
|
+
view_key = str(item.get("viewKey") or "").strip()
|
|
480
|
+
if not view_key:
|
|
481
|
+
continue
|
|
482
|
+
normalized: JSONObject = {
|
|
483
|
+
"view_id": f"custom:{view_key}",
|
|
484
|
+
"name": str(item.get("viewName") or view_key).strip() or view_key,
|
|
485
|
+
"kind": "custom",
|
|
486
|
+
}
|
|
487
|
+
view_type = str(item.get("viewType") or item.get("viewgraphType") or "").strip()
|
|
488
|
+
if view_type:
|
|
489
|
+
normalized["view_type"] = view_type
|
|
490
|
+
normalized["analysis_supported"] = _analysis_supported_for_view_type(view_type or None)
|
|
491
|
+
items.append(normalized)
|
|
492
|
+
return items
|
|
493
|
+
|
|
494
|
+
def _compact_base_info(self, result: dict[str, Any]) -> JSONObject:
|
|
495
|
+
publish_status = result.get("appPublishStatus")
|
|
496
|
+
auth = result.get("auth") if isinstance(result.get("auth"), dict) else {}
|
|
497
|
+
contact_auth = auth.get("contactAuth") if isinstance(auth, dict) and isinstance(auth.get("contactAuth"), dict) else {}
|
|
498
|
+
external_auth = auth.get("externalMemberAuth") if isinstance(auth, dict) and isinstance(auth.get("externalMemberAuth"), dict) else {}
|
|
499
|
+
tag_items = result.get("tagItems") if isinstance(result.get("tagItems"), list) else []
|
|
500
|
+
tag_ids = result.get("tagIds") if isinstance(result.get("tagIds"), list) else []
|
|
501
|
+
return {
|
|
502
|
+
"appKey": result.get("appKey"),
|
|
503
|
+
"formId": result.get("formId"),
|
|
504
|
+
"formTitle": result.get("formTitle"),
|
|
505
|
+
"appIcon": result.get("appIcon"),
|
|
506
|
+
"appPublishStatus": publish_status,
|
|
507
|
+
"appPublishStatusLabel": get_app_publish_status_label(publish_status if isinstance(publish_status, int) else None),
|
|
508
|
+
"appOpenStatus": result.get("appOpenStatus"),
|
|
509
|
+
"applyOpenStatus": result.get("applyOpenStatus"),
|
|
510
|
+
"dataManageStatus": result.get("dataManageStatus"),
|
|
511
|
+
"editItemStatus": result.get("editItemStatus"),
|
|
512
|
+
"deleteItemStatus": result.get("deleteItemStatus"),
|
|
513
|
+
"copyAppStatus": result.get("copyAppStatus"),
|
|
514
|
+
"flowStatus": result.get("flowStatus"),
|
|
515
|
+
"createTime": result.get("createTime"),
|
|
516
|
+
"creator": self._compact_user(result.get("creator")),
|
|
517
|
+
"tagIds": tag_ids,
|
|
518
|
+
"tagCount": len(tag_ids),
|
|
519
|
+
"tagItemCount": len(tag_items),
|
|
520
|
+
"tagItemsPreview": [self._compact_tag_item(item) for item in tag_items[:10] if isinstance(item, dict)],
|
|
521
|
+
"authSummary": {
|
|
522
|
+
"type": auth.get("type") if isinstance(auth, dict) else None,
|
|
523
|
+
"contactAuthType": contact_auth.get("type") if isinstance(contact_auth, dict) else None,
|
|
524
|
+
"contactDepartments": self._count_auth_members(contact_auth, "depart"),
|
|
525
|
+
"contactMembers": self._count_auth_members(contact_auth, "member"),
|
|
526
|
+
"contactRoles": self._count_auth_members(contact_auth, "role"),
|
|
527
|
+
"contactIncludeSubDeparts": self._extract_include_sub_departs(contact_auth),
|
|
528
|
+
"externalAuthType": external_auth.get("type") if isinstance(external_auth, dict) else None,
|
|
529
|
+
"externalDepartments": self._count_auth_members(external_auth, "depart"),
|
|
530
|
+
"externalMembers": self._count_auth_members(external_auth, "member"),
|
|
531
|
+
"externalRoles": self._count_auth_members(external_auth, "role"),
|
|
532
|
+
},
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
def _compact_form_schema(self, result: dict[str, Any]) -> JSONObject:
|
|
536
|
+
base_questions_raw = result.get("baseQues") if isinstance(result.get("baseQues"), list) else []
|
|
537
|
+
form_questions_raw = result.get("formQues") if isinstance(result.get("formQues"), list) else []
|
|
538
|
+
base_questions = [self._compact_question(question) for question in base_questions_raw if isinstance(question, dict)]
|
|
539
|
+
form_questions = [self._compact_question(question) for question in form_questions_raw if isinstance(question, dict)]
|
|
540
|
+
all_questions = base_questions + form_questions
|
|
541
|
+
type_counts = Counter(
|
|
542
|
+
question["queType"]
|
|
543
|
+
for question in all_questions
|
|
544
|
+
if isinstance(question.get("queType"), int)
|
|
545
|
+
)
|
|
546
|
+
required_count = sum(1 for question in all_questions if question.get("required") is True)
|
|
547
|
+
return {
|
|
548
|
+
"appKey": result.get("appKey"),
|
|
549
|
+
"formId": result.get("formId"),
|
|
550
|
+
"formTitle": result.get("formTitle"),
|
|
551
|
+
"editVersionNo": result.get("editVersionNo"),
|
|
552
|
+
"serialNumType": result.get("serialNumType"),
|
|
553
|
+
"hideCopyright": result.get("hideCopyright"),
|
|
554
|
+
"questionCounts": {
|
|
555
|
+
"baseQuestions": len(base_questions),
|
|
556
|
+
"formQuestions": len(form_questions),
|
|
557
|
+
"totalQuestions": len(all_questions),
|
|
558
|
+
"requiredQuestions": required_count,
|
|
559
|
+
"questionRelations": len(result.get("questionRelations") or []),
|
|
560
|
+
"selectScopeRelations": len(result.get("selectScopeRelations") or []),
|
|
561
|
+
"serialNumConfigs": len(result.get("serialNumConfig") or []),
|
|
562
|
+
},
|
|
563
|
+
"questionTypeCounts": [
|
|
564
|
+
{"queType": que_type, "count": count}
|
|
565
|
+
for que_type, count in sorted(type_counts.items())
|
|
566
|
+
],
|
|
567
|
+
"baseQuestions": base_questions,
|
|
568
|
+
"formQuestions": form_questions,
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
def _compact_user(self, user: Any) -> JSONObject | None:
|
|
572
|
+
if not isinstance(user, dict):
|
|
573
|
+
return None
|
|
574
|
+
return {
|
|
575
|
+
"uid": user.get("uid"),
|
|
576
|
+
"nickName": user.get("nickName"),
|
|
577
|
+
"email": user.get("email"),
|
|
578
|
+
"remark": user.get("remark"),
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
def _compact_tag_item(self, item: dict[str, Any]) -> JSONObject:
|
|
582
|
+
return {
|
|
583
|
+
"itemType": item.get("itemType"),
|
|
584
|
+
"title": item.get("title"),
|
|
585
|
+
"appKey": item.get("appKey"),
|
|
586
|
+
"formId": item.get("formId"),
|
|
587
|
+
"dashKey": item.get("dashKey"),
|
|
588
|
+
"pageKey": item.get("pageKey"),
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
def _compact_question(self, question: dict[str, Any]) -> JSONObject:
|
|
592
|
+
options = question.get("options")
|
|
593
|
+
inner_questions = question.get("innerQuestions")
|
|
594
|
+
sub_questions = question.get("subQuestions")
|
|
595
|
+
compact = {
|
|
596
|
+
"queId": question.get("queId"),
|
|
597
|
+
"queTitle": question.get("queTitle"),
|
|
598
|
+
"queType": question.get("queType"),
|
|
599
|
+
"required": question.get("required") is True,
|
|
600
|
+
"ordinal": question.get("ordinal"),
|
|
601
|
+
"inlineOrdinal": question.get("inlineOrdinal"),
|
|
602
|
+
"queWidth": question.get("queWidth"),
|
|
603
|
+
"sectionId": question.get("sectionId"),
|
|
604
|
+
"supId": question.get("supId"),
|
|
605
|
+
"relatedQueId": question.get("relatedQueId"),
|
|
606
|
+
"pluginStatus": question.get("pluginStatus"),
|
|
607
|
+
"optionCount": len(options) if isinstance(options, list) else 0,
|
|
608
|
+
"innerQuestionCount": len(inner_questions) if isinstance(inner_questions, list) else 0,
|
|
609
|
+
"subQuestionCount": len(sub_questions) if isinstance(sub_questions, list) else 0,
|
|
610
|
+
}
|
|
611
|
+
return {key: value for key, value in compact.items() if value is not None}
|
|
612
|
+
|
|
613
|
+
def _extract_visible_apps(self, result: Any) -> tuple[list[JSONObject], str]:
|
|
614
|
+
apps: list[JSONObject] = []
|
|
615
|
+
seen: set[str] = set()
|
|
616
|
+
|
|
617
|
+
def walk(
|
|
618
|
+
node: Any,
|
|
619
|
+
*,
|
|
620
|
+
package_tag_id: int | None = None,
|
|
621
|
+
package_name: str | None = None,
|
|
622
|
+
group_id: int | None = None,
|
|
623
|
+
group_name: str | None = None,
|
|
624
|
+
) -> None:
|
|
625
|
+
if isinstance(node, list):
|
|
626
|
+
for item in node:
|
|
627
|
+
walk(
|
|
628
|
+
item,
|
|
629
|
+
package_tag_id=package_tag_id,
|
|
630
|
+
package_name=package_name,
|
|
631
|
+
group_id=group_id,
|
|
632
|
+
group_name=group_name,
|
|
633
|
+
)
|
|
634
|
+
return
|
|
635
|
+
if not isinstance(node, dict):
|
|
636
|
+
return
|
|
637
|
+
|
|
638
|
+
next_package_tag_id = _coerce_positive_int(node.get("tagId")) or package_tag_id
|
|
639
|
+
next_package_name = str(node.get("tagName") or "").strip() or package_name
|
|
640
|
+
next_group_id = _coerce_positive_int(node.get("groupId")) or group_id
|
|
641
|
+
next_group_name = str(node.get("groupName") or node.get("groupTitle") or "").strip() or group_name
|
|
642
|
+
|
|
643
|
+
normalized = self._normalize_visible_app(
|
|
644
|
+
node,
|
|
645
|
+
package_tag_id=next_package_tag_id,
|
|
646
|
+
package_name=next_package_name,
|
|
647
|
+
group_id=next_group_id,
|
|
648
|
+
group_name=next_group_name,
|
|
649
|
+
)
|
|
650
|
+
if normalized is not None:
|
|
651
|
+
app_key = str(normalized.get("app_key") or "").strip()
|
|
652
|
+
if app_key and app_key not in seen:
|
|
653
|
+
seen.add(app_key)
|
|
654
|
+
apps.append(normalized)
|
|
655
|
+
|
|
656
|
+
for value in node.values():
|
|
657
|
+
if isinstance(value, (list, dict)):
|
|
658
|
+
walk(
|
|
659
|
+
value,
|
|
660
|
+
package_tag_id=next_package_tag_id,
|
|
661
|
+
package_name=next_package_name,
|
|
662
|
+
group_id=next_group_id,
|
|
663
|
+
group_name=next_group_name,
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
walk(result)
|
|
667
|
+
return apps, type(result).__name__
|
|
668
|
+
|
|
669
|
+
def _normalize_visible_app(
|
|
670
|
+
self,
|
|
671
|
+
item: dict[str, Any],
|
|
672
|
+
*,
|
|
673
|
+
package_tag_id: int | None,
|
|
674
|
+
package_name: str | None,
|
|
675
|
+
group_id: int | None,
|
|
676
|
+
group_name: str | None,
|
|
677
|
+
) -> JSONObject | None:
|
|
678
|
+
app_key = str(item.get("appKey") or item.get("app_key") or "").strip()
|
|
679
|
+
if not app_key:
|
|
680
|
+
return None
|
|
681
|
+
title = str(item.get("title") or item.get("formTitle") or item.get("appName") or item.get("name") or app_key).strip() or app_key
|
|
682
|
+
tag_ids = item.get("tagIds") if isinstance(item.get("tagIds"), list) else []
|
|
683
|
+
compact = {
|
|
684
|
+
"app_key": app_key,
|
|
685
|
+
"title": title,
|
|
686
|
+
"form_id": item.get("formId"),
|
|
687
|
+
"tag_id": package_tag_id,
|
|
688
|
+
"package_name": package_name,
|
|
689
|
+
"group_id": group_id,
|
|
690
|
+
"group_name": group_name,
|
|
691
|
+
"tag_ids": [value for value in (_coerce_positive_int(tag_id) for tag_id in tag_ids) if value is not None],
|
|
692
|
+
}
|
|
693
|
+
return {key: value for key, value in compact.items() if value not in (None, [], "", {})}
|
|
694
|
+
|
|
695
|
+
def _count_auth_members(self, auth_payload: Any, member_key: str) -> int:
|
|
696
|
+
if not isinstance(auth_payload, dict):
|
|
697
|
+
return 0
|
|
698
|
+
auth_members = auth_payload.get("authMembers")
|
|
699
|
+
if not isinstance(auth_members, dict):
|
|
700
|
+
return 0
|
|
701
|
+
members = auth_members.get(member_key)
|
|
702
|
+
return len(members) if isinstance(members, list) else 0
|
|
703
|
+
|
|
704
|
+
def _extract_include_sub_departs(self, auth_payload: Any) -> bool | None:
|
|
705
|
+
if not isinstance(auth_payload, dict):
|
|
706
|
+
return None
|
|
707
|
+
auth_members = auth_payload.get("authMembers")
|
|
708
|
+
if isinstance(auth_members, dict):
|
|
709
|
+
include_sub_departs = auth_members.get("includeSubDeparts")
|
|
710
|
+
if isinstance(include_sub_departs, bool):
|
|
711
|
+
return include_sub_departs
|
|
712
|
+
include_sub_departs = auth_payload.get("includeSubDeparts")
|
|
713
|
+
if isinstance(include_sub_departs, bool):
|
|
714
|
+
return include_sub_departs
|
|
715
|
+
return None
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
FORM_TYPE_ALIASES = {
|
|
719
|
+
"default": 1,
|
|
720
|
+
"form": 1,
|
|
721
|
+
"schema": 1,
|
|
722
|
+
"new": 1,
|
|
723
|
+
"draft": 1,
|
|
724
|
+
"edit": 1,
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def _normalize_form_type(value: int | str) -> int:
|
|
729
|
+
if isinstance(value, bool):
|
|
730
|
+
raise_tool_error(QingflowApiError.config_error("form_type must be a positive integer or supported alias"))
|
|
731
|
+
if isinstance(value, int):
|
|
732
|
+
return value
|
|
733
|
+
text = str(value or "").strip().lower()
|
|
734
|
+
if not text:
|
|
735
|
+
raise_tool_error(QingflowApiError.config_error("form_type is required"))
|
|
736
|
+
if text.isdigit():
|
|
737
|
+
return int(text)
|
|
738
|
+
if text in FORM_TYPE_ALIASES:
|
|
739
|
+
return FORM_TYPE_ALIASES[text]
|
|
740
|
+
raise_tool_error(QingflowApiError.config_error("form_type must be a positive integer or one of: default, form, schema, new, draft, edit"))
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def _normalize_view_list(payload: Any) -> list[JSONObject]:
|
|
744
|
+
if not isinstance(payload, list):
|
|
745
|
+
return []
|
|
746
|
+
flattened: list[JSONObject] = []
|
|
747
|
+
for group in payload:
|
|
748
|
+
if not isinstance(group, dict):
|
|
749
|
+
continue
|
|
750
|
+
view_list = group.get("viewList")
|
|
751
|
+
if not isinstance(view_list, list):
|
|
752
|
+
continue
|
|
753
|
+
for item in view_list:
|
|
754
|
+
if isinstance(item, dict) and item.get("viewKey"):
|
|
755
|
+
flattened.append(item)
|
|
756
|
+
return flattened
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
def _analysis_supported_for_view_type(view_type: str | None) -> bool:
|
|
760
|
+
normalized = str(view_type or "").strip().lower()
|
|
761
|
+
if not normalized:
|
|
762
|
+
return True
|
|
763
|
+
return normalized not in {"boardview", "ganttview"}
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def _derive_import_capability(base_info: Any) -> tuple[JSONObject, list[JSONObject]]:
|
|
767
|
+
warnings: list[JSONObject] = []
|
|
768
|
+
if not isinstance(base_info, dict):
|
|
769
|
+
warnings.append(
|
|
770
|
+
{
|
|
771
|
+
"code": "IMPORT_CAPABILITY_UNAVAILABLE",
|
|
772
|
+
"message": "app_get could not determine import capability because baseInfo was unavailable.",
|
|
773
|
+
}
|
|
774
|
+
)
|
|
775
|
+
return _unknown_import_capability(), warnings
|
|
776
|
+
|
|
777
|
+
has_data_import_status = "dataImportStatus" in base_info
|
|
778
|
+
has_data_manage_status = "dataManageStatus" in base_info
|
|
779
|
+
applicant_import_enabled = _coerce_optional_bool(base_info.get("dataImportStatus")) if has_data_import_status else None
|
|
780
|
+
data_manage_status = _coerce_optional_bool(base_info.get("dataManageStatus")) if has_data_manage_status else None
|
|
781
|
+
|
|
782
|
+
if applicant_import_enabled is True:
|
|
783
|
+
return {
|
|
784
|
+
"can_import": True,
|
|
785
|
+
"auth_source": "apply_auth",
|
|
786
|
+
"applicant_import_enabled": True,
|
|
787
|
+
"data_manage_status": data_manage_status,
|
|
788
|
+
"runtime_checks_required": ["user_disabled", "function_demoted"],
|
|
789
|
+
"confidence": "preflight",
|
|
790
|
+
}, warnings
|
|
791
|
+
|
|
792
|
+
if data_manage_status is True:
|
|
793
|
+
return {
|
|
794
|
+
"can_import": True,
|
|
795
|
+
"auth_source": "data_manage_auth",
|
|
796
|
+
"applicant_import_enabled": applicant_import_enabled,
|
|
797
|
+
"data_manage_status": True,
|
|
798
|
+
"runtime_checks_required": ["user_disabled", "function_demoted"],
|
|
799
|
+
"confidence": "preflight",
|
|
800
|
+
}, warnings
|
|
801
|
+
|
|
802
|
+
if applicant_import_enabled is False and data_manage_status is False:
|
|
803
|
+
return {
|
|
804
|
+
"can_import": False,
|
|
805
|
+
"auth_source": "none",
|
|
806
|
+
"applicant_import_enabled": False,
|
|
807
|
+
"data_manage_status": False,
|
|
808
|
+
"runtime_checks_required": [],
|
|
809
|
+
"confidence": "preflight",
|
|
810
|
+
}, warnings
|
|
811
|
+
|
|
812
|
+
warnings.append(
|
|
813
|
+
{
|
|
814
|
+
"code": "IMPORT_CAPABILITY_UNAVAILABLE",
|
|
815
|
+
"message": "app_get could not fully determine import capability because baseInfo did not include a complete import permission summary.",
|
|
816
|
+
}
|
|
817
|
+
)
|
|
818
|
+
return _unknown_import_capability(
|
|
819
|
+
applicant_import_enabled=applicant_import_enabled,
|
|
820
|
+
data_manage_status=data_manage_status,
|
|
821
|
+
), warnings
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def _unknown_import_capability(
|
|
825
|
+
*,
|
|
826
|
+
applicant_import_enabled: bool | None = None,
|
|
827
|
+
data_manage_status: bool | None = None,
|
|
828
|
+
) -> JSONObject:
|
|
829
|
+
return {
|
|
830
|
+
"can_import": None,
|
|
831
|
+
"auth_source": "unknown",
|
|
832
|
+
"applicant_import_enabled": applicant_import_enabled,
|
|
833
|
+
"data_manage_status": data_manage_status,
|
|
834
|
+
"runtime_checks_required": [],
|
|
835
|
+
"confidence": "unknown",
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
def _coerce_positive_int(value: Any) -> int | None:
|
|
840
|
+
try:
|
|
841
|
+
number = int(value)
|
|
842
|
+
except (TypeError, ValueError):
|
|
843
|
+
return None
|
|
844
|
+
return number if number > 0 else None
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
def _coerce_optional_bool(value: Any) -> bool | None:
|
|
848
|
+
if isinstance(value, bool):
|
|
849
|
+
return value
|
|
850
|
+
return None
|