@qingflow-tech/qingflow-app-builder-mcp 1.0.10 → 1.0.12
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 +6 -3
- package/docs/local-agent-install.md +54 -3
- package/entry_point.py +1 -1
- package/npm/bin/qingflow-skills.mjs +5 -0
- package/npm/lib/runtime.mjs +304 -13
- package/npm/scripts/postinstall.mjs +1 -5
- package/package.json +3 -2
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +12 -12
- package/skills/qingflow-app-builder/references/create-app.md +3 -3
- package/skills/qingflow-app-builder/references/environments.md +1 -1
- package/skills/qingflow-app-builder/references/gotchas.md +1 -1
- package/skills/qingflow-app-builder/references/public-surface-sync.md +75 -0
- package/skills/qingflow-app-builder/references/tool-selection.md +6 -5
- package/skills/qingflow-app-builder/references/update-views.md +1 -1
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +3 -3
- package/skills/qingflow-app-builder-code-integrations/references/code-block.md +1 -1
- package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +1 -1
- package/src/qingflow_mcp/__main__.py +6 -2
- package/src/qingflow_mcp/builder_facade/models.py +41 -2
- package/src/qingflow_mcp/builder_facade/service.py +2743 -423
- package/src/qingflow_mcp/cli/commands/app.py +3 -16
- package/src/qingflow_mcp/cli/commands/builder.py +30 -4
- package/src/qingflow_mcp/cli/commands/exports.py +2 -2
- package/src/qingflow_mcp/cli/commands/imports.py +1 -1
- package/src/qingflow_mcp/cli/commands/record.py +54 -11
- package/src/qingflow_mcp/cli/context.py +0 -3
- package/src/qingflow_mcp/cli/formatters.py +238 -8
- package/src/qingflow_mcp/cli/main.py +47 -3
- package/src/qingflow_mcp/errors.py +43 -2
- package/src/qingflow_mcp/public_surface.py +24 -16
- package/src/qingflow_mcp/response_trim.py +119 -12
- package/src/qingflow_mcp/server.py +17 -14
- package/src/qingflow_mcp/server_app_builder.py +29 -7
- package/src/qingflow_mcp/server_app_user.py +23 -24
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +294 -0
- package/src/qingflow_mcp/solution/executor.py +112 -15
- package/src/qingflow_mcp/tools/ai_builder_tools.py +497 -65
- package/src/qingflow_mcp/tools/app_tools.py +237 -51
- package/src/qingflow_mcp/tools/approval_tools.py +196 -34
- package/src/qingflow_mcp/tools/auth_tools.py +92 -16
- package/src/qingflow_mcp/tools/code_block_tools.py +296 -39
- package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
- package/src/qingflow_mcp/tools/directory_tools.py +236 -72
- package/src/qingflow_mcp/tools/export_tools.py +230 -33
- package/src/qingflow_mcp/tools/file_tools.py +7 -3
- package/src/qingflow_mcp/tools/import_tools.py +293 -40
- package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
- package/src/qingflow_mcp/tools/package_tools.py +134 -8
- package/src/qingflow_mcp/tools/portal_tools.py +39 -3
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
- package/src/qingflow_mcp/tools/record_tools.py +2305 -442
- package/src/qingflow_mcp/tools/resource_read_tools.py +191 -39
- package/src/qingflow_mcp/tools/role_tools.py +80 -9
- package/src/qingflow_mcp/tools/solution_tools.py +57 -15
- package/src/qingflow_mcp/tools/task_context_tools.py +569 -119
- package/src/qingflow_mcp/tools/task_tools.py +113 -29
- package/src/qingflow_mcp/tools/view_tools.py +106 -3
- package/src/qingflow_mcp/tools/workflow_tools.py +17 -1
- package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
|
@@ -7,9 +7,10 @@ from mcp.server.fastmcp import FastMCP
|
|
|
7
7
|
|
|
8
8
|
from ..backend_client import BackendRequestContext
|
|
9
9
|
from ..config import DEFAULT_PROFILE
|
|
10
|
-
from ..errors import QingflowApiError, raise_tool_error
|
|
10
|
+
from ..errors import QingflowApiError, backend_code_int, is_auth_like_error, raise_tool_error
|
|
11
11
|
from ..json_types import JSONObject
|
|
12
12
|
from ..list_type_labels import SYSTEM_VIEW_DEFINITIONS, get_app_publish_status_label
|
|
13
|
+
from ..solution.compiler.icon_utils import workspace_icon_config
|
|
13
14
|
from .base import ToolBase, tool_cn_name
|
|
14
15
|
|
|
15
16
|
|
|
@@ -18,7 +19,7 @@ class AppTools(ToolBase):
|
|
|
18
19
|
|
|
19
20
|
类型:应用元数据与配置工具。
|
|
20
21
|
主要职责:
|
|
21
|
-
1.
|
|
22
|
+
1. 查询应用列表、读取应用详情;
|
|
22
23
|
2. 读取与更新应用基础配置、表单结构与发布状态;
|
|
23
24
|
3. 提供应用创建、删除、发布等管理能力。
|
|
24
25
|
"""
|
|
@@ -26,12 +27,8 @@ class AppTools(ToolBase):
|
|
|
26
27
|
def register(self, mcp: FastMCP) -> None:
|
|
27
28
|
"""注册当前工具到 MCP 服务。"""
|
|
28
29
|
@mcp.tool()
|
|
29
|
-
def app_list(profile: str = DEFAULT_PROFILE, ship_auth: bool = False) -> JSONObject:
|
|
30
|
-
return self.app_list(profile=profile, ship_auth=ship_auth)
|
|
31
|
-
|
|
32
|
-
@mcp.tool()
|
|
33
|
-
def app_search(profile: str = DEFAULT_PROFILE, keyword: str = "", page_num: int = 1, page_size: int = 50) -> JSONObject:
|
|
34
|
-
return self.app_search(profile=profile, keyword=keyword, page_num=page_num, page_size=page_size)
|
|
30
|
+
def app_list(profile: str = DEFAULT_PROFILE, query: str = "", keyword: str = "", ship_auth: bool = False) -> JSONObject:
|
|
31
|
+
return self.app_list(profile=profile, query=query, keyword=keyword, ship_auth=ship_auth)
|
|
35
32
|
|
|
36
33
|
@mcp.tool()
|
|
37
34
|
def app_get(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
|
|
@@ -90,24 +87,65 @@ class AppTools(ToolBase):
|
|
|
90
87
|
return self.app_publish(profile=profile, app_key=app_key, payload=payload or {})
|
|
91
88
|
|
|
92
89
|
@tool_cn_name("应用列表")
|
|
93
|
-
def app_list(self, *, profile: str, ship_auth: bool = False) -> JSONObject:
|
|
90
|
+
def app_list(self, *, profile: str, query: str = "", keyword: str = "", ship_auth: bool = False) -> JSONObject:
|
|
94
91
|
"""List current-user visible apps in the selected workspace."""
|
|
95
92
|
def runner(session_profile, context):
|
|
96
93
|
result = self.backend.request("GET", context, "/tag/apps")
|
|
97
94
|
items, source_shape = self._extract_visible_apps(result)
|
|
95
|
+
normalized_query, warnings = _normalize_app_list_query(query=query, keyword=keyword)
|
|
96
|
+
unfiltered_count = len(items)
|
|
97
|
+
if normalized_query:
|
|
98
|
+
items = self._filter_visible_apps(items, normalized_query)
|
|
98
99
|
response = {
|
|
99
100
|
"profile": profile,
|
|
100
101
|
"ws_id": session_profile.selected_ws_id,
|
|
101
102
|
"items": items,
|
|
102
103
|
"count": len(items),
|
|
103
104
|
"source_shape": source_shape,
|
|
105
|
+
"query": normalized_query,
|
|
106
|
+
"matched_count": len(items),
|
|
107
|
+
"unfiltered_count": unfiltered_count,
|
|
108
|
+
"filter_mode": "local_visible_apps",
|
|
104
109
|
}
|
|
110
|
+
if warnings:
|
|
111
|
+
response["warnings"] = warnings
|
|
105
112
|
if ship_auth:
|
|
106
113
|
response["raw"] = result
|
|
107
114
|
return response
|
|
108
115
|
|
|
109
116
|
return self._run(profile, runner)
|
|
110
117
|
|
|
118
|
+
def _filter_visible_apps(self, items: list[JSONObject], query: str) -> list[JSONObject]:
|
|
119
|
+
"""Filter current-user visible apps locally without calling admin search APIs."""
|
|
120
|
+
needle = query.casefold()
|
|
121
|
+
matched: list[JSONObject] = []
|
|
122
|
+
for item in items:
|
|
123
|
+
haystacks = (
|
|
124
|
+
item.get("app_key"),
|
|
125
|
+
item.get("app_name"),
|
|
126
|
+
item.get("title"),
|
|
127
|
+
item.get("package_name"),
|
|
128
|
+
item.get("tag_name"),
|
|
129
|
+
item.get("group_name"),
|
|
130
|
+
)
|
|
131
|
+
if any(needle in str(value).casefold() for value in haystacks if value not in (None, "")):
|
|
132
|
+
matched.append(item)
|
|
133
|
+
return matched
|
|
134
|
+
|
|
135
|
+
def _resolve_visible_app(self, context: BackendRequestContext, app_key: str) -> JSONObject | None:
|
|
136
|
+
"""Resolve app display metadata from the current user's visible app tree."""
|
|
137
|
+
try:
|
|
138
|
+
result = self.backend.request("GET", context, "/tag/apps")
|
|
139
|
+
except QingflowApiError as exc:
|
|
140
|
+
if not _is_optional_app_metadata_error(exc):
|
|
141
|
+
raise
|
|
142
|
+
return None
|
|
143
|
+
items, _source_shape = self._extract_visible_apps(result)
|
|
144
|
+
for item in items:
|
|
145
|
+
if str(item.get("app_key") or "").strip() == app_key:
|
|
146
|
+
return item
|
|
147
|
+
return None
|
|
148
|
+
|
|
111
149
|
@tool_cn_name("应用搜索")
|
|
112
150
|
def app_search(self, *, profile: str, keyword: str = "", page_num: int = 1, page_size: int = 50) -> JSONObject:
|
|
113
151
|
"""Search apps by keyword in name/title using backend search API.
|
|
@@ -117,9 +155,50 @@ class AppTools(ToolBase):
|
|
|
117
155
|
params: JSONObject = {"pageNum": page_num, "pageSize": page_size}
|
|
118
156
|
if keyword:
|
|
119
157
|
params["queryKey"] = keyword
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
158
|
+
|
|
159
|
+
warnings: list[JSONObject] = []
|
|
160
|
+
source = "app_item_search"
|
|
161
|
+
try:
|
|
162
|
+
result = self.backend.request("GET", context, "/app/item", params=params)
|
|
163
|
+
except QingflowApiError as exc:
|
|
164
|
+
if is_auth_like_error(exc):
|
|
165
|
+
raise
|
|
166
|
+
if not _is_optional_app_metadata_error(exc):
|
|
167
|
+
raise
|
|
168
|
+
visible_payload = self.backend.request("GET", context, "/tag/apps")
|
|
169
|
+
all_apps, source_shape = self._extract_visible_apps(visible_payload)
|
|
170
|
+
matched_apps = self._filter_visible_apps(all_apps, keyword) if keyword else all_apps
|
|
171
|
+
page_start = max(page_num - 1, 0) * page_size
|
|
172
|
+
page_end = page_start + page_size
|
|
173
|
+
apps = matched_apps[page_start:page_end]
|
|
174
|
+
warnings.append(
|
|
175
|
+
{
|
|
176
|
+
"code": "APP_SEARCH_FALLBACK_VISIBLE_APPS",
|
|
177
|
+
"message": (
|
|
178
|
+
"app_search used the current user's visible app tree because the global app search "
|
|
179
|
+
"endpoint is unavailable in this permission context."
|
|
180
|
+
),
|
|
181
|
+
"backend_code": exc.backend_code,
|
|
182
|
+
"http_status": exc.http_status,
|
|
183
|
+
"request_id": exc.request_id,
|
|
184
|
+
}
|
|
185
|
+
)
|
|
186
|
+
return {
|
|
187
|
+
"profile": profile,
|
|
188
|
+
"ws_id": session_profile.selected_ws_id,
|
|
189
|
+
"keyword": keyword,
|
|
190
|
+
"page_num": page_num,
|
|
191
|
+
"page_size": page_size,
|
|
192
|
+
"total": len(matched_apps),
|
|
193
|
+
"items": apps,
|
|
194
|
+
"apps": apps,
|
|
195
|
+
"source": "visible_apps_fallback",
|
|
196
|
+
"source_shape": source_shape,
|
|
197
|
+
"unfiltered_count": len(all_apps),
|
|
198
|
+
"matched_count": len(matched_apps),
|
|
199
|
+
"warnings": warnings,
|
|
200
|
+
}
|
|
201
|
+
|
|
123
202
|
apps = []
|
|
124
203
|
if isinstance(result, dict):
|
|
125
204
|
items = result.get("list", [])
|
|
@@ -144,6 +223,7 @@ class AppTools(ToolBase):
|
|
|
144
223
|
"total": result.get("total") if isinstance(result, dict) else len(apps),
|
|
145
224
|
"items": apps,
|
|
146
225
|
"apps": apps,
|
|
226
|
+
"source": source,
|
|
147
227
|
}
|
|
148
228
|
|
|
149
229
|
return self._run(profile, runner)
|
|
@@ -157,6 +237,8 @@ class AppTools(ToolBase):
|
|
|
157
237
|
warnings: list[JSONObject] = []
|
|
158
238
|
app_name = app_key
|
|
159
239
|
base_info: JSONObject | None = None
|
|
240
|
+
app_icon = None
|
|
241
|
+
visible_app: JSONObject | None = None
|
|
160
242
|
|
|
161
243
|
try:
|
|
162
244
|
base_info = self.backend.request("GET", context, f"/app/{app_key}/baseInfo")
|
|
@@ -165,32 +247,33 @@ class AppTools(ToolBase):
|
|
|
165
247
|
str(base_info.get("formTitle") or base_info.get("title") or base_info.get("appName") or app_key).strip()
|
|
166
248
|
or app_key
|
|
167
249
|
)
|
|
250
|
+
app_icon = str(base_info.get("appIcon") or "").strip() or None
|
|
168
251
|
except QingflowApiError as exc:
|
|
169
|
-
if
|
|
252
|
+
if not _is_optional_app_metadata_error(exc):
|
|
170
253
|
raise
|
|
254
|
+
visible_app = self._resolve_visible_app(context, app_key)
|
|
255
|
+
if visible_app is not None:
|
|
256
|
+
app_name = str(visible_app.get("app_name") or visible_app.get("title") or app_key).strip() or app_key
|
|
257
|
+
app_icon = str(visible_app.get("app_icon") or "").strip() or None
|
|
171
258
|
warnings.append(
|
|
172
|
-
|
|
259
|
+
_with_api_error_fields(
|
|
260
|
+
{
|
|
173
261
|
"code": "APP_BASE_INFO_UNAVAILABLE",
|
|
174
|
-
"message": f"app_get could not load base info for {app_key}; using app_key as app_name.",
|
|
175
|
-
}
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
try:
|
|
179
|
-
can_create = self._probe_create_access(context, app_key)
|
|
180
|
-
except QingflowApiError as exc:
|
|
181
|
-
can_create = False
|
|
182
|
-
warnings.append(
|
|
183
|
-
{
|
|
184
|
-
"code": "APP_CREATE_PROBE_UNVERIFIED",
|
|
185
262
|
"message": (
|
|
186
|
-
f"app_get could not
|
|
187
|
-
|
|
263
|
+
f"app_get could not load base info for {app_key}; "
|
|
264
|
+
"using the current user's visible app tree when available."
|
|
188
265
|
),
|
|
189
|
-
|
|
266
|
+
},
|
|
267
|
+
exc,
|
|
268
|
+
)
|
|
190
269
|
)
|
|
270
|
+
|
|
271
|
+
can_create = self._probe_create_access(context, app_key)
|
|
191
272
|
accessible_views, system_view_warnings = self._resolve_accessible_system_views(context, app_key)
|
|
192
273
|
warnings.extend(system_view_warnings)
|
|
193
|
-
|
|
274
|
+
custom_views, custom_view_warnings = self._resolve_accessible_custom_views(context, app_key)
|
|
275
|
+
warnings.extend(custom_view_warnings)
|
|
276
|
+
accessible_views.extend(custom_views)
|
|
194
277
|
import_capability, import_warnings = _derive_import_capability(base_info)
|
|
195
278
|
warnings.extend(import_warnings)
|
|
196
279
|
return {
|
|
@@ -202,6 +285,9 @@ class AppTools(ToolBase):
|
|
|
202
285
|
"data": {
|
|
203
286
|
"app_key": app_key,
|
|
204
287
|
"app_name": app_name,
|
|
288
|
+
"app_icon": app_icon,
|
|
289
|
+
"icon_config": workspace_icon_config(app_icon),
|
|
290
|
+
**({"visible_app": visible_app} if base_info is None and visible_app is not None else {}),
|
|
205
291
|
"can_create": can_create,
|
|
206
292
|
"import_capability": import_capability,
|
|
207
293
|
"accessible_views": accessible_views,
|
|
@@ -216,7 +302,30 @@ class AppTools(ToolBase):
|
|
|
216
302
|
self._require_app_key(app_key)
|
|
217
303
|
|
|
218
304
|
def runner(session_profile, context):
|
|
219
|
-
|
|
305
|
+
warnings: list[JSONObject] = []
|
|
306
|
+
verification: JSONObject = {"base_info_verified": True, "fallback_visible_app_used": False}
|
|
307
|
+
try:
|
|
308
|
+
result = self.backend.request("GET", context, f"/app/{app_key}/baseInfo")
|
|
309
|
+
except QingflowApiError as exc:
|
|
310
|
+
if not _is_optional_app_metadata_error(exc):
|
|
311
|
+
raise
|
|
312
|
+
visible_app = self._resolve_visible_app(context, app_key)
|
|
313
|
+
if visible_app is None:
|
|
314
|
+
raise
|
|
315
|
+
result = self._base_info_from_visible_app(app_key, visible_app)
|
|
316
|
+
warnings.append(
|
|
317
|
+
_with_api_error_fields(
|
|
318
|
+
{
|
|
319
|
+
"code": "APP_BASE_INFO_UNAVAILABLE",
|
|
320
|
+
"message": (
|
|
321
|
+
f"app_get_base could not load base info for {app_key}; "
|
|
322
|
+
"using the current user's visible app tree."
|
|
323
|
+
),
|
|
324
|
+
},
|
|
325
|
+
exc,
|
|
326
|
+
)
|
|
327
|
+
)
|
|
328
|
+
verification = {"base_info_verified": False, "fallback_visible_app_used": True}
|
|
220
329
|
publish_status = result.get("appPublishStatus") if isinstance(result, dict) else None
|
|
221
330
|
compact = self._compact_base_info(result if isinstance(result, dict) else {})
|
|
222
331
|
response = {
|
|
@@ -227,7 +336,10 @@ class AppTools(ToolBase):
|
|
|
227
336
|
"app_publish_status": publish_status,
|
|
228
337
|
"app_publish_status_label": get_app_publish_status_label(publish_status if isinstance(publish_status, int) else None),
|
|
229
338
|
"compact": not include_raw,
|
|
339
|
+
"verification": verification,
|
|
230
340
|
}
|
|
341
|
+
if warnings:
|
|
342
|
+
response["warnings"] = warnings
|
|
231
343
|
if include_raw:
|
|
232
344
|
response["summary"] = compact
|
|
233
345
|
return response
|
|
@@ -461,7 +573,9 @@ class AppTools(ToolBase):
|
|
|
461
573
|
)
|
|
462
574
|
return True
|
|
463
575
|
except QingflowApiError as exc:
|
|
464
|
-
if exc
|
|
576
|
+
if is_auth_like_error(exc):
|
|
577
|
+
raise
|
|
578
|
+
if backend_code_int(exc) in {40002, 40027, 404} or exc.http_status == 404:
|
|
465
579
|
return False
|
|
466
580
|
raise
|
|
467
581
|
|
|
@@ -476,7 +590,9 @@ class AppTools(ToolBase):
|
|
|
476
590
|
)
|
|
477
591
|
return True
|
|
478
592
|
except QingflowApiError as exc:
|
|
479
|
-
if exc
|
|
593
|
+
if is_auth_like_error(exc):
|
|
594
|
+
raise
|
|
595
|
+
if backend_code_int(exc) in {40002, 40027, 404} or exc.http_status == 404:
|
|
480
596
|
return False
|
|
481
597
|
raise
|
|
482
598
|
|
|
@@ -488,40 +604,42 @@ class AppTools(ToolBase):
|
|
|
488
604
|
try:
|
|
489
605
|
can_access = self._probe_list_type_access(context, app_key, list_type)
|
|
490
606
|
except QingflowApiError as exc:
|
|
491
|
-
|
|
492
|
-
{
|
|
493
|
-
"code": "SYSTEM_VIEW_PROBE_UNVERIFIED",
|
|
494
|
-
"message": (
|
|
495
|
-
f"app_get skipped system view '{name}' because access probing failed: "
|
|
496
|
-
f"{exc.message or 'probe failed'}"
|
|
497
|
-
),
|
|
498
|
-
"view_id": view_id,
|
|
499
|
-
"list_type": list_type,
|
|
500
|
-
}
|
|
501
|
-
)
|
|
502
|
-
continue
|
|
607
|
+
raise
|
|
503
608
|
if not can_access:
|
|
504
609
|
continue
|
|
505
610
|
items.append({"view_id": view_id, "name": name, "kind": "system", "analysis_supported": True})
|
|
506
611
|
return items, warnings
|
|
507
612
|
|
|
508
|
-
def _resolve_accessible_custom_views(self, context: BackendRequestContext, app_key: str) -> list[JSONObject]:
|
|
613
|
+
def _resolve_accessible_custom_views(self, context: BackendRequestContext, app_key: str) -> tuple[list[JSONObject], list[JSONObject]]:
|
|
509
614
|
"""执行内部辅助逻辑。"""
|
|
615
|
+
warnings: list[JSONObject] = []
|
|
510
616
|
try:
|
|
511
617
|
payload = self.backend.request("GET", context, f"/app/{app_key}/view/viewList")
|
|
512
618
|
except QingflowApiError as exc:
|
|
513
|
-
if exc
|
|
514
|
-
|
|
619
|
+
if _is_optional_app_metadata_error(exc):
|
|
620
|
+
warnings.append(
|
|
621
|
+
_with_api_error_fields(
|
|
622
|
+
{
|
|
623
|
+
"code": "CUSTOM_VIEW_LIST_UNAVAILABLE",
|
|
624
|
+
"message": (
|
|
625
|
+
f"app_get could not load custom views for {app_key}; "
|
|
626
|
+
"system views and other app metadata may still be usable."
|
|
627
|
+
),
|
|
628
|
+
},
|
|
629
|
+
exc,
|
|
630
|
+
)
|
|
631
|
+
)
|
|
632
|
+
return [], warnings
|
|
515
633
|
raise
|
|
516
634
|
|
|
517
635
|
items: list[JSONObject] = []
|
|
518
636
|
for item in _normalize_view_list(payload):
|
|
519
|
-
view_key = str(item.get("viewKey") or "").strip()
|
|
637
|
+
view_key = str(item.get("viewKey") or item.get("viewgraphKey") or "").strip()
|
|
520
638
|
if not view_key:
|
|
521
639
|
continue
|
|
522
640
|
normalized: JSONObject = {
|
|
523
641
|
"view_id": f"custom:{view_key}",
|
|
524
|
-
"name": str(item.get("viewName") or view_key).strip() or view_key,
|
|
642
|
+
"name": str(item.get("viewName") or item.get("viewgraphName") or view_key).strip() or view_key,
|
|
525
643
|
"kind": "custom",
|
|
526
644
|
}
|
|
527
645
|
view_type = str(item.get("viewType") or item.get("viewgraphType") or "").strip()
|
|
@@ -529,7 +647,7 @@ class AppTools(ToolBase):
|
|
|
529
647
|
normalized["view_type"] = view_type
|
|
530
648
|
normalized["analysis_supported"] = _analysis_supported_for_view_type(view_type or None)
|
|
531
649
|
items.append(normalized)
|
|
532
|
-
return items
|
|
650
|
+
return items, warnings
|
|
533
651
|
|
|
534
652
|
def _compact_base_info(self, result: dict[str, Any]) -> JSONObject:
|
|
535
653
|
"""执行内部辅助逻辑。"""
|
|
@@ -573,6 +691,30 @@ class AppTools(ToolBase):
|
|
|
573
691
|
},
|
|
574
692
|
}
|
|
575
693
|
|
|
694
|
+
def _base_info_from_visible_app(self, app_key: str, visible_app: JSONObject) -> JSONObject:
|
|
695
|
+
"""Build a base-info-shaped fallback from a current-user visible app item."""
|
|
696
|
+
tag_ids: list[int] = []
|
|
697
|
+
tag_id = _coerce_positive_int(visible_app.get("tag_id"))
|
|
698
|
+
if tag_id is not None:
|
|
699
|
+
tag_ids.append(tag_id)
|
|
700
|
+
for value in visible_app.get("tag_ids") if isinstance(visible_app.get("tag_ids"), list) else []:
|
|
701
|
+
tag_id = _coerce_positive_int(value)
|
|
702
|
+
if tag_id is not None and tag_id not in tag_ids:
|
|
703
|
+
tag_ids.append(tag_id)
|
|
704
|
+
|
|
705
|
+
fallback: JSONObject = {
|
|
706
|
+
"appKey": app_key,
|
|
707
|
+
"formId": visible_app.get("form_id"),
|
|
708
|
+
"formTitle": visible_app.get("app_name") or visible_app.get("title") or app_key,
|
|
709
|
+
"appIcon": visible_app.get("app_icon"),
|
|
710
|
+
"tagIds": tag_ids,
|
|
711
|
+
"visibleApp": visible_app,
|
|
712
|
+
}
|
|
713
|
+
package_name = visible_app.get("package_name") or visible_app.get("tag_name") or visible_app.get("group_name")
|
|
714
|
+
if package_name:
|
|
715
|
+
fallback["tagItems"] = [{"tagId": tag_id, "tagName": package_name} for tag_id in tag_ids]
|
|
716
|
+
return fallback
|
|
717
|
+
|
|
576
718
|
def _compact_form_schema(self, result: dict[str, Any]) -> JSONObject:
|
|
577
719
|
"""执行内部辅助逻辑。"""
|
|
578
720
|
base_questions_raw = result.get("baseQues") if isinstance(result.get("baseQues"), list) else []
|
|
@@ -731,6 +873,7 @@ class AppTools(ToolBase):
|
|
|
731
873
|
"app_key": app_key,
|
|
732
874
|
"app_name": title,
|
|
733
875
|
"title": title,
|
|
876
|
+
"app_icon": str(item.get("appIcon") or "").strip() or None,
|
|
734
877
|
"form_id": item.get("formId"),
|
|
735
878
|
"tag_id": package_tag_id,
|
|
736
879
|
"package_name": package_name,
|
|
@@ -791,17 +934,26 @@ def _normalize_form_type(value: int | str) -> int:
|
|
|
791
934
|
|
|
792
935
|
|
|
793
936
|
def _normalize_view_list(payload: Any) -> list[JSONObject]:
|
|
937
|
+
if isinstance(payload, dict):
|
|
938
|
+
raw_list = payload.get("list")
|
|
939
|
+
if isinstance(raw_list, list):
|
|
940
|
+
payload = raw_list
|
|
941
|
+
else:
|
|
942
|
+
payload = [payload]
|
|
794
943
|
if not isinstance(payload, list):
|
|
795
944
|
return []
|
|
796
945
|
flattened: list[JSONObject] = []
|
|
797
946
|
for group in payload:
|
|
798
947
|
if not isinstance(group, dict):
|
|
799
948
|
continue
|
|
949
|
+
if group.get("viewKey") or group.get("viewgraphKey"):
|
|
950
|
+
flattened.append(group)
|
|
951
|
+
continue
|
|
800
952
|
view_list = group.get("viewList")
|
|
801
953
|
if not isinstance(view_list, list):
|
|
802
954
|
continue
|
|
803
955
|
for item in view_list:
|
|
804
|
-
if isinstance(item, dict) and item.get("viewKey"):
|
|
956
|
+
if isinstance(item, dict) and (item.get("viewKey") or item.get("viewgraphKey")):
|
|
805
957
|
flattened.append(item)
|
|
806
958
|
return flattened
|
|
807
959
|
|
|
@@ -813,6 +965,23 @@ def _analysis_supported_for_view_type(view_type: str | None) -> bool:
|
|
|
813
965
|
return normalized not in {"boardview", "ganttview"}
|
|
814
966
|
|
|
815
967
|
|
|
968
|
+
def _is_optional_app_metadata_error(error: QingflowApiError) -> bool:
|
|
969
|
+
if is_auth_like_error(error):
|
|
970
|
+
return False
|
|
971
|
+
backend_code = backend_code_int(error)
|
|
972
|
+
return backend_code in {40002, 40027, 404} or error.http_status == 404
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
def _with_api_error_fields(payload: JSONObject, error: QingflowApiError) -> JSONObject:
|
|
976
|
+
if error.backend_code is not None:
|
|
977
|
+
payload["backend_code"] = error.backend_code
|
|
978
|
+
if error.http_status is not None:
|
|
979
|
+
payload["http_status"] = error.http_status
|
|
980
|
+
if error.request_id:
|
|
981
|
+
payload["request_id"] = error.request_id
|
|
982
|
+
return payload
|
|
983
|
+
|
|
984
|
+
|
|
816
985
|
def _derive_import_capability(base_info: Any) -> tuple[JSONObject, list[JSONObject]]:
|
|
817
986
|
warnings: list[JSONObject] = []
|
|
818
987
|
if not isinstance(base_info, dict):
|
|
@@ -924,3 +1093,20 @@ def _coerce_optional_bool(value: Any) -> bool | None:
|
|
|
924
1093
|
if isinstance(value, bool):
|
|
925
1094
|
return value
|
|
926
1095
|
return None
|
|
1096
|
+
|
|
1097
|
+
|
|
1098
|
+
def _normalize_app_list_query(*, query: str = "", keyword: str = "") -> tuple[str, list[JSONObject]]:
|
|
1099
|
+
normalized_query = str(query or "").strip()
|
|
1100
|
+
normalized_keyword = str(keyword or "").strip()
|
|
1101
|
+
warnings: list[JSONObject] = []
|
|
1102
|
+
if normalized_query and normalized_keyword:
|
|
1103
|
+
warnings.append(
|
|
1104
|
+
{
|
|
1105
|
+
"code": "APP_LIST_QUERY_TAKES_PRECEDENCE",
|
|
1106
|
+
"message": "Both query and keyword were provided; query was used and keyword was ignored.",
|
|
1107
|
+
"ignored_keyword": normalized_keyword,
|
|
1108
|
+
}
|
|
1109
|
+
)
|
|
1110
|
+
if normalized_query:
|
|
1111
|
+
return normalized_query, warnings
|
|
1112
|
+
return normalized_keyword, warnings
|