@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.
Files changed (79) hide show
  1. package/README.md +30 -0
  2. package/docs/local-agent-install.md +235 -0
  3. package/entry_point.py +13 -0
  4. package/npm/bin/qingflow.mjs +5 -0
  5. package/npm/lib/runtime.mjs +204 -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 +5 -0
  11. package/src/qingflow_mcp/__main__.py +5 -0
  12. package/src/qingflow_mcp/backend_client.py +547 -0
  13. package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
  14. package/src/qingflow_mcp/builder_facade/models.py +985 -0
  15. package/src/qingflow_mcp/builder_facade/service.py +8243 -0
  16. package/src/qingflow_mcp/cli/__init__.py +1 -0
  17. package/src/qingflow_mcp/cli/commands/__init__.py +15 -0
  18. package/src/qingflow_mcp/cli/commands/app.py +40 -0
  19. package/src/qingflow_mcp/cli/commands/auth.py +78 -0
  20. package/src/qingflow_mcp/cli/commands/builder.py +184 -0
  21. package/src/qingflow_mcp/cli/commands/common.py +47 -0
  22. package/src/qingflow_mcp/cli/commands/imports.py +86 -0
  23. package/src/qingflow_mcp/cli/commands/record.py +202 -0
  24. package/src/qingflow_mcp/cli/commands/task.py +87 -0
  25. package/src/qingflow_mcp/cli/commands/workspace.py +33 -0
  26. package/src/qingflow_mcp/cli/context.py +48 -0
  27. package/src/qingflow_mcp/cli/formatters.py +269 -0
  28. package/src/qingflow_mcp/cli/json_io.py +50 -0
  29. package/src/qingflow_mcp/cli/main.py +147 -0
  30. package/src/qingflow_mcp/config.py +221 -0
  31. package/src/qingflow_mcp/errors.py +66 -0
  32. package/src/qingflow_mcp/import_store.py +121 -0
  33. package/src/qingflow_mcp/json_types.py +18 -0
  34. package/src/qingflow_mcp/list_type_labels.py +76 -0
  35. package/src/qingflow_mcp/server.py +211 -0
  36. package/src/qingflow_mcp/server_app_builder.py +387 -0
  37. package/src/qingflow_mcp/server_app_user.py +317 -0
  38. package/src/qingflow_mcp/session_store.py +289 -0
  39. package/src/qingflow_mcp/solution/__init__.py +6 -0
  40. package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
  41. package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
  42. package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
  43. package/src/qingflow_mcp/solution/compiler/form_compiler.py +466 -0
  44. package/src/qingflow_mcp/solution/compiler/icon_utils.py +113 -0
  45. package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
  46. package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
  47. package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
  48. package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
  49. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
  50. package/src/qingflow_mcp/solution/design_session.py +222 -0
  51. package/src/qingflow_mcp/solution/design_store.py +100 -0
  52. package/src/qingflow_mcp/solution/executor.py +2339 -0
  53. package/src/qingflow_mcp/solution/normalizer.py +23 -0
  54. package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
  55. package/src/qingflow_mcp/solution/run_store.py +244 -0
  56. package/src/qingflow_mcp/solution/spec_models.py +853 -0
  57. package/src/qingflow_mcp/tools/__init__.py +1 -0
  58. package/src/qingflow_mcp/tools/ai_builder_tools.py +2063 -0
  59. package/src/qingflow_mcp/tools/app_tools.py +850 -0
  60. package/src/qingflow_mcp/tools/approval_tools.py +833 -0
  61. package/src/qingflow_mcp/tools/auth_tools.py +697 -0
  62. package/src/qingflow_mcp/tools/base.py +81 -0
  63. package/src/qingflow_mcp/tools/code_block_tools.py +679 -0
  64. package/src/qingflow_mcp/tools/directory_tools.py +648 -0
  65. package/src/qingflow_mcp/tools/feedback_tools.py +230 -0
  66. package/src/qingflow_mcp/tools/file_tools.py +385 -0
  67. package/src/qingflow_mcp/tools/import_tools.py +1971 -0
  68. package/src/qingflow_mcp/tools/navigation_tools.py +177 -0
  69. package/src/qingflow_mcp/tools/package_tools.py +240 -0
  70. package/src/qingflow_mcp/tools/portal_tools.py +131 -0
  71. package/src/qingflow_mcp/tools/qingbi_report_tools.py +269 -0
  72. package/src/qingflow_mcp/tools/record_tools.py +12739 -0
  73. package/src/qingflow_mcp/tools/role_tools.py +94 -0
  74. package/src/qingflow_mcp/tools/solution_tools.py +3887 -0
  75. package/src/qingflow_mcp/tools/task_context_tools.py +1423 -0
  76. package/src/qingflow_mcp/tools/task_tools.py +843 -0
  77. package/src/qingflow_mcp/tools/view_tools.py +280 -0
  78. package/src/qingflow_mcp/tools/workflow_tools.py +312 -0
  79. package/src/qingflow_mcp/tools/workspace_tools.py +219 -0
