@josephyan/qingflow-cli 0.2.0-beta.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/docs/local-agent-install.md +235 -0
- package/entry_point.py +13 -0
- package/npm/bin/qingflow.mjs +5 -0
- package/npm/lib/runtime.mjs +204 -0
- package/npm/scripts/postinstall.mjs +16 -0
- package/package.json +34 -0
- package/pyproject.toml +67 -0
- package/qingflow +15 -0
- package/src/qingflow_mcp/__init__.py +5 -0
- package/src/qingflow_mcp/__main__.py +5 -0
- package/src/qingflow_mcp/backend_client.py +547 -0
- package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
- package/src/qingflow_mcp/builder_facade/models.py +985 -0
- package/src/qingflow_mcp/builder_facade/service.py +8243 -0
- package/src/qingflow_mcp/cli/__init__.py +1 -0
- package/src/qingflow_mcp/cli/commands/__init__.py +15 -0
- package/src/qingflow_mcp/cli/commands/app.py +40 -0
- package/src/qingflow_mcp/cli/commands/auth.py +78 -0
- package/src/qingflow_mcp/cli/commands/builder.py +184 -0
- package/src/qingflow_mcp/cli/commands/common.py +47 -0
- package/src/qingflow_mcp/cli/commands/imports.py +86 -0
- package/src/qingflow_mcp/cli/commands/record.py +202 -0
- package/src/qingflow_mcp/cli/commands/task.py +87 -0
- package/src/qingflow_mcp/cli/commands/workspace.py +33 -0
- package/src/qingflow_mcp/cli/context.py +48 -0
- package/src/qingflow_mcp/cli/formatters.py +269 -0
- package/src/qingflow_mcp/cli/json_io.py +50 -0
- package/src/qingflow_mcp/cli/main.py +147 -0
- package/src/qingflow_mcp/config.py +221 -0
- package/src/qingflow_mcp/errors.py +66 -0
- package/src/qingflow_mcp/import_store.py +121 -0
- package/src/qingflow_mcp/json_types.py +18 -0
- package/src/qingflow_mcp/list_type_labels.py +76 -0
- package/src/qingflow_mcp/server.py +211 -0
- package/src/qingflow_mcp/server_app_builder.py +387 -0
- package/src/qingflow_mcp/server_app_user.py +317 -0
- package/src/qingflow_mcp/session_store.py +289 -0
- package/src/qingflow_mcp/solution/__init__.py +6 -0
- package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
- package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
- package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +466 -0
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +113 -0
- package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
- package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
- package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
- package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
- package/src/qingflow_mcp/solution/design_session.py +222 -0
- package/src/qingflow_mcp/solution/design_store.py +100 -0
- package/src/qingflow_mcp/solution/executor.py +2339 -0
- package/src/qingflow_mcp/solution/normalizer.py +23 -0
- package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
- package/src/qingflow_mcp/solution/run_store.py +244 -0
- package/src/qingflow_mcp/solution/spec_models.py +853 -0
- package/src/qingflow_mcp/tools/__init__.py +1 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +2063 -0
- package/src/qingflow_mcp/tools/app_tools.py +850 -0
- package/src/qingflow_mcp/tools/approval_tools.py +833 -0
- package/src/qingflow_mcp/tools/auth_tools.py +697 -0
- package/src/qingflow_mcp/tools/base.py +81 -0
- package/src/qingflow_mcp/tools/code_block_tools.py +679 -0
- package/src/qingflow_mcp/tools/directory_tools.py +648 -0
- package/src/qingflow_mcp/tools/feedback_tools.py +230 -0
- package/src/qingflow_mcp/tools/file_tools.py +385 -0
- package/src/qingflow_mcp/tools/import_tools.py +1971 -0
- package/src/qingflow_mcp/tools/navigation_tools.py +177 -0
- package/src/qingflow_mcp/tools/package_tools.py +240 -0
- package/src/qingflow_mcp/tools/portal_tools.py +131 -0
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +269 -0
- package/src/qingflow_mcp/tools/record_tools.py +12739 -0
- package/src/qingflow_mcp/tools/role_tools.py +94 -0
- package/src/qingflow_mcp/tools/solution_tools.py +3887 -0
- package/src/qingflow_mcp/tools/task_context_tools.py +1423 -0
- package/src/qingflow_mcp/tools/task_tools.py +843 -0
- package/src/qingflow_mcp/tools/view_tools.py +280 -0
- package/src/qingflow_mcp/tools/workflow_tools.py +312 -0
- package/src/qingflow_mcp/tools/workspace_tools.py +219 -0
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from mcp.server.fastmcp import FastMCP
|
|
6
|
+
|
|
7
|
+
from ..config import DEFAULT_PROFILE
|
|
8
|
+
from ..errors import QingflowApiError, raise_tool_error
|
|
9
|
+
from ..json_types import JSONObject
|
|
10
|
+
from ..list_type_labels import get_record_list_type_label
|
|
11
|
+
from .base import ToolBase
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ApprovalTools(ToolBase):
|
|
15
|
+
def __init__(self, sessions, backend) -> None: # type: ignore[no-untyped-def]
|
|
16
|
+
super().__init__(sessions, backend)
|
|
17
|
+
self._form_id_cache: dict[str, int] = {}
|
|
18
|
+
|
|
19
|
+
def register(self, mcp: FastMCP) -> None:
|
|
20
|
+
@mcp.tool()
|
|
21
|
+
def record_comment_write(
|
|
22
|
+
profile: str = DEFAULT_PROFILE,
|
|
23
|
+
app_key: str = "",
|
|
24
|
+
record_id: int = 0,
|
|
25
|
+
payload: dict[str, Any] | None = None,
|
|
26
|
+
) -> dict[str, Any]:
|
|
27
|
+
return self.record_comment_write(profile=profile, app_key=app_key, record_id=record_id, payload=payload or {})
|
|
28
|
+
|
|
29
|
+
@mcp.tool()
|
|
30
|
+
def record_comment_list(
|
|
31
|
+
profile: str = DEFAULT_PROFILE,
|
|
32
|
+
app_key: str = "",
|
|
33
|
+
record_id: int = 0,
|
|
34
|
+
page_size: int = 20,
|
|
35
|
+
list_type: int | None = None,
|
|
36
|
+
page_num: int | None = 1,
|
|
37
|
+
) -> dict[str, Any]:
|
|
38
|
+
return self.record_comment_list(
|
|
39
|
+
profile=profile,
|
|
40
|
+
app_key=app_key,
|
|
41
|
+
apply_id=record_id,
|
|
42
|
+
page_size=page_size,
|
|
43
|
+
list_type=list_type,
|
|
44
|
+
page_num=page_num,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
@mcp.tool()
|
|
48
|
+
def record_comment_mentions(
|
|
49
|
+
profile: str = DEFAULT_PROFILE,
|
|
50
|
+
app_key: str = "",
|
|
51
|
+
record_id: int = 0,
|
|
52
|
+
page_size: int = 20,
|
|
53
|
+
page_num: int = 1,
|
|
54
|
+
list_type: int | None = None,
|
|
55
|
+
keyword: str | None = None,
|
|
56
|
+
) -> dict[str, Any]:
|
|
57
|
+
return self.record_comment_mentions(
|
|
58
|
+
profile=profile,
|
|
59
|
+
app_key=app_key,
|
|
60
|
+
record_id=record_id,
|
|
61
|
+
page_size=page_size,
|
|
62
|
+
page_num=page_num,
|
|
63
|
+
list_type=list_type,
|
|
64
|
+
keyword=keyword,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@mcp.tool()
|
|
68
|
+
def record_comment_mark_read(profile: str = DEFAULT_PROFILE, app_key: str = "", record_id: int = 0) -> dict[str, Any]:
|
|
69
|
+
return self.record_comment_mark_read(profile=profile, app_key=app_key, apply_id=record_id)
|
|
70
|
+
|
|
71
|
+
@mcp.tool(description=self._high_risk_tool_description(operation="approve", target="workflow task"))
|
|
72
|
+
def task_approve(profile: str = DEFAULT_PROFILE, app_key: str = "", record_id: int = 0, payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
73
|
+
return self.task_approve(profile=profile, app_key=app_key, record_id=record_id, payload=payload or {})
|
|
74
|
+
|
|
75
|
+
@mcp.tool(description=self._high_risk_tool_description(operation="reject", target="workflow task"))
|
|
76
|
+
def task_reject(profile: str = DEFAULT_PROFILE, app_key: str = "", record_id: int = 0, payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
77
|
+
return self.task_reject(profile=profile, app_key=app_key, record_id=record_id, payload=payload or {})
|
|
78
|
+
|
|
79
|
+
@mcp.tool()
|
|
80
|
+
def task_rollback_candidates(
|
|
81
|
+
profile: str = DEFAULT_PROFILE,
|
|
82
|
+
app_key: str = "",
|
|
83
|
+
record_id: int = 0,
|
|
84
|
+
workflow_node_id: int = 0,
|
|
85
|
+
) -> dict[str, Any]:
|
|
86
|
+
return self.task_rollback_candidates(profile=profile, app_key=app_key, record_id=record_id, workflow_node_id=workflow_node_id)
|
|
87
|
+
|
|
88
|
+
@mcp.tool()
|
|
89
|
+
def task_rollback(profile: str = DEFAULT_PROFILE, app_key: str = "", record_id: int = 0, payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
90
|
+
return self.task_rollback(profile=profile, app_key=app_key, record_id=record_id, payload=payload or {})
|
|
91
|
+
|
|
92
|
+
@mcp.tool()
|
|
93
|
+
def task_transfer(profile: str = DEFAULT_PROFILE, app_key: str = "", record_id: int = 0, payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
94
|
+
return self.task_transfer(profile=profile, app_key=app_key, record_id=record_id, payload=payload or {})
|
|
95
|
+
|
|
96
|
+
@mcp.tool()
|
|
97
|
+
def task_transfer_candidates(
|
|
98
|
+
profile: str = DEFAULT_PROFILE,
|
|
99
|
+
app_key: str = "",
|
|
100
|
+
record_id: int = 0,
|
|
101
|
+
page_size: int = 20,
|
|
102
|
+
page_num: int = 1,
|
|
103
|
+
workflow_node_id: int = 0,
|
|
104
|
+
keyword: str | None = None,
|
|
105
|
+
) -> dict[str, Any]:
|
|
106
|
+
return self.task_transfer_candidates(
|
|
107
|
+
profile=profile,
|
|
108
|
+
app_key=app_key,
|
|
109
|
+
record_id=record_id,
|
|
110
|
+
page_size=page_size,
|
|
111
|
+
page_num=page_num,
|
|
112
|
+
workflow_node_id=workflow_node_id,
|
|
113
|
+
keyword=keyword,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def record_comment_write(self, *, profile: str, app_key: str, record_id: int, payload: dict[str, Any]) -> dict[str, Any]:
|
|
117
|
+
raw = self.record_comment_add(profile=profile, app_key=app_key, apply_id=record_id, payload=payload)
|
|
118
|
+
return self._public_action_response(
|
|
119
|
+
raw,
|
|
120
|
+
action="record_comment_write",
|
|
121
|
+
resource={"app_key": app_key, "record_id": record_id},
|
|
122
|
+
selection={},
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def record_comment_mentions(
|
|
126
|
+
self,
|
|
127
|
+
*,
|
|
128
|
+
profile: str,
|
|
129
|
+
app_key: str,
|
|
130
|
+
record_id: int,
|
|
131
|
+
page_size: int = 20,
|
|
132
|
+
page_num: int = 1,
|
|
133
|
+
list_type: int | None = None,
|
|
134
|
+
keyword: str | None = None,
|
|
135
|
+
) -> dict[str, Any]:
|
|
136
|
+
raw = self.record_comment_mention_candidates(
|
|
137
|
+
profile=profile,
|
|
138
|
+
app_key=app_key,
|
|
139
|
+
apply_id=record_id,
|
|
140
|
+
page_size=page_size,
|
|
141
|
+
page_num=page_num,
|
|
142
|
+
list_type=list_type,
|
|
143
|
+
keyword=keyword,
|
|
144
|
+
)
|
|
145
|
+
items = _approval_page_items(raw.get("page"))
|
|
146
|
+
return self._public_page_response(
|
|
147
|
+
raw,
|
|
148
|
+
items=items,
|
|
149
|
+
pagination={
|
|
150
|
+
"page": page_num,
|
|
151
|
+
"page_size": page_size,
|
|
152
|
+
"returned_items": len(items),
|
|
153
|
+
"page_amount": _approval_page_amount(raw.get("page")),
|
|
154
|
+
"reported_total": _approval_page_total(raw.get("page")),
|
|
155
|
+
},
|
|
156
|
+
selection={"app_key": app_key, "record_id": record_id, "list_type": list_type, "keyword": keyword},
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def task_approve(self, *, profile: str, app_key: str, record_id: int, payload: dict[str, Any]) -> dict[str, Any]:
|
|
160
|
+
raw = self.record_approve(profile=profile, app_key=app_key, apply_id=record_id, payload=payload)
|
|
161
|
+
return self._public_action_response(
|
|
162
|
+
raw,
|
|
163
|
+
action="task_approve",
|
|
164
|
+
resource={"app_key": app_key, "record_id": record_id},
|
|
165
|
+
selection={},
|
|
166
|
+
human_review=True,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def task_reject(self, *, profile: str, app_key: str, record_id: int, payload: dict[str, Any]) -> dict[str, Any]:
|
|
170
|
+
raw = self.record_reject(profile=profile, app_key=app_key, apply_id=record_id, payload=payload)
|
|
171
|
+
return self._public_action_response(
|
|
172
|
+
raw,
|
|
173
|
+
action="task_reject",
|
|
174
|
+
resource={"app_key": app_key, "record_id": record_id},
|
|
175
|
+
selection={},
|
|
176
|
+
human_review=True,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def task_rollback_candidates(self, *, profile: str, app_key: str, record_id: int, workflow_node_id: int) -> dict[str, Any]:
|
|
180
|
+
raw = self.record_rollback_candidates(profile=profile, app_key=app_key, apply_id=record_id, audit_node_id=workflow_node_id)
|
|
181
|
+
items = _approval_page_items(raw.get("result"))
|
|
182
|
+
return self._public_page_response(
|
|
183
|
+
raw,
|
|
184
|
+
items=items,
|
|
185
|
+
pagination={"returned_items": len(items)},
|
|
186
|
+
selection={"app_key": app_key, "record_id": record_id, "workflow_node_id": workflow_node_id},
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def task_rollback(self, *, profile: str, app_key: str, record_id: int, payload: dict[str, Any]) -> dict[str, Any]:
|
|
190
|
+
raw = self.record_rollback(profile=profile, app_key=app_key, apply_id=record_id, payload=payload)
|
|
191
|
+
return self._public_action_response(
|
|
192
|
+
raw,
|
|
193
|
+
action="task_rollback",
|
|
194
|
+
resource={"app_key": app_key, "record_id": record_id},
|
|
195
|
+
selection={},
|
|
196
|
+
human_review=True,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
def task_transfer(self, *, profile: str, app_key: str, record_id: int, payload: dict[str, Any]) -> dict[str, Any]:
|
|
200
|
+
raw = self.record_transfer(profile=profile, app_key=app_key, apply_id=record_id, payload=payload)
|
|
201
|
+
return self._public_action_response(
|
|
202
|
+
raw,
|
|
203
|
+
action="task_transfer",
|
|
204
|
+
resource={"app_key": app_key, "record_id": record_id},
|
|
205
|
+
selection={},
|
|
206
|
+
human_review=True,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
def task_transfer_candidates(
|
|
210
|
+
self,
|
|
211
|
+
*,
|
|
212
|
+
profile: str,
|
|
213
|
+
app_key: str,
|
|
214
|
+
record_id: int,
|
|
215
|
+
page_size: int = 20,
|
|
216
|
+
page_num: int = 1,
|
|
217
|
+
workflow_node_id: int = 0,
|
|
218
|
+
keyword: str | None = None,
|
|
219
|
+
) -> dict[str, Any]:
|
|
220
|
+
raw = self.record_transfer_candidates(
|
|
221
|
+
profile=profile,
|
|
222
|
+
app_key=app_key,
|
|
223
|
+
apply_id=record_id,
|
|
224
|
+
page_size=page_size,
|
|
225
|
+
page_num=page_num,
|
|
226
|
+
audit_node_id=workflow_node_id,
|
|
227
|
+
keyword=keyword,
|
|
228
|
+
)
|
|
229
|
+
items = _approval_page_items(raw.get("page"))
|
|
230
|
+
return self._public_page_response(
|
|
231
|
+
raw,
|
|
232
|
+
items=items,
|
|
233
|
+
pagination={
|
|
234
|
+
"page": page_num,
|
|
235
|
+
"page_size": page_size,
|
|
236
|
+
"returned_items": len(items),
|
|
237
|
+
"page_amount": _approval_page_amount(raw.get("page")),
|
|
238
|
+
"reported_total": _approval_page_total(raw.get("page")),
|
|
239
|
+
},
|
|
240
|
+
selection={"app_key": app_key, "record_id": record_id, "workflow_node_id": workflow_node_id, "keyword": keyword},
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def record_comment_add(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
|
|
244
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
245
|
+
self._validate_comment_payload(payload)
|
|
246
|
+
|
|
247
|
+
def runner(session_profile, context):
|
|
248
|
+
result = self.backend.request("POST", context, f"/app/{app_key}/apply/{apply_id}/comment", json_body=payload)
|
|
249
|
+
return {
|
|
250
|
+
"profile": profile,
|
|
251
|
+
"ws_id": session_profile.selected_ws_id,
|
|
252
|
+
"app_key": app_key,
|
|
253
|
+
"apply_id": apply_id,
|
|
254
|
+
"result": result,
|
|
255
|
+
"request_route": self._request_route_payload(context),
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return self._run(profile, runner)
|
|
259
|
+
|
|
260
|
+
def record_comment_list(self, *, profile: str, app_key: str, apply_id: int, page_size: int = 20, list_type: int | None = None, page_num: int | None = 1) -> dict[str, Any]:
|
|
261
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
262
|
+
|
|
263
|
+
def runner(session_profile, context):
|
|
264
|
+
params: dict[str, Any] = {"pageSize": page_size}
|
|
265
|
+
if list_type is not None:
|
|
266
|
+
params["listType"] = list_type
|
|
267
|
+
if page_num is not None:
|
|
268
|
+
params["pageNum"] = page_num
|
|
269
|
+
result = self.backend.request("GET", context, f"/app/{app_key}/apply/{apply_id}/comment", params=params)
|
|
270
|
+
return {
|
|
271
|
+
"profile": profile,
|
|
272
|
+
"ws_id": session_profile.selected_ws_id,
|
|
273
|
+
"app_key": app_key,
|
|
274
|
+
"apply_id": apply_id,
|
|
275
|
+
"list_type": list_type,
|
|
276
|
+
"list_type_label": get_record_list_type_label(list_type),
|
|
277
|
+
"page": result,
|
|
278
|
+
"request_route": self._request_route_payload(context),
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
raw = self._run(profile, runner)
|
|
282
|
+
items = _approval_page_items(raw.get("page"))
|
|
283
|
+
return self._public_page_response(
|
|
284
|
+
raw,
|
|
285
|
+
items=items,
|
|
286
|
+
pagination={
|
|
287
|
+
"page": page_num,
|
|
288
|
+
"page_size": page_size,
|
|
289
|
+
"returned_items": len(items),
|
|
290
|
+
"page_amount": _approval_page_amount(raw.get("page")),
|
|
291
|
+
"reported_total": _approval_page_total(raw.get("page")),
|
|
292
|
+
},
|
|
293
|
+
selection={"app_key": app_key, "record_id": apply_id, "list_type": list_type},
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
def record_comment_mention_candidates(
|
|
297
|
+
self,
|
|
298
|
+
*,
|
|
299
|
+
profile: str,
|
|
300
|
+
app_key: str,
|
|
301
|
+
apply_id: int,
|
|
302
|
+
page_size: int = 20,
|
|
303
|
+
page_num: int = 1,
|
|
304
|
+
list_type: int | None = None,
|
|
305
|
+
keyword: str | None = None,
|
|
306
|
+
) -> dict[str, Any]:
|
|
307
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
308
|
+
|
|
309
|
+
def runner(session_profile, context):
|
|
310
|
+
params: dict[str, Any] = {"pageSize": page_size, "pageNum": page_num}
|
|
311
|
+
if list_type is not None:
|
|
312
|
+
params["listType"] = list_type
|
|
313
|
+
if keyword:
|
|
314
|
+
params["keyword"] = keyword
|
|
315
|
+
result = self.backend.request("GET", context, f"/app/{app_key}/apply/{apply_id}/comment/member", params=params)
|
|
316
|
+
return {
|
|
317
|
+
"profile": profile,
|
|
318
|
+
"ws_id": session_profile.selected_ws_id,
|
|
319
|
+
"app_key": app_key,
|
|
320
|
+
"apply_id": apply_id,
|
|
321
|
+
"list_type": list_type,
|
|
322
|
+
"list_type_label": get_record_list_type_label(list_type),
|
|
323
|
+
"page": result,
|
|
324
|
+
"request_route": self._request_route_payload(context),
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return self._run(profile, runner)
|
|
328
|
+
|
|
329
|
+
def record_comment_mark_read(self, *, profile: str, app_key: str, apply_id: int) -> dict[str, Any]:
|
|
330
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
331
|
+
|
|
332
|
+
def runner(session_profile, context):
|
|
333
|
+
result = self.backend.request("POST", context, f"/app/{app_key}/apply/{apply_id}/comment/read")
|
|
334
|
+
return {
|
|
335
|
+
"profile": profile,
|
|
336
|
+
"ws_id": session_profile.selected_ws_id,
|
|
337
|
+
"app_key": app_key,
|
|
338
|
+
"apply_id": apply_id,
|
|
339
|
+
"result": result,
|
|
340
|
+
"request_route": self._request_route_payload(context),
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
raw = self._run(profile, runner)
|
|
344
|
+
return self._public_action_response(
|
|
345
|
+
raw,
|
|
346
|
+
action="record_comment_mark_read",
|
|
347
|
+
resource={"app_key": app_key, "record_id": apply_id},
|
|
348
|
+
selection={},
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
def record_comment_stats(self, *, profile: str, app_key: str, apply_id: int) -> dict[str, Any]:
|
|
352
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
353
|
+
|
|
354
|
+
def runner(session_profile, context):
|
|
355
|
+
result = self.backend.request("GET", context, f"/app/{app_key}/apply/{apply_id}/comment/statistic")
|
|
356
|
+
return {
|
|
357
|
+
"profile": profile,
|
|
358
|
+
"ws_id": session_profile.selected_ws_id,
|
|
359
|
+
"app_key": app_key,
|
|
360
|
+
"apply_id": apply_id,
|
|
361
|
+
"result": result,
|
|
362
|
+
"request_route": self._request_route_payload(context),
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return self._run(profile, runner)
|
|
366
|
+
|
|
367
|
+
def record_approve(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
|
|
368
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
369
|
+
body = self._require_dict(payload)
|
|
370
|
+
|
|
371
|
+
def runner(session_profile, context):
|
|
372
|
+
approval_body = self._normalize_approval_payload(profile, context, app_key, apply_id, body)
|
|
373
|
+
result = self.backend.request("POST", context, "/workflow/engine/approval/approve", json_body=approval_body)
|
|
374
|
+
return {
|
|
375
|
+
"profile": profile,
|
|
376
|
+
"ws_id": session_profile.selected_ws_id,
|
|
377
|
+
"app_key": app_key,
|
|
378
|
+
"apply_id": apply_id,
|
|
379
|
+
"form_id": approval_body["formId"],
|
|
380
|
+
"node_id": approval_body["nodeId"],
|
|
381
|
+
"result": result,
|
|
382
|
+
"request_route": self._request_route_payload(context),
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return self._run(profile, runner)
|
|
386
|
+
|
|
387
|
+
def record_reject(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
|
|
388
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
389
|
+
body = self._require_dict(payload)
|
|
390
|
+
|
|
391
|
+
def runner(session_profile, context):
|
|
392
|
+
approval_body = self._normalize_approval_payload(profile, context, app_key, apply_id, body)
|
|
393
|
+
result = self.backend.request("POST", context, "/workflow/engine/approval/reject", json_body=approval_body)
|
|
394
|
+
return {
|
|
395
|
+
"profile": profile,
|
|
396
|
+
"ws_id": session_profile.selected_ws_id,
|
|
397
|
+
"app_key": app_key,
|
|
398
|
+
"apply_id": apply_id,
|
|
399
|
+
"form_id": approval_body["formId"],
|
|
400
|
+
"node_id": approval_body["nodeId"],
|
|
401
|
+
"result": result,
|
|
402
|
+
"request_route": self._request_route_payload(context),
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return self._run(profile, runner)
|
|
406
|
+
|
|
407
|
+
def record_rollback_candidates(self, *, profile: str, app_key: str, apply_id: int, audit_node_id: int) -> dict[str, Any]:
|
|
408
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
409
|
+
if audit_node_id <= 0:
|
|
410
|
+
raise_tool_error(QingflowApiError.config_error("audit_node_id must be positive"))
|
|
411
|
+
|
|
412
|
+
def runner(session_profile, context):
|
|
413
|
+
result = self.backend.request(
|
|
414
|
+
"GET",
|
|
415
|
+
context,
|
|
416
|
+
f"/app/{app_key}/apply/{apply_id}/revertNode",
|
|
417
|
+
params={"auditNodeId": audit_node_id},
|
|
418
|
+
)
|
|
419
|
+
return {
|
|
420
|
+
"profile": profile,
|
|
421
|
+
"ws_id": session_profile.selected_ws_id,
|
|
422
|
+
"app_key": app_key,
|
|
423
|
+
"apply_id": apply_id,
|
|
424
|
+
"result": result,
|
|
425
|
+
"request_route": self._request_route_payload(context),
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return self._run(profile, runner)
|
|
429
|
+
|
|
430
|
+
def record_rollback(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
|
|
431
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
432
|
+
body = self._require_dict(payload)
|
|
433
|
+
self._validate_audit_payload(body)
|
|
434
|
+
|
|
435
|
+
def runner(session_profile, context):
|
|
436
|
+
result = self.backend.request("POST", context, f"/app/{app_key}/apply/{apply_id}/rollback", json_body=body)
|
|
437
|
+
return {
|
|
438
|
+
"profile": profile,
|
|
439
|
+
"ws_id": session_profile.selected_ws_id,
|
|
440
|
+
"app_key": app_key,
|
|
441
|
+
"apply_id": apply_id,
|
|
442
|
+
"result": result,
|
|
443
|
+
"request_route": self._request_route_payload(context),
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return self._run(profile, runner)
|
|
447
|
+
|
|
448
|
+
def record_transfer(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
|
|
449
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
450
|
+
body = self._require_dict(payload)
|
|
451
|
+
self._validate_audit_payload(body, require_uid=True)
|
|
452
|
+
|
|
453
|
+
def runner(session_profile, context):
|
|
454
|
+
result = self.backend.request("POST", context, f"/app/{app_key}/apply/{apply_id}/transfer", json_body=body)
|
|
455
|
+
return {
|
|
456
|
+
"profile": profile,
|
|
457
|
+
"ws_id": session_profile.selected_ws_id,
|
|
458
|
+
"app_key": app_key,
|
|
459
|
+
"apply_id": apply_id,
|
|
460
|
+
"result": result,
|
|
461
|
+
"request_route": self._request_route_payload(context),
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return self._run(profile, runner)
|
|
465
|
+
|
|
466
|
+
def record_transfer_candidates(
|
|
467
|
+
self,
|
|
468
|
+
*,
|
|
469
|
+
profile: str,
|
|
470
|
+
app_key: str,
|
|
471
|
+
apply_id: int,
|
|
472
|
+
page_size: int = 20,
|
|
473
|
+
page_num: int = 1,
|
|
474
|
+
audit_node_id: int = 0,
|
|
475
|
+
keyword: str | None = None,
|
|
476
|
+
) -> dict[str, Any]:
|
|
477
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
478
|
+
if audit_node_id <= 0:
|
|
479
|
+
raise_tool_error(QingflowApiError.config_error("audit_node_id must be positive"))
|
|
480
|
+
|
|
481
|
+
def runner(session_profile, context):
|
|
482
|
+
params: dict[str, Any] = {"pageSize": page_size, "pageNum": page_num, "auditNodeId": audit_node_id}
|
|
483
|
+
if keyword:
|
|
484
|
+
params["keyword"] = keyword
|
|
485
|
+
result = self.backend.request("GET", context, f"/app/{app_key}/apply/{apply_id}/transfer/member", params=params)
|
|
486
|
+
return {
|
|
487
|
+
"profile": profile,
|
|
488
|
+
"ws_id": session_profile.selected_ws_id,
|
|
489
|
+
"app_key": app_key,
|
|
490
|
+
"apply_id": apply_id,
|
|
491
|
+
"page": result,
|
|
492
|
+
"request_route": self._request_route_payload(context),
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return self._run(profile, runner)
|
|
496
|
+
|
|
497
|
+
def record_reassign_get(self, *, profile: str, app_key: str, apply_id: int) -> dict[str, Any]:
|
|
498
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
499
|
+
|
|
500
|
+
def runner(session_profile, context):
|
|
501
|
+
result = self.backend.request("GET", context, f"/app/{app_key}/apply/{apply_id}/reassign")
|
|
502
|
+
return {
|
|
503
|
+
"profile": profile,
|
|
504
|
+
"ws_id": session_profile.selected_ws_id,
|
|
505
|
+
"app_key": app_key,
|
|
506
|
+
"apply_id": apply_id,
|
|
507
|
+
"result": result,
|
|
508
|
+
"request_route": self._request_route_payload(context),
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return self._run(profile, runner)
|
|
512
|
+
|
|
513
|
+
def record_reassign(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
|
|
514
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
515
|
+
body = self._require_dict(payload)
|
|
516
|
+
|
|
517
|
+
def runner(session_profile, context):
|
|
518
|
+
result = self.backend.request("POST", context, f"/app/{app_key}/apply/{apply_id}/reassign", json_body=body)
|
|
519
|
+
return {
|
|
520
|
+
"profile": profile,
|
|
521
|
+
"ws_id": session_profile.selected_ws_id,
|
|
522
|
+
"app_key": app_key,
|
|
523
|
+
"apply_id": apply_id,
|
|
524
|
+
"result": result,
|
|
525
|
+
"request_route": self._request_route_payload(context),
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return self._run(profile, runner)
|
|
529
|
+
|
|
530
|
+
def record_countersign_candidates(
|
|
531
|
+
self,
|
|
532
|
+
*,
|
|
533
|
+
profile: str,
|
|
534
|
+
app_key: str,
|
|
535
|
+
apply_id: int,
|
|
536
|
+
page_size: int = 20,
|
|
537
|
+
page_num: int = 1,
|
|
538
|
+
audit_node_id: int = 0,
|
|
539
|
+
search_key: str | None = None,
|
|
540
|
+
) -> dict[str, Any]:
|
|
541
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
542
|
+
if audit_node_id <= 0:
|
|
543
|
+
raise_tool_error(QingflowApiError.config_error("audit_node_id must be positive"))
|
|
544
|
+
|
|
545
|
+
def runner(session_profile, context):
|
|
546
|
+
params: dict[str, Any] = {"pageSize": page_size, "pageNum": page_num, "auditNodeId": audit_node_id}
|
|
547
|
+
if search_key:
|
|
548
|
+
params["searchKey"] = search_key
|
|
549
|
+
result = self.backend.request("GET", context, f"/app/{app_key}/apply/{apply_id}/countersign/member", params=params)
|
|
550
|
+
return {
|
|
551
|
+
"profile": profile,
|
|
552
|
+
"ws_id": session_profile.selected_ws_id,
|
|
553
|
+
"app_key": app_key,
|
|
554
|
+
"apply_id": apply_id,
|
|
555
|
+
"page": result,
|
|
556
|
+
"request_route": self._request_route_payload(context),
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return self._run(profile, runner)
|
|
560
|
+
|
|
561
|
+
def record_countersign(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
|
|
562
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
563
|
+
body = self._require_dict(payload)
|
|
564
|
+
self._validate_countersign_payload(body)
|
|
565
|
+
|
|
566
|
+
def runner(session_profile, context):
|
|
567
|
+
result = self.backend.request("POST", context, f"/app/{app_key}/apply/{apply_id}/countersign", json_body=body)
|
|
568
|
+
return {
|
|
569
|
+
"profile": profile,
|
|
570
|
+
"ws_id": session_profile.selected_ws_id,
|
|
571
|
+
"app_key": app_key,
|
|
572
|
+
"apply_id": apply_id,
|
|
573
|
+
"result": result,
|
|
574
|
+
"request_route": self._request_route_payload(context),
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return self._run(profile, runner)
|
|
578
|
+
|
|
579
|
+
def _request_route_payload(self, context) -> JSONObject: # type: ignore[no-untyped-def]
|
|
580
|
+
describe_route = getattr(self.backend, "describe_route", None)
|
|
581
|
+
if callable(describe_route):
|
|
582
|
+
payload = describe_route(context)
|
|
583
|
+
if isinstance(payload, dict):
|
|
584
|
+
return payload
|
|
585
|
+
return {
|
|
586
|
+
"base_url": getattr(context, "base_url", None),
|
|
587
|
+
"qf_version": getattr(context, "qf_version", None),
|
|
588
|
+
"qf_version_source": getattr(context, "qf_version_source", None) or ("context" if getattr(context, "qf_version", None) else "unknown"),
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
def _require_app_and_apply(self, app_key: str, apply_id: int) -> None:
|
|
592
|
+
if not app_key:
|
|
593
|
+
raise_tool_error(QingflowApiError.config_error("app_key is required"))
|
|
594
|
+
if apply_id <= 0:
|
|
595
|
+
raise_tool_error(QingflowApiError.config_error("apply_id must be positive"))
|
|
596
|
+
|
|
597
|
+
def _validate_comment_payload(self, payload: dict[str, Any]) -> None:
|
|
598
|
+
comment_detail = payload.get("commentDetail")
|
|
599
|
+
if not isinstance(comment_detail, dict):
|
|
600
|
+
raise_tool_error(QingflowApiError.config_error("payload.commentDetail must be an object"))
|
|
601
|
+
if not comment_detail.get("commentMsg"):
|
|
602
|
+
raise_tool_error(QingflowApiError.config_error("payload.commentDetail.commentMsg is required"))
|
|
603
|
+
|
|
604
|
+
def _normalize_approval_payload(self, profile: str, context, app_key: str, apply_id: int, payload: dict[str, Any]) -> JSONObject: # type: ignore[no-untyped-def]
|
|
605
|
+
body: JSONObject = dict(payload)
|
|
606
|
+
self._normalize_alias(body, "auditFeedback", "audit_feedback")
|
|
607
|
+
self._normalize_alias(body, "uploadFileSize", "upload_file_size")
|
|
608
|
+
self._normalize_alias(body, "handSignImageUrl", "hand_sign_image_url")
|
|
609
|
+
self._normalize_alias(body, "beingSaveSignature", "being_save_signature")
|
|
610
|
+
self._normalize_alias(body, "applyId", "apply_id")
|
|
611
|
+
self._normalize_alias(body, "formId", "form_id")
|
|
612
|
+
|
|
613
|
+
node_id = self._extract_node_id(body)
|
|
614
|
+
body["nodeId"] = self._resolve_actionable_node_id(context, app_key, apply_id, node_id)
|
|
615
|
+
body["applyId"] = self._match_or_fill_int(body, field_name="applyId", expected_value=apply_id)
|
|
616
|
+
body["formId"] = self._resolve_form_id(profile, context, app_key, explicit_form_id=body.get("formId"))
|
|
617
|
+
if body.get("answers") is None:
|
|
618
|
+
body["answers"] = self._fetch_current_todo_answers(context, app_key, apply_id, body["nodeId"])
|
|
619
|
+
|
|
620
|
+
self._validate_approval_payload(body)
|
|
621
|
+
return body
|
|
622
|
+
|
|
623
|
+
def _extract_node_id(self, payload: JSONObject) -> int:
|
|
624
|
+
node_id = payload.get("nodeId")
|
|
625
|
+
audit_node_id = payload.pop("auditNodeId", None)
|
|
626
|
+
if node_id is None:
|
|
627
|
+
node_id = audit_node_id
|
|
628
|
+
elif audit_node_id is not None and node_id != audit_node_id:
|
|
629
|
+
raise_tool_error(QingflowApiError.config_error("payload.nodeId and payload.auditNodeId must match when both are provided"))
|
|
630
|
+
if not isinstance(node_id, int) or node_id <= 0:
|
|
631
|
+
raise_tool_error(QingflowApiError.config_error("payload.nodeId or payload.auditNodeId must be a positive integer"))
|
|
632
|
+
return node_id
|
|
633
|
+
|
|
634
|
+
def _resolve_form_id(self, profile: str, context, app_key: str, *, explicit_form_id: Any | None) -> int: # type: ignore[no-untyped-def]
|
|
635
|
+
if explicit_form_id is not None:
|
|
636
|
+
if not isinstance(explicit_form_id, int) or explicit_form_id <= 0:
|
|
637
|
+
raise_tool_error(QingflowApiError.config_error("payload.formId must be a positive integer"))
|
|
638
|
+
form_id = self._get_form_id(profile, context, app_key)
|
|
639
|
+
if form_id != explicit_form_id:
|
|
640
|
+
raise_tool_error(
|
|
641
|
+
QingflowApiError.config_error(
|
|
642
|
+
f"payload.formId={explicit_form_id} does not match app_key '{app_key}' formId={form_id}"
|
|
643
|
+
)
|
|
644
|
+
)
|
|
645
|
+
return explicit_form_id
|
|
646
|
+
return self._get_form_id(profile, context, app_key)
|
|
647
|
+
|
|
648
|
+
def _get_form_id(self, profile: str, context, app_key: str) -> int: # type: ignore[no-untyped-def]
|
|
649
|
+
cache_key = f"{profile}:{app_key}"
|
|
650
|
+
cached = self._form_id_cache.get(cache_key)
|
|
651
|
+
if cached is not None:
|
|
652
|
+
return cached
|
|
653
|
+
result = self.backend.request("GET", context, f"/app/{app_key}/baseInfo")
|
|
654
|
+
form_id = result.get("formId") if isinstance(result, dict) else None
|
|
655
|
+
if not isinstance(form_id, int) or form_id <= 0:
|
|
656
|
+
raise_tool_error(QingflowApiError.config_error(f"cannot resolve formId for app_key '{app_key}'"))
|
|
657
|
+
self._form_id_cache[cache_key] = form_id
|
|
658
|
+
return form_id
|
|
659
|
+
|
|
660
|
+
def _match_or_fill_int(self, payload: JSONObject, *, field_name: str, expected_value: int) -> int:
|
|
661
|
+
current = payload.get(field_name)
|
|
662
|
+
if current is None:
|
|
663
|
+
return expected_value
|
|
664
|
+
if not isinstance(current, int) or current <= 0:
|
|
665
|
+
raise_tool_error(QingflowApiError.config_error(f"payload.{field_name} must be a positive integer"))
|
|
666
|
+
if current != expected_value:
|
|
667
|
+
raise_tool_error(QingflowApiError.config_error(f"payload.{field_name}={current} does not match apply_id={expected_value}"))
|
|
668
|
+
return current
|
|
669
|
+
|
|
670
|
+
def _normalize_alias(self, payload: JSONObject, canonical_key: str, alias_key: str) -> None:
|
|
671
|
+
alias_value = payload.pop(alias_key, None)
|
|
672
|
+
if canonical_key not in payload and alias_value is not None:
|
|
673
|
+
payload[canonical_key] = alias_value
|
|
674
|
+
elif alias_value is not None and payload.get(canonical_key) != alias_value:
|
|
675
|
+
raise_tool_error(QingflowApiError.config_error(f"payload.{canonical_key} and payload.{alias_key} must match when both are provided"))
|
|
676
|
+
|
|
677
|
+
def _resolve_actionable_node_id(self, context, app_key: str, apply_id: int, node_id: int) -> int: # type: ignore[no-untyped-def]
|
|
678
|
+
infos = self.backend.request(
|
|
679
|
+
"GET",
|
|
680
|
+
context,
|
|
681
|
+
f"/app/{app_key}/apply/{apply_id}/auditInfo",
|
|
682
|
+
params={"type": 1},
|
|
683
|
+
)
|
|
684
|
+
if not isinstance(infos, list) or not infos:
|
|
685
|
+
raise_tool_error(
|
|
686
|
+
QingflowApiError.config_error(
|
|
687
|
+
f"apply_id={apply_id} is not currently actionable for the logged-in user in todo list"
|
|
688
|
+
)
|
|
689
|
+
)
|
|
690
|
+
actionable_node_ids = {
|
|
691
|
+
candidate
|
|
692
|
+
for item in infos
|
|
693
|
+
if isinstance(item, dict)
|
|
694
|
+
for candidate in (item.get("auditNodeId"), item.get("nodeId"))
|
|
695
|
+
if isinstance(candidate, int) and candidate > 0
|
|
696
|
+
}
|
|
697
|
+
if node_id not in actionable_node_ids:
|
|
698
|
+
raise_tool_error(
|
|
699
|
+
QingflowApiError.config_error(
|
|
700
|
+
f"payload.nodeId={node_id} is not an actionable todo node for apply_id={apply_id}"
|
|
701
|
+
)
|
|
702
|
+
)
|
|
703
|
+
return node_id
|
|
704
|
+
|
|
705
|
+
def _fetch_current_todo_answers(self, context, app_key: str, apply_id: int, node_id: int) -> list[dict[str, Any]]: # type: ignore[no-untyped-def]
|
|
706
|
+
detail = self.backend.request(
|
|
707
|
+
"GET",
|
|
708
|
+
context,
|
|
709
|
+
f"/app/{app_key}/apply/{apply_id}",
|
|
710
|
+
params={"role": 3, "listType": 1, "auditNodeId": node_id},
|
|
711
|
+
)
|
|
712
|
+
answers = detail.get("answers") if isinstance(detail, dict) else None
|
|
713
|
+
if not isinstance(answers, list):
|
|
714
|
+
raise_tool_error(
|
|
715
|
+
QingflowApiError.config_error(
|
|
716
|
+
f"cannot resolve current answers for apply_id={apply_id} nodeId={node_id}"
|
|
717
|
+
)
|
|
718
|
+
)
|
|
719
|
+
normalized_answers: list[dict[str, Any]] = []
|
|
720
|
+
for item in answers:
|
|
721
|
+
if isinstance(item, dict):
|
|
722
|
+
normalized_answers.append(dict(item))
|
|
723
|
+
return normalized_answers
|
|
724
|
+
|
|
725
|
+
def _validate_approval_payload(self, payload: dict[str, Any]) -> None:
|
|
726
|
+
self._reject_unsupported_fields(payload)
|
|
727
|
+
if not isinstance(payload.get("formId"), int) or payload["formId"] <= 0:
|
|
728
|
+
raise_tool_error(QingflowApiError.config_error("payload.formId must be a positive integer"))
|
|
729
|
+
if not isinstance(payload.get("applyId"), int) or payload["applyId"] <= 0:
|
|
730
|
+
raise_tool_error(QingflowApiError.config_error("payload.applyId must be a positive integer"))
|
|
731
|
+
if not isinstance(payload.get("nodeId"), int) or payload["nodeId"] <= 0:
|
|
732
|
+
raise_tool_error(QingflowApiError.config_error("payload.nodeId must be a positive integer"))
|
|
733
|
+
answers = payload.get("answers")
|
|
734
|
+
if answers is not None and not isinstance(answers, list):
|
|
735
|
+
raise_tool_error(QingflowApiError.config_error("payload.answers must be an array when provided"))
|
|
736
|
+
|
|
737
|
+
def _validate_audit_payload(self, payload: dict[str, Any], *, require_uid: bool = False) -> None:
|
|
738
|
+
self._reject_unsupported_fields(payload)
|
|
739
|
+
if require_uid and not payload.get("uid"):
|
|
740
|
+
raise_tool_error(QingflowApiError.config_error("payload.uid is required"))
|
|
741
|
+
|
|
742
|
+
def _validate_countersign_payload(self, payload: dict[str, Any]) -> None:
|
|
743
|
+
self._reject_unsupported_fields(payload)
|
|
744
|
+
members = payload.get("countersignMembers")
|
|
745
|
+
if not isinstance(members, list) or not members:
|
|
746
|
+
raise_tool_error(QingflowApiError.config_error("payload.countersignMembers must be a non-empty array"))
|
|
747
|
+
|
|
748
|
+
def _reject_unsupported_fields(self, payload: dict[str, Any]) -> None:
|
|
749
|
+
if payload.get("handSignImageUrl"):
|
|
750
|
+
raise_tool_error(QingflowApiError.not_supported("NOT_SUPPORTED_IN_V1: handSignImageUrl is not supported"))
|
|
751
|
+
|
|
752
|
+
def _public_page_response(
|
|
753
|
+
self,
|
|
754
|
+
raw: dict[str, Any],
|
|
755
|
+
*,
|
|
756
|
+
items: list[dict[str, Any]],
|
|
757
|
+
pagination: dict[str, Any],
|
|
758
|
+
selection: dict[str, Any],
|
|
759
|
+
) -> dict[str, Any]:
|
|
760
|
+
response = dict(raw)
|
|
761
|
+
response["ok"] = bool(raw.get("ok", True))
|
|
762
|
+
response["warnings"] = []
|
|
763
|
+
response["output_profile"] = "normal"
|
|
764
|
+
response["data"] = {
|
|
765
|
+
"items": items,
|
|
766
|
+
"pagination": pagination,
|
|
767
|
+
"selection": selection,
|
|
768
|
+
}
|
|
769
|
+
return response
|
|
770
|
+
|
|
771
|
+
def _public_action_response(
|
|
772
|
+
self,
|
|
773
|
+
raw: dict[str, Any],
|
|
774
|
+
*,
|
|
775
|
+
action: str,
|
|
776
|
+
resource: dict[str, Any],
|
|
777
|
+
selection: dict[str, Any],
|
|
778
|
+
human_review: bool = False,
|
|
779
|
+
) -> dict[str, Any]:
|
|
780
|
+
response = dict(raw)
|
|
781
|
+
response["ok"] = bool(raw.get("ok", True))
|
|
782
|
+
response["warnings"] = []
|
|
783
|
+
response["output_profile"] = "normal"
|
|
784
|
+
response["data"] = {
|
|
785
|
+
"action": action,
|
|
786
|
+
"resource": resource,
|
|
787
|
+
"selection": selection,
|
|
788
|
+
"result": raw.get("result"),
|
|
789
|
+
"human_review": human_review,
|
|
790
|
+
}
|
|
791
|
+
return response
|
|
792
|
+
|
|
793
|
+
def _request_route_payload(self, context) -> JSONObject: # type: ignore[no-untyped-def]
|
|
794
|
+
describe_route = getattr(self.backend, "describe_route", None)
|
|
795
|
+
if callable(describe_route):
|
|
796
|
+
payload = describe_route(context)
|
|
797
|
+
if isinstance(payload, dict):
|
|
798
|
+
return payload
|
|
799
|
+
return {
|
|
800
|
+
"base_url": context.base_url,
|
|
801
|
+
"qf_version": context.qf_version,
|
|
802
|
+
"qf_version_source": getattr(context, "qf_version_source", None) or ("context" if getattr(context, "qf_version", None) else "unknown"),
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def _approval_page_items(payload: Any) -> list[dict[str, Any]]:
|
|
807
|
+
if isinstance(payload, list):
|
|
808
|
+
return [item for item in payload if isinstance(item, dict)]
|
|
809
|
+
if not isinstance(payload, dict):
|
|
810
|
+
return []
|
|
811
|
+
for key in ("list", "items", "rows", "result"):
|
|
812
|
+
value = payload.get(key)
|
|
813
|
+
if isinstance(value, list):
|
|
814
|
+
return [item for item in value if isinstance(item, dict)]
|
|
815
|
+
for container_key in ("data", "page"):
|
|
816
|
+
nested = payload.get(container_key)
|
|
817
|
+
if isinstance(nested, dict):
|
|
818
|
+
nested_items = _approval_page_items(nested)
|
|
819
|
+
if nested_items:
|
|
820
|
+
return nested_items
|
|
821
|
+
return []
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def _approval_page_amount(payload: Any) -> Any:
|
|
825
|
+
if isinstance(payload, dict):
|
|
826
|
+
return payload.get("pageAmount", payload.get("page_amount"))
|
|
827
|
+
return None
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
def _approval_page_total(payload: Any) -> Any:
|
|
831
|
+
if isinstance(payload, dict):
|
|
832
|
+
return payload.get("total", payload.get("count"))
|
|
833
|
+
return None
|