@josephyan/qingflow-cli 0.2.0-beta.1000

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