@@ -0,0 +1,280 @@
1
+ from __future__ import annotations
2
+
3
+ from mcp.server.fastmcp import FastMCP
4
+
5
+ from ..config import DEFAULT_PROFILE
6
+ from ..errors import QingflowApiError, raise_tool_error
7
+ from ..json_types import JSONObject, JSONValue
8
+ from .base import ToolBase
9
+
10
+
11
+ class ViewTools(ToolBase):
12
+ def register(self, mcp: FastMCP) -> None:
13
+ @mcp.tool()
14
+ def view_list(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
15
+ return self.view_list(profile=profile, app_key=app_key)
16
+
17
+ @mcp.tool()
18
+ def view_list_flat(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
19
+ return self.view_list_flat(profile=profile, app_key=app_key)
20
+
21
+ @mcp.tool()
22
+ def view_get_config(profile: str = DEFAULT_PROFILE, viewgraph_key: str = "") -> JSONObject:
23
+ return self.view_get_config(profile=profile, viewgraph_key=viewgraph_key)
24
+
25
+ @mcp.tool()
26
+ def view_get_base_info(profile: str = DEFAULT_PROFILE, viewgraph_key: str = "", passcode: str | None = None) -> JSONObject:
27
+ return self.view_get_base_info(profile=profile, viewgraph_key=viewgraph_key, passcode=passcode)
28
+
29
+ @mcp.tool()
30
+ def view_list_questions(profile: str = DEFAULT_PROFILE, viewgraph_key: str = "") -> JSONObject:
31
+ return self.view_list_questions(profile=profile, viewgraph_key=viewgraph_key)
32
+
33
+ @mcp.tool()
34
+ def view_list_associations(profile: str = DEFAULT_PROFILE, viewgraph_key: str = "") -> JSONObject:
35
+ return self.view_list_associations(profile=profile, viewgraph_key=viewgraph_key)
36
+
37
+ @mcp.tool()
38
+ def view_board_get_lane_base_info(profile: str = DEFAULT_PROFILE, viewgraph_key: str = "", page_num: int = 1, page_size: int = 20) -> JSONObject:
39
+ return self.view_board_get_lane_base_info(profile=profile, viewgraph_key=viewgraph_key, page_num=page_num, page_size=page_size)
40
+
41
+ @mcp.tool()
42
+ def view_get_future_nodes(profile: str = DEFAULT_PROFILE, viewgraph_key: str = "", apply_id: int = 0, app_key: str = "") -> JSONObject:
43
+ return self.view_get_future_nodes(profile=profile, viewgraph_key=viewgraph_key, apply_id=apply_id, app_key=app_key)
44
+
45
+ @mcp.tool()
46
+ def view_get_workflow_status(profile: str = DEFAULT_PROFILE, viewgraph_key: str = "", row_record_id: int = 0) -> JSONObject:
47
+ return self.view_get_workflow_status(profile=profile, viewgraph_key=viewgraph_key, row_record_id=row_record_id)
48
+
49
+ def view_list(self, *, profile: str, app_key: str) -> JSONObject:
50
+ self._require_app_key(app_key)
51
+ return self._request(profile, "GET", f"/app/{app_key}/view/viewList", app_key=app_key)
52
+
53
+ def view_list_flat(self, *, profile: str, app_key: str) -> JSONObject:
54
+ self._require_app_key(app_key)
55
+ return self._request(profile, "GET", f"/app/{app_key}/view/views", app_key=app_key)
56
+
57
+ def view_reorder(self, *, profile: str, app_key: str, payload: list[JSONObject]) -> JSONObject:
58
+ self._require_app_key(app_key)
59
+ if not isinstance(payload, list) or not payload:
60
+ raise_tool_error(QingflowApiError.config_error("payload must be a non-empty array"))
61
+ return self._request(
62
+ profile,
63
+ "POST",
64
+ f"/app/{app_key}/view/ordinal",
65
+ app_key=app_key,
66
+ json_body=self._normalize_reorder_payload(payload),
67
+ )
68
+
69
+ def view_set_column_width(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
70
+ self._require_app_key(app_key)
71
+ body = self._require_dict(payload)
72
+ return self._request(profile, "POST", f"/app/{app_key}/view/que/width", app_key=app_key, json_body=body)
73
+
74
+ def view_create(self, *, profile: str, payload: JSONObject) -> JSONObject:
75
+ body = self._require_dict(payload)
76
+ app_key = str(body.get("appKey") or "")
77
+ return self._request(profile, "POST", "/view/viewConfig", app_key=app_key, json_body=body)
78
+
79
+ def view_get_config(self, *, profile: str, viewgraph_key: str) -> JSONObject:
80
+ self._require_viewgraph_key(viewgraph_key)
81
+ return self._request(profile, "GET", f"/view/{viewgraph_key}/viewConfig", viewgraph_key=viewgraph_key)
82
+
83
+ def view_get_base_info(self, *, profile: str, viewgraph_key: str, passcode: str | None) -> JSONObject:
84
+ self._require_viewgraph_key(viewgraph_key)
85
+ params = {"pass": passcode} if passcode else None
86
+ return self._request(profile, "GET", f"/view/{viewgraph_key}/viewConfig/baseInfo", viewgraph_key=viewgraph_key, params=params)
87
+
88
+ def view_update(self, *, profile: str, viewgraph_key: str, payload: JSONObject) -> JSONObject:
89
+ self._require_viewgraph_key(viewgraph_key)
90
+ body = self._require_dict(payload)
91
+ return self._request(
92
+ profile,
93
+ "POST",
94
+ f"/view/{viewgraph_key}/viewConfig",
95
+ viewgraph_key=viewgraph_key,
96
+ json_body=body,
97
+ risk_operation="update",
98
+ risk_target="view configuration",
99
+ )
100
+
101
+ def view_delete(self, *, profile: str, viewgraph_key: str) -> JSONObject:
102
+ self._require_viewgraph_key(viewgraph_key)
103
+ return self._request(
104
+ profile,
105
+ "DELETE",
106
+ f"/view/{viewgraph_key}",
107
+ viewgraph_key=viewgraph_key,
108
+ risk_operation="delete",
109
+ risk_target="view configuration",
110
+ )
111
+
112
+ def view_copy(self, *, profile: str, viewgraph_key: str) -> JSONObject:
113
+ self._require_viewgraph_key(viewgraph_key)
114
+ return self._request(profile, "POST", f"/view/{viewgraph_key}/copy", viewgraph_key=viewgraph_key)
115
+
116
+ def view_list_questions(self, *, profile: str, viewgraph_key: str) -> JSONObject:
117
+ self._require_viewgraph_key(viewgraph_key)
118
+ return self._request(profile, "GET", f"/view/{viewgraph_key}/question", viewgraph_key=viewgraph_key)
119
+
120
+ def view_list_associations(self, *, profile: str, viewgraph_key: str) -> JSONObject:
121
+ self._require_viewgraph_key(viewgraph_key)
122
+ return self._request(profile, "GET", f"/view/{viewgraph_key}/association", viewgraph_key=viewgraph_key)
123
+
124
+ def view_update_member_config(self, *, profile: str, viewgraph_key: str, payload: JSONObject) -> JSONObject:
125
+ self._require_viewgraph_key(viewgraph_key)
126
+ body = self._require_dict(payload)
127
+ return self._request(
128
+ profile,
129
+ "POST",
130
+ f"/view/{viewgraph_key}/member/config",
131
+ viewgraph_key=viewgraph_key,
132
+ json_body=body,
133
+ risk_operation="update",
134
+ risk_target="view member visibility",
135
+ )
136
+
137
+ def view_update_apply_config(self, *, profile: str, viewgraph_key: str, payload: JSONObject) -> JSONObject:
138
+ self._require_viewgraph_key(viewgraph_key)
139
+ body = self._require_dict(payload)
140
+ return self._request(
141
+ profile,
142
+ "POST",
143
+ f"/view/{viewgraph_key}/apply/config",
144
+ viewgraph_key=viewgraph_key,
145
+ json_body=body,
146
+ risk_operation="update",
147
+ risk_target="view apply visibility",
148
+ )
149
+
150
+ def view_board_set_lane_config(self, *, profile: str, viewgraph_key: str, payload: JSONObject) -> JSONObject:
151
+ self._require_viewgraph_key(viewgraph_key)
152
+ body = self._require_dict(payload)
153
+ return self._request(
154
+ profile,
155
+ "POST",
156
+ f"/view/{viewgraph_key}/lane/config",
157
+ viewgraph_key=viewgraph_key,
158
+ json_body=body,
159
+ risk_operation="update",
160
+ risk_target="board lane configuration",
161
+ )
162
+
163
+ def view_board_get_lane_base_info(self, *, profile: str, viewgraph_key: str, page_num: int, page_size: int) -> JSONObject:
164
+ self._require_viewgraph_key(viewgraph_key)
165
+ return self._request(
166
+ profile,
167
+ "GET",
168
+ f"/view/{viewgraph_key}/lane/baseInfo",
169
+ viewgraph_key=viewgraph_key,
170
+ params={"pageNum": page_num, "pageSize": page_size},
171
+ )
172
+
173
+ def view_gantt_switch_auto_calibration(self, *, profile: str, viewgraph_key: str, payload: JSONObject) -> JSONObject:
174
+ self._require_viewgraph_key(viewgraph_key)
175
+ body = self._require_dict(payload)
176
+ return self._request(
177
+ profile,
178
+ "PUT",
179
+ f"/view/gantt/{viewgraph_key}/auto/calibration",
180
+ viewgraph_key=viewgraph_key,
181
+ json_body=body,
182
+ risk_operation="update",
183
+ risk_target="gantt auto calibration settings",
184
+ )
185
+
186
+ def view_gantt_batch_update_time(self, *, profile: str, viewgraph_key: str, payload: JSONObject) -> JSONObject:
187
+ self._require_viewgraph_key(viewgraph_key)
188
+ body = self._require_dict(payload)
189
+ return self._request(
190
+ profile,
191
+ "PUT",
192
+ f"/view/gantt/{viewgraph_key}/batch/update",
193
+ viewgraph_key=viewgraph_key,
194
+ json_body=body,
195
+ risk_operation="update",
196
+ risk_target="gantt task timing",
197
+ )
198
+
199
+ def view_get_future_nodes(self, *, profile: str, viewgraph_key: str, apply_id: int, app_key: str) -> JSONObject:
200
+ self._require_viewgraph_key(viewgraph_key)
201
+ self._require_app_key(app_key)
202
+ self._require_positive("apply_id", apply_id)
203
+ return self._request(
204
+ profile,
205
+ "GET",
206
+ f"/view/{viewgraph_key}/auditNode/futureList/{apply_id}",
207
+ viewgraph_key=viewgraph_key,
208
+ app_key=app_key,
209
+ apply_id=apply_id,
210
+ params={"appKey": app_key},
211
+ )
212
+
213
+ def view_get_workflow_status(self, *, profile: str, viewgraph_key: str, row_record_id: int) -> JSONObject:
214
+ self._require_viewgraph_key(viewgraph_key)
215
+ self._require_positive("row_record_id", row_record_id)
216
+ return self._request(profile, "GET", f"/view/{viewgraph_key}/workflow/status/{row_record_id}", viewgraph_key=viewgraph_key, row_record_id=row_record_id)
217
+
218
+ def _request(
219
+ self,
220
+ profile: str,
221
+ method: str,
222
+ path: str,
223
+ *,
224
+ json_body: JSONValue = None,
225
+ params: JSONObject | None = None,
226
+ risk_operation: str | None = None,
227
+ risk_target: str | None = None,
228
+ **extra: JSONValue,
229
+ ) -> JSONObject:
230
+ def runner(session_profile, context):
231
+ result = self.backend.request(method, context, path, json_body=json_body, params=params)
232
+ response: JSONObject = {"profile": profile, "ws_id": session_profile.selected_ws_id, "result": result}
233
+ response.update(extra)
234
+ if risk_operation and risk_target:
235
+ return self._attach_human_review_notice(response, operation=risk_operation, target=risk_target)
236
+ return response
237
+
238
+ return self._run(profile, runner)
239
+
240
+ def _require_app_key(self, app_key: str) -> None:
241
+ if not app_key:
242
+ raise_tool_error(QingflowApiError.config_error("app_key is required"))
243
+
244
+ def _require_viewgraph_key(self, viewgraph_key: str) -> None:
245
+ if not viewgraph_key:
246
+ raise_tool_error(QingflowApiError.config_error("viewgraph_key is required"))
247
+
248
+ def _require_positive(self, field_name: str, value: int) -> None:
249
+ if value <= 0:
250
+ raise_tool_error(QingflowApiError.config_error(f"{field_name} must be positive"))
251
+
252
+ def _normalize_reorder_payload(self, payload: list[JSONObject]) -> list[JSONObject]:
253
+ first_item = payload[0]
254
+ if "ordinalType" in first_item and "viewKeyList" in first_item:
255
+ return payload
256
+ if "ordinalType" in first_item and "viewList" in first_item:
257
+ normalized_groups: list[JSONObject] = []
258
+ for group in payload:
259
+ view_list = group.get("viewList")
260
+ view_keys = [
261
+ str(view.get("viewKey"))
262
+ for view in view_list
263
+ if isinstance(view_list, list) and isinstance(view, dict) and view.get("viewKey")
264
+ ]
265
+ if group.get("ordinalType") and view_keys:
266
+ normalized_groups.append({"ordinalType": group["ordinalType"], "viewKeyList": view_keys})
267
+ if normalized_groups:
268
+ return normalized_groups
269
+ sorted_items = sorted(
270
+ payload,
271
+ key=lambda item: int(item.get("ordinal", payload.index(item))) if isinstance(item.get("ordinal"), int) else payload.index(item),
272
+ )
273
+ view_keys = [
274
+ str(item.get("viewgraphKey") or item.get("viewKey"))
275
+ for item in sorted_items
276
+ if item.get("viewgraphKey") or item.get("viewKey")
277
+ ]
278
+ if not view_keys:
279
+ raise_tool_error(QingflowApiError.config_error("payload must include viewgraphKey/viewKey or ordinalType/viewKeyList"))
280
+ return [{"ordinalType": "FIXED_VIEW_LIST", "viewKeyList": view_keys}]
@@ -0,0 +1,312 @@
1
+ from __future__ import annotations
2
+
3
+ from mcp.server.fastmcp import FastMCP
4
+
5
+ from ..backend_client import BackendRequestContext
6
+ from ..config import DEFAULT_PROFILE
7
+ from ..errors import QingflowApiError, raise_tool_error
8
+ from ..json_types import JSONObject, JSONValue
9
+ from .base import ToolBase
10
+
11
+
12
+ class WorkflowTools(ToolBase):
13
+ def register(self, mcp: FastMCP) -> None:
14
+ @mcp.tool()
15
+ def workflow_list_nodes(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
16
+ return self.workflow_list_nodes(profile=profile, app_key=app_key)
17
+
18
+ @mcp.tool()
19
+ def workflow_get_node_detail(profile: str = DEFAULT_PROFILE, app_key: str = "", audit_node_id: int = 0) -> JSONObject:
20
+ return self.workflow_get_node_detail(profile=profile, app_key=app_key, audit_node_id=audit_node_id)
21
+
22
+ @mcp.tool()
23
+ def workflow_get_global_settings(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
24
+ return self.workflow_get_global_settings(profile=profile, app_key=app_key)
25
+
26
+ @mcp.tool()
27
+ def workflow_get_future_nodes(profile: str = DEFAULT_PROFILE, app_key: str = "", apply_id: int = 0) -> JSONObject:
28
+ return self.workflow_get_future_nodes(profile=profile, app_key=app_key, apply_id=apply_id)
29
+
30
+ @mcp.tool()
31
+ def workflow_get_future_nodes_app(
32
+ profile: str = DEFAULT_PROFILE,
33
+ app_key: str = "",
34
+ apply_id: int = 0,
35
+ role: int = 1,
36
+ audit_node_id: int | None = None,
37
+ ) -> JSONObject:
38
+ return self.workflow_get_future_nodes_app(
39
+ profile=profile,
40
+ app_key=app_key,
41
+ apply_id=apply_id,
42
+ role=role,
43
+ audit_node_id=audit_node_id,
44
+ )
45
+
46
+ @mcp.tool()
47
+ def workflow_get_qsource_active(profile: str = DEFAULT_PROFILE, app_key: str = "", qsource_id: int = 0) -> JSONObject:
48
+ return self.workflow_get_qsource_active(profile=profile, app_key=app_key, qsource_id=qsource_id)
49
+
50
+ @mcp.tool()
51
+ def workflow_get_qsource_passive(profile: str = DEFAULT_PROFILE, app_key: str = "", qsource_id: int = 0) -> JSONObject:
52
+ return self.workflow_get_qsource_passive(profile=profile, app_key=app_key, qsource_id=qsource_id)
53
+
54
+ @mcp.tool()
55
+ def workflow_get_editable_question_ids(profile: str = DEFAULT_PROFILE, app_key: str = "", audit_node_id: int = 0) -> JSONObject:
56
+ return self.workflow_get_editable_question_ids(profile=profile, app_key=app_key, audit_node_id=audit_node_id)
57
+
58
+ @mcp.tool()
59
+ def workflow_get_print_nodes(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
60
+ return self.workflow_get_print_nodes(profile=profile, app_key=app_key)
61
+
62
+ def workflow_list_nodes(self, *, profile: str, app_key: str) -> JSONObject:
63
+ self._require_app_key(app_key)
64
+ return self._request(profile, "GET", f"/app/{app_key}/auditNodes", app_key=app_key)
65
+
66
+ def workflow_get_node_detail(self, *, profile: str, app_key: str, audit_node_id: int) -> JSONObject:
67
+ self._require_app_key(app_key)
68
+ self._require_positive("audit_node_id", audit_node_id)
69
+ return self._request(profile, "GET", f"/app/{app_key}/auditNodes/{audit_node_id}", app_key=app_key, audit_node_id=audit_node_id)
70
+
71
+ def workflow_add_node(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
72
+ self._require_app_key(app_key)
73
+ body = self._require_dict(payload)
74
+ return self._request_with_post_fallbacks(
75
+ profile=profile,
76
+ app_key=app_key,
77
+ path=f"/app/{app_key}/auditNodes",
78
+ json_body=body,
79
+ alternate_paths=[f"/app/{app_key}/auditNode"],
80
+ )
81
+
82
+ def workflow_update_node(self, *, profile: str, app_key: str, audit_node_id: int, payload: JSONObject) -> JSONObject:
83
+ self._require_app_key(app_key)
84
+ self._require_positive("audit_node_id", audit_node_id)
85
+ body = self._require_dict(payload)
86
+ return self._request_with_post_fallbacks(
87
+ profile=profile,
88
+ app_key=app_key,
89
+ path=f"/app/{app_key}/auditNodes/{audit_node_id}",
90
+ json_body=body,
91
+ alternate_paths=[f"/app/{app_key}/auditNode/{audit_node_id}"],
92
+ risk_operation="update",
93
+ risk_target="workflow node configuration",
94
+ audit_node_id=audit_node_id,
95
+ )
96
+
97
+ def workflow_delete_node(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
98
+ self._require_app_key(app_key)
99
+ body = self._require_dict(payload)
100
+ return self._request(profile, "DELETE", f"/app/{app_key}/auditNode", app_key=app_key, json_body=body, risk_operation="delete", risk_target="workflow node configuration")
101
+
102
+ def workflow_copy_paste_node(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
103
+ self._require_app_key(app_key)
104
+ body = self._require_dict(payload)
105
+ return self._request(profile, "POST", f"/app/{app_key}/auditNode/copyAndPaste", app_key=app_key, json_body=body)
106
+
107
+ def workflow_cut_paste_node(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
108
+ self._require_app_key(app_key)
109
+ body = self._require_dict(payload)
110
+ return self._request(profile, "POST", f"/app/{app_key}/auditNode/cutAndPaste", app_key=app_key, json_body=body)
111
+
112
+ def workflow_create_sub_branch(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
113
+ self._require_app_key(app_key)
114
+ body = self._require_dict(payload)
115
+ return self._request(profile, "POST", f"/app/{app_key}/auditNode/subBranch", app_key=app_key, json_body=body)
116
+
117
+ def workflow_delete_sub_branch(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
118
+ self._require_app_key(app_key)
119
+ body = self._require_dict(payload)
120
+ return self._request(profile, "DELETE", f"/app/{app_key}/auditNode/subBranch", app_key=app_key, json_body=body, risk_operation="delete", risk_target="workflow branch configuration")
121
+
122
+ def workflow_get_global_settings(self, *, profile: str, app_key: str) -> JSONObject:
123
+ self._require_app_key(app_key)
124
+ return self._request(profile, "GET", f"/app/{app_key}/workflow/global/setting", app_key=app_key)
125
+
126
+ def workflow_update_global_settings(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
127
+ self._require_app_key(app_key)
128
+ body = self._require_dict(payload)
129
+ return self._request_with_post_fallbacks(
130
+ profile=profile,
131
+ app_key=app_key,
132
+ path=f"/app/{app_key}/workflow/global/setting",
133
+ json_body=body,
134
+ alternate_paths=[],
135
+ risk_operation="update",
136
+ risk_target="workflow global settings",
137
+ )
138
+
139
+ def workflow_publish(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
140
+ self._require_app_key(app_key)
141
+ body = self._require_dict(payload)
142
+ return self._request_with_post_fallbacks(
143
+ profile=profile,
144
+ app_key=app_key,
145
+ path=f"/app/{app_key}/publish",
146
+ json_body=body,
147
+ alternate_paths=[],
148
+ )
149
+
150
+ def workflow_get_future_nodes(self, *, profile: str, app_key: str, apply_id: int) -> JSONObject:
151
+ self._require_app_key(app_key)
152
+ self._require_positive("apply_id", apply_id)
153
+ return self._request(profile, "GET", f"/app/{app_key}/auditNode/futureList/{apply_id}", app_key=app_key, apply_id=apply_id)
154
+
155
+ def workflow_get_future_nodes_app(
156
+ self,
157
+ *,
158
+ profile: str,
159
+ app_key: str,
160
+ apply_id: int,
161
+ role: int,
162
+ audit_node_id: int | None,
163
+ ) -> JSONObject:
164
+ self._require_app_key(app_key)
165
+ self._require_positive("apply_id", apply_id)
166
+ params: JSONObject = {"role": role}
167
+ if audit_node_id is not None:
168
+ self._require_positive("audit_node_id", audit_node_id)
169
+ params["auditNodeId"] = audit_node_id
170
+ return self._request(
171
+ profile,
172
+ "GET",
173
+ f"/app/{app_key}/auditNode/appFutureListV2/{apply_id}",
174
+ app_key=app_key,
175
+ apply_id=apply_id,
176
+ params=params,
177
+ )
178
+
179
+ def workflow_webhook_test(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
180
+ self._require_app_key(app_key)
181
+ body = self._require_dict(payload)
182
+ return self._request(profile, "POST", f"/app/{app_key}/auditNode/webhookTest", app_key=app_key, json_body=body)
183
+
184
+ def workflow_qsource_query(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
185
+ self._require_app_key(app_key)
186
+ body = self._require_dict(payload)
187
+ return self._request(profile, "POST", f"/app/{app_key}/auditNode/qSourceQuery", app_key=app_key, json_body=body)
188
+
189
+ def workflow_qsource_test(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
190
+ self._require_app_key(app_key)
191
+ body = self._require_dict(payload)
192
+ return self._request(profile, "POST", f"/app/{app_key}/auditNode/qSourceQueryTest", app_key=app_key, json_body=body)
193
+
194
+ def workflow_get_qsource_active(self, *, profile: str, app_key: str, qsource_id: int) -> JSONObject:
195
+ self._require_app_key(app_key)
196
+ self._require_positive("qsource_id", qsource_id)
197
+ return self._request(profile, "GET", f"/app/{app_key}/auditNode/active/qsource", app_key=app_key, params={"qSourceId": qsource_id})
198
+
199
+ def workflow_upsert_qsource_active(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
200
+ self._require_app_key(app_key)
201
+ body = self._require_dict(payload)
202
+ return self._request(profile, "POST", f"/app/{app_key}/auditNode/active/qsource", app_key=app_key, json_body=body)
203
+
204
+ def workflow_get_qsource_passive(self, *, profile: str, app_key: str, qsource_id: int) -> JSONObject:
205
+ self._require_app_key(app_key)
206
+ self._require_positive("qsource_id", qsource_id)
207
+ return self._request(profile, "GET", f"/app/{app_key}/auditNode/passive/qsource", app_key=app_key, params={"qSourceId": qsource_id})
208
+
209
+ def workflow_upsert_qsource_passive(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
210
+ self._require_app_key(app_key)
211
+ body = self._require_dict(payload)
212
+ return self._request(profile, "POST", f"/app/{app_key}/auditNode/passive/qsource", app_key=app_key, json_body=body)
213
+
214
+ def workflow_switch_qsource_status(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
215
+ self._require_app_key(app_key)
216
+ body = self._require_dict(payload)
217
+ return self._request(profile, "POST", f"/app/{app_key}/auditNode/source/status", app_key=app_key, json_body=body)
218
+
219
+ def workflow_delete_qsource(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
220
+ self._require_app_key(app_key)
221
+ body = self._require_dict(payload)
222
+ return self._request(profile, "DELETE", f"/app/{app_key}/auditNode/qsource", app_key=app_key, json_body=body, risk_operation="delete", risk_target="workflow qsource configuration")
223
+
224
+ def workflow_get_editable_question_ids(self, *, profile: str, app_key: str, audit_node_id: int) -> JSONObject:
225
+ self._require_app_key(app_key)
226
+ self._require_positive("audit_node_id", audit_node_id)
227
+ return self._request(
228
+ profile,
229
+ "GET",
230
+ f"/app/{app_key}/auditNode/{audit_node_id}/editableQueIds",
231
+ app_key=app_key,
232
+ audit_node_id=audit_node_id,
233
+ )
234
+
235
+ def workflow_get_print_nodes(self, *, profile: str, app_key: str) -> JSONObject:
236
+ self._require_app_key(app_key)
237
+ return self._request(profile, "GET", f"/app/{app_key}/auditNode/printNodes", app_key=app_key)
238
+
239
+ def _request(
240
+ self,
241
+ profile: str,
242
+ method: str,
243
+ path: str,
244
+ *,
245
+ app_key: str,
246
+ json_body: JSONValue = None,
247
+ params: JSONObject | None = None,
248
+ risk_operation: str | None = None,
249
+ risk_target: str | None = None,
250
+ **extra: JSONValue,
251
+ ) -> JSONObject:
252
+ def runner(session_profile, context):
253
+ result = self.backend.request(method, context, path, json_body=json_body, params=params)
254
+ response: JSONObject = {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "result": result}
255
+ response.update(extra)
256
+ if risk_operation and risk_target:
257
+ return self._attach_human_review_notice(response, operation=risk_operation, target=risk_target)
258
+ return response
259
+
260
+ return self._run(profile, runner)
261
+
262
+ def _request_with_post_fallbacks(
263
+ self,
264
+ *,
265
+ profile: str,
266
+ app_key: str,
267
+ path: str,
268
+ json_body: JSONObject,
269
+ alternate_paths: list[str],
270
+ risk_operation: str | None = None,
271
+ risk_target: str | None = None,
272
+ **extra: JSONValue,
273
+ ) -> JSONObject:
274
+ def runner(session_profile, context):
275
+ attempted_contexts = [context]
276
+ if context.qf_version is not None:
277
+ attempted_contexts.append(
278
+ BackendRequestContext(
279
+ base_url=context.base_url,
280
+ token=context.token,
281
+ ws_id=context.ws_id,
282
+ qf_version=None,
283
+ qf_version_source="workflow_retry_without_qf_version",
284
+ )
285
+ )
286
+ paths = [path, *alternate_paths]
287
+ last_error: QingflowApiError | None = None
288
+ for call_context in attempted_contexts:
289
+ for candidate_path in paths:
290
+ try:
291
+ result = self.backend.request("POST", call_context, candidate_path, json_body=json_body)
292
+ response: JSONObject = {"profile": profile, "ws_id": session_profile.selected_ws_id, "result": result}
293
+ response.update(extra)
294
+ if risk_operation and risk_target:
295
+ return self._attach_human_review_notice(response, operation=risk_operation, target=risk_target)
296
+ return response
297
+ except QingflowApiError as error:
298
+ last_error = error
299
+ if error.http_status != 404:
300
+ raise
301
+ assert last_error is not None
302
+ raise last_error
303
+
304
+ return self._run(profile, runner)
305
+
306
+ def _require_app_key(self, app_key: str) -> None:
307
+ if not app_key:
308
+ raise_tool_error(QingflowApiError.config_error("app_key is required"))
309
+
310
+ def _require_positive(self, field_name: str, value: int) -> None:
311
+ if value <= 0:
312
+ raise_tool_error(QingflowApiError.config_error(f"{field_name} must be positive"))