@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.
Files changed (60) hide show
  1. package/README.md +6 -3
  2. package/docs/local-agent-install.md +54 -3
  3. package/entry_point.py +1 -1
  4. package/npm/bin/qingflow-skills.mjs +5 -0
  5. package/npm/lib/runtime.mjs +304 -13
  6. package/npm/scripts/postinstall.mjs +1 -5
  7. package/package.json +3 -2
  8. package/pyproject.toml +1 -1
  9. package/skills/qingflow-app-builder/SKILL.md +12 -12
  10. package/skills/qingflow-app-builder/references/create-app.md +3 -3
  11. package/skills/qingflow-app-builder/references/environments.md +1 -1
  12. package/skills/qingflow-app-builder/references/gotchas.md +1 -1
  13. package/skills/qingflow-app-builder/references/public-surface-sync.md +75 -0
  14. package/skills/qingflow-app-builder/references/tool-selection.md +6 -5
  15. package/skills/qingflow-app-builder/references/update-views.md +1 -1
  16. package/skills/qingflow-app-builder-code-integrations/SKILL.md +3 -3
  17. package/skills/qingflow-app-builder-code-integrations/references/code-block.md +1 -1
  18. package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +1 -1
  19. package/src/qingflow_mcp/__main__.py +6 -2
  20. package/src/qingflow_mcp/builder_facade/models.py +41 -2
  21. package/src/qingflow_mcp/builder_facade/service.py +2743 -423
  22. package/src/qingflow_mcp/cli/commands/app.py +3 -16
  23. package/src/qingflow_mcp/cli/commands/builder.py +30 -4
  24. package/src/qingflow_mcp/cli/commands/exports.py +2 -2
  25. package/src/qingflow_mcp/cli/commands/imports.py +1 -1
  26. package/src/qingflow_mcp/cli/commands/record.py +54 -11
  27. package/src/qingflow_mcp/cli/context.py +0 -3
  28. package/src/qingflow_mcp/cli/formatters.py +238 -8
  29. package/src/qingflow_mcp/cli/main.py +47 -3
  30. package/src/qingflow_mcp/errors.py +43 -2
  31. package/src/qingflow_mcp/public_surface.py +24 -16
  32. package/src/qingflow_mcp/response_trim.py +119 -12
  33. package/src/qingflow_mcp/server.py +17 -14
  34. package/src/qingflow_mcp/server_app_builder.py +29 -7
  35. package/src/qingflow_mcp/server_app_user.py +23 -24
  36. package/src/qingflow_mcp/solution/compiler/icon_utils.py +294 -0
  37. package/src/qingflow_mcp/solution/executor.py +112 -15
  38. package/src/qingflow_mcp/tools/ai_builder_tools.py +497 -65
  39. package/src/qingflow_mcp/tools/app_tools.py +237 -51
  40. package/src/qingflow_mcp/tools/approval_tools.py +196 -34
  41. package/src/qingflow_mcp/tools/auth_tools.py +92 -16
  42. package/src/qingflow_mcp/tools/code_block_tools.py +296 -39
  43. package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
  44. package/src/qingflow_mcp/tools/directory_tools.py +236 -72
  45. package/src/qingflow_mcp/tools/export_tools.py +230 -33
  46. package/src/qingflow_mcp/tools/file_tools.py +7 -3
  47. package/src/qingflow_mcp/tools/import_tools.py +293 -40
  48. package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
  49. package/src/qingflow_mcp/tools/package_tools.py +134 -8
  50. package/src/qingflow_mcp/tools/portal_tools.py +39 -3
  51. package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
  52. package/src/qingflow_mcp/tools/record_tools.py +2305 -442
  53. package/src/qingflow_mcp/tools/resource_read_tools.py +191 -39
  54. package/src/qingflow_mcp/tools/role_tools.py +80 -9
  55. package/src/qingflow_mcp/tools/solution_tools.py +57 -15
  56. package/src/qingflow_mcp/tools/task_context_tools.py +569 -119
  57. package/src/qingflow_mcp/tools/task_tools.py +113 -29
  58. package/src/qingflow_mcp/tools/view_tools.py +106 -3
  59. package/src/qingflow_mcp/tools/workflow_tools.py +17 -1
  60. 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
- result = self.backend.request("GET", context, "/app/item", params=params)
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 exc.backend_code not in {40002, 40027}:
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 fully verify create access for {app_key}: "
187
- f"{exc.message or 'probe failed'}"
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
- accessible_views.extend(self._resolve_accessible_custom_views(context, app_key))
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
- result = self.backend.request("GET", context, f"/app/{app_key}/baseInfo")
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.backend_code in {40002, 40027}:
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.backend_code in {40002, 40027}:
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
- warnings.append(
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.backend_code in {40002, 40027}:
514
- return []
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