@josephyan/qingflow-mcp 0.1.0-beta.2
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 +517 -0
- package/docs/local-agent-install.md +213 -0
- package/entry_point.py +13 -0
- package/npm/bin/qingflow-mcp.mjs +7 -0
- package/npm/lib/runtime.mjs +146 -0
- package/npm/scripts/postinstall.mjs +12 -0
- package/package.json +34 -0
- package/pyproject.toml +63 -0
- package/qingflow-mcp +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 +336 -0
- package/src/qingflow_mcp/config.py +166 -0
- package/src/qingflow_mcp/errors.py +66 -0
- package/src/qingflow_mcp/json_types.py +18 -0
- package/src/qingflow_mcp/server.py +70 -0
- package/src/qingflow_mcp/session_store.py +235 -0
- package/src/qingflow_mcp/solution/__init__.py +6 -0
- package/src/qingflow_mcp/solution/build_assembly_store.py +137 -0
- package/src/qingflow_mcp/solution/compiler/__init__.py +265 -0
- package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +456 -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 +134 -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 +2064 -0
- package/src/qingflow_mcp/solution/normalizer.py +23 -0
- package/src/qingflow_mcp/solution/run_store.py +221 -0
- package/src/qingflow_mcp/solution/spec_models.py +755 -0
- package/src/qingflow_mcp/tools/__init__.py +1 -0
- package/src/qingflow_mcp/tools/app_tools.py +239 -0
- package/src/qingflow_mcp/tools/approval_tools.py +481 -0
- package/src/qingflow_mcp/tools/auth_tools.py +496 -0
- package/src/qingflow_mcp/tools/base.py +81 -0
- package/src/qingflow_mcp/tools/directory_tools.py +476 -0
- package/src/qingflow_mcp/tools/file_tools.py +375 -0
- package/src/qingflow_mcp/tools/navigation_tools.py +177 -0
- package/src/qingflow_mcp/tools/package_tools.py +142 -0
- package/src/qingflow_mcp/tools/portal_tools.py +100 -0
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +258 -0
- package/src/qingflow_mcp/tools/record_tools.py +4305 -0
- package/src/qingflow_mcp/tools/role_tools.py +94 -0
- package/src/qingflow_mcp/tools/solution_tools.py +1860 -0
- package/src/qingflow_mcp/tools/task_tools.py +677 -0
- package/src/qingflow_mcp/tools/view_tools.py +324 -0
- package/src/qingflow_mcp/tools/workflow_tools.py +311 -0
- package/src/qingflow_mcp/tools/workspace_tools.py +143 -0
|
@@ -0,0 +1,481 @@
|
|
|
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 .base import ToolBase
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ApprovalTools(ToolBase):
|
|
14
|
+
def __init__(self, sessions, backend) -> None: # type: ignore[no-untyped-def]
|
|
15
|
+
super().__init__(sessions, backend)
|
|
16
|
+
self._form_id_cache: dict[str, int] = {}
|
|
17
|
+
|
|
18
|
+
def register(self, mcp: FastMCP) -> None:
|
|
19
|
+
@mcp.tool()
|
|
20
|
+
def record_comment_add(profile: str = DEFAULT_PROFILE, app_key: str = "", apply_id: int = 0, payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
21
|
+
return self.record_comment_add(profile=profile, app_key=app_key, apply_id=apply_id, payload=payload or {})
|
|
22
|
+
|
|
23
|
+
@mcp.tool()
|
|
24
|
+
def record_comment_list(
|
|
25
|
+
profile: str = DEFAULT_PROFILE,
|
|
26
|
+
app_key: str = "",
|
|
27
|
+
apply_id: int = 0,
|
|
28
|
+
page_size: int = 20,
|
|
29
|
+
list_type: int | None = None,
|
|
30
|
+
page_num: int | None = 1,
|
|
31
|
+
) -> dict[str, Any]:
|
|
32
|
+
return self.record_comment_list(
|
|
33
|
+
profile=profile,
|
|
34
|
+
app_key=app_key,
|
|
35
|
+
apply_id=apply_id,
|
|
36
|
+
page_size=page_size,
|
|
37
|
+
list_type=list_type,
|
|
38
|
+
page_num=page_num,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
@mcp.tool()
|
|
42
|
+
def record_comment_mention_candidates(
|
|
43
|
+
profile: str = DEFAULT_PROFILE,
|
|
44
|
+
app_key: str = "",
|
|
45
|
+
apply_id: int = 0,
|
|
46
|
+
page_size: int = 20,
|
|
47
|
+
page_num: int = 1,
|
|
48
|
+
list_type: int | None = None,
|
|
49
|
+
keyword: str | None = None,
|
|
50
|
+
) -> dict[str, Any]:
|
|
51
|
+
return self.record_comment_mention_candidates(
|
|
52
|
+
profile=profile,
|
|
53
|
+
app_key=app_key,
|
|
54
|
+
apply_id=apply_id,
|
|
55
|
+
page_size=page_size,
|
|
56
|
+
page_num=page_num,
|
|
57
|
+
list_type=list_type,
|
|
58
|
+
keyword=keyword,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
@mcp.tool()
|
|
62
|
+
def record_comment_mark_read(profile: str = DEFAULT_PROFILE, app_key: str = "", apply_id: int = 0) -> dict[str, Any]:
|
|
63
|
+
return self.record_comment_mark_read(profile=profile, app_key=app_key, apply_id=apply_id)
|
|
64
|
+
|
|
65
|
+
@mcp.tool()
|
|
66
|
+
def record_comment_stats(profile: str = DEFAULT_PROFILE, app_key: str = "", apply_id: int = 0) -> dict[str, Any]:
|
|
67
|
+
return self.record_comment_stats(profile=profile, app_key=app_key, apply_id=apply_id)
|
|
68
|
+
|
|
69
|
+
@mcp.tool(description=self._high_risk_tool_description(operation="approve", target="workflow record"))
|
|
70
|
+
def record_approve(profile: str = DEFAULT_PROFILE, app_key: str = "", apply_id: int = 0, payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
71
|
+
return self.record_approve(profile=profile, app_key=app_key, apply_id=apply_id, payload=payload or {})
|
|
72
|
+
|
|
73
|
+
@mcp.tool(description=self._high_risk_tool_description(operation="reject", target="workflow record"))
|
|
74
|
+
def record_reject(profile: str = DEFAULT_PROFILE, app_key: str = "", apply_id: int = 0, payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
75
|
+
return self.record_reject(profile=profile, app_key=app_key, apply_id=apply_id, payload=payload or {})
|
|
76
|
+
|
|
77
|
+
@mcp.tool()
|
|
78
|
+
def record_rollback_candidates(profile: str = DEFAULT_PROFILE, app_key: str = "", apply_id: int = 0, audit_node_id: int = 0) -> dict[str, Any]:
|
|
79
|
+
return self.record_rollback_candidates(profile=profile, app_key=app_key, apply_id=apply_id, audit_node_id=audit_node_id)
|
|
80
|
+
|
|
81
|
+
@mcp.tool()
|
|
82
|
+
def record_rollback(profile: str = DEFAULT_PROFILE, app_key: str = "", apply_id: int = 0, payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
83
|
+
return self.record_rollback(profile=profile, app_key=app_key, apply_id=apply_id, payload=payload or {})
|
|
84
|
+
|
|
85
|
+
@mcp.tool()
|
|
86
|
+
def record_transfer(profile: str = DEFAULT_PROFILE, app_key: str = "", apply_id: int = 0, payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
87
|
+
return self.record_transfer(profile=profile, app_key=app_key, apply_id=apply_id, payload=payload or {})
|
|
88
|
+
|
|
89
|
+
@mcp.tool()
|
|
90
|
+
def record_transfer_candidates(
|
|
91
|
+
profile: str = DEFAULT_PROFILE,
|
|
92
|
+
app_key: str = "",
|
|
93
|
+
apply_id: int = 0,
|
|
94
|
+
page_size: int = 20,
|
|
95
|
+
page_num: int = 1,
|
|
96
|
+
audit_node_id: int = 0,
|
|
97
|
+
keyword: str | None = None,
|
|
98
|
+
) -> dict[str, Any]:
|
|
99
|
+
return self.record_transfer_candidates(
|
|
100
|
+
profile=profile,
|
|
101
|
+
app_key=app_key,
|
|
102
|
+
apply_id=apply_id,
|
|
103
|
+
page_size=page_size,
|
|
104
|
+
page_num=page_num,
|
|
105
|
+
audit_node_id=audit_node_id,
|
|
106
|
+
keyword=keyword,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
@mcp.tool()
|
|
110
|
+
def record_reassign_get(profile: str = DEFAULT_PROFILE, app_key: str = "", apply_id: int = 0) -> dict[str, Any]:
|
|
111
|
+
return self.record_reassign_get(profile=profile, app_key=app_key, apply_id=apply_id)
|
|
112
|
+
|
|
113
|
+
@mcp.tool()
|
|
114
|
+
def record_reassign(profile: str = DEFAULT_PROFILE, app_key: str = "", apply_id: int = 0, payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
115
|
+
return self.record_reassign(profile=profile, app_key=app_key, apply_id=apply_id, payload=payload or {})
|
|
116
|
+
|
|
117
|
+
@mcp.tool()
|
|
118
|
+
def record_countersign_candidates(
|
|
119
|
+
profile: str = DEFAULT_PROFILE,
|
|
120
|
+
app_key: str = "",
|
|
121
|
+
apply_id: int = 0,
|
|
122
|
+
page_size: int = 20,
|
|
123
|
+
page_num: int = 1,
|
|
124
|
+
audit_node_id: int = 0,
|
|
125
|
+
search_key: str | None = None,
|
|
126
|
+
) -> dict[str, Any]:
|
|
127
|
+
return self.record_countersign_candidates(
|
|
128
|
+
profile=profile,
|
|
129
|
+
app_key=app_key,
|
|
130
|
+
apply_id=apply_id,
|
|
131
|
+
page_size=page_size,
|
|
132
|
+
page_num=page_num,
|
|
133
|
+
audit_node_id=audit_node_id,
|
|
134
|
+
search_key=search_key,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
@mcp.tool()
|
|
138
|
+
def record_countersign(profile: str = DEFAULT_PROFILE, app_key: str = "", apply_id: int = 0, payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
139
|
+
return self.record_countersign(profile=profile, app_key=app_key, apply_id=apply_id, payload=payload or {})
|
|
140
|
+
|
|
141
|
+
def record_comment_add(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
|
|
142
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
143
|
+
self._validate_comment_payload(payload)
|
|
144
|
+
|
|
145
|
+
def runner(session_profile, context):
|
|
146
|
+
result = self.backend.request("POST", context, f"/app/{app_key}/apply/{apply_id}/comment", json_body=payload)
|
|
147
|
+
return {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "apply_id": apply_id, "result": result}
|
|
148
|
+
|
|
149
|
+
return self._run(profile, runner)
|
|
150
|
+
|
|
151
|
+
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]:
|
|
152
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
153
|
+
|
|
154
|
+
def runner(session_profile, context):
|
|
155
|
+
params: dict[str, Any] = {"pageSize": page_size}
|
|
156
|
+
if list_type is not None:
|
|
157
|
+
params["listType"] = list_type
|
|
158
|
+
if page_num is not None:
|
|
159
|
+
params["pageNum"] = page_num
|
|
160
|
+
result = self.backend.request("GET", context, f"/app/{app_key}/apply/{apply_id}/comment", params=params)
|
|
161
|
+
return {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "apply_id": apply_id, "page": result}
|
|
162
|
+
|
|
163
|
+
return self._run(profile, runner)
|
|
164
|
+
|
|
165
|
+
def record_comment_mention_candidates(
|
|
166
|
+
self,
|
|
167
|
+
*,
|
|
168
|
+
profile: str,
|
|
169
|
+
app_key: str,
|
|
170
|
+
apply_id: int,
|
|
171
|
+
page_size: int = 20,
|
|
172
|
+
page_num: int = 1,
|
|
173
|
+
list_type: int | None = None,
|
|
174
|
+
keyword: str | None = None,
|
|
175
|
+
) -> dict[str, Any]:
|
|
176
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
177
|
+
|
|
178
|
+
def runner(session_profile, context):
|
|
179
|
+
params: dict[str, Any] = {"pageSize": page_size, "pageNum": page_num}
|
|
180
|
+
if list_type is not None:
|
|
181
|
+
params["listType"] = list_type
|
|
182
|
+
if keyword:
|
|
183
|
+
params["keyword"] = keyword
|
|
184
|
+
result = self.backend.request("GET", context, f"/app/{app_key}/apply/{apply_id}/comment/member", params=params)
|
|
185
|
+
return {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "apply_id": apply_id, "page": result}
|
|
186
|
+
|
|
187
|
+
return self._run(profile, runner)
|
|
188
|
+
|
|
189
|
+
def record_comment_mark_read(self, *, profile: str, app_key: str, apply_id: int) -> dict[str, Any]:
|
|
190
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
191
|
+
|
|
192
|
+
def runner(session_profile, context):
|
|
193
|
+
result = self.backend.request("POST", context, f"/app/{app_key}/apply/{apply_id}/comment/read")
|
|
194
|
+
return {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "apply_id": apply_id, "result": result}
|
|
195
|
+
|
|
196
|
+
return self._run(profile, runner)
|
|
197
|
+
|
|
198
|
+
def record_comment_stats(self, *, profile: str, app_key: str, apply_id: int) -> dict[str, Any]:
|
|
199
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
200
|
+
|
|
201
|
+
def runner(session_profile, context):
|
|
202
|
+
result = self.backend.request("GET", context, f"/app/{app_key}/apply/{apply_id}/comment/statistic")
|
|
203
|
+
return {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "apply_id": apply_id, "result": result}
|
|
204
|
+
|
|
205
|
+
return self._run(profile, runner)
|
|
206
|
+
|
|
207
|
+
def record_approve(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
|
|
208
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
209
|
+
body = self._require_dict(payload)
|
|
210
|
+
|
|
211
|
+
def runner(session_profile, context):
|
|
212
|
+
approval_body = self._normalize_approval_payload(profile, context, app_key, apply_id, body)
|
|
213
|
+
result = self.backend.request("POST", context, "/workflow/engine/approval/approve", json_body=approval_body)
|
|
214
|
+
return {
|
|
215
|
+
"profile": profile,
|
|
216
|
+
"ws_id": session_profile.selected_ws_id,
|
|
217
|
+
"app_key": app_key,
|
|
218
|
+
"apply_id": apply_id,
|
|
219
|
+
"form_id": approval_body["formId"],
|
|
220
|
+
"node_id": approval_body["nodeId"],
|
|
221
|
+
"result": result,
|
|
222
|
+
"request_route": self._request_route_payload(context),
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return self._run(profile, runner)
|
|
226
|
+
|
|
227
|
+
def record_reject(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
|
|
228
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
229
|
+
body = self._require_dict(payload)
|
|
230
|
+
|
|
231
|
+
def runner(session_profile, context):
|
|
232
|
+
approval_body = self._normalize_approval_payload(profile, context, app_key, apply_id, body)
|
|
233
|
+
result = self.backend.request("POST", context, "/workflow/engine/approval/reject", json_body=approval_body)
|
|
234
|
+
return {
|
|
235
|
+
"profile": profile,
|
|
236
|
+
"ws_id": session_profile.selected_ws_id,
|
|
237
|
+
"app_key": app_key,
|
|
238
|
+
"apply_id": apply_id,
|
|
239
|
+
"form_id": approval_body["formId"],
|
|
240
|
+
"node_id": approval_body["nodeId"],
|
|
241
|
+
"result": result,
|
|
242
|
+
"request_route": self._request_route_payload(context),
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return self._run(profile, runner)
|
|
246
|
+
|
|
247
|
+
def record_rollback_candidates(self, *, profile: str, app_key: str, apply_id: int, audit_node_id: int) -> dict[str, Any]:
|
|
248
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
249
|
+
if audit_node_id <= 0:
|
|
250
|
+
raise_tool_error(QingflowApiError.config_error("audit_node_id must be positive"))
|
|
251
|
+
|
|
252
|
+
def runner(session_profile, context):
|
|
253
|
+
result = self.backend.request(
|
|
254
|
+
"GET",
|
|
255
|
+
context,
|
|
256
|
+
f"/app/{app_key}/apply/{apply_id}/revertNode",
|
|
257
|
+
params={"auditNodeId": audit_node_id},
|
|
258
|
+
)
|
|
259
|
+
return {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "apply_id": apply_id, "result": result}
|
|
260
|
+
|
|
261
|
+
return self._run(profile, runner)
|
|
262
|
+
|
|
263
|
+
def record_rollback(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
|
|
264
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
265
|
+
body = self._require_dict(payload)
|
|
266
|
+
self._validate_audit_payload(body)
|
|
267
|
+
|
|
268
|
+
def runner(session_profile, context):
|
|
269
|
+
result = self.backend.request("POST", context, f"/app/{app_key}/apply/{apply_id}/rollback", json_body=body)
|
|
270
|
+
return {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "apply_id": apply_id, "result": result}
|
|
271
|
+
|
|
272
|
+
return self._run(profile, runner)
|
|
273
|
+
|
|
274
|
+
def record_transfer(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
|
|
275
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
276
|
+
body = self._require_dict(payload)
|
|
277
|
+
self._validate_audit_payload(body, require_uid=True)
|
|
278
|
+
|
|
279
|
+
def runner(session_profile, context):
|
|
280
|
+
result = self.backend.request("POST", context, f"/app/{app_key}/apply/{apply_id}/transfer", json_body=body)
|
|
281
|
+
return {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "apply_id": apply_id, "result": result}
|
|
282
|
+
|
|
283
|
+
return self._run(profile, runner)
|
|
284
|
+
|
|
285
|
+
def record_transfer_candidates(
|
|
286
|
+
self,
|
|
287
|
+
*,
|
|
288
|
+
profile: str,
|
|
289
|
+
app_key: str,
|
|
290
|
+
apply_id: int,
|
|
291
|
+
page_size: int = 20,
|
|
292
|
+
page_num: int = 1,
|
|
293
|
+
audit_node_id: int = 0,
|
|
294
|
+
keyword: str | None = None,
|
|
295
|
+
) -> dict[str, Any]:
|
|
296
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
297
|
+
if audit_node_id <= 0:
|
|
298
|
+
raise_tool_error(QingflowApiError.config_error("audit_node_id must be positive"))
|
|
299
|
+
|
|
300
|
+
def runner(session_profile, context):
|
|
301
|
+
params: dict[str, Any] = {"pageSize": page_size, "pageNum": page_num, "auditNodeId": audit_node_id}
|
|
302
|
+
if keyword:
|
|
303
|
+
params["keyword"] = keyword
|
|
304
|
+
result = self.backend.request("GET", context, f"/app/{app_key}/apply/{apply_id}/transfer/member", params=params)
|
|
305
|
+
return {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "apply_id": apply_id, "page": result}
|
|
306
|
+
|
|
307
|
+
return self._run(profile, runner)
|
|
308
|
+
|
|
309
|
+
def record_reassign_get(self, *, profile: str, app_key: str, apply_id: int) -> dict[str, Any]:
|
|
310
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
311
|
+
|
|
312
|
+
def runner(session_profile, context):
|
|
313
|
+
result = self.backend.request("GET", context, f"/app/{app_key}/apply/{apply_id}/reassign")
|
|
314
|
+
return {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "apply_id": apply_id, "result": result}
|
|
315
|
+
|
|
316
|
+
return self._run(profile, runner)
|
|
317
|
+
|
|
318
|
+
def record_reassign(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
|
|
319
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
320
|
+
body = self._require_dict(payload)
|
|
321
|
+
|
|
322
|
+
def runner(session_profile, context):
|
|
323
|
+
result = self.backend.request("POST", context, f"/app/{app_key}/apply/{apply_id}/reassign", json_body=body)
|
|
324
|
+
return {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "apply_id": apply_id, "result": result}
|
|
325
|
+
|
|
326
|
+
return self._run(profile, runner)
|
|
327
|
+
|
|
328
|
+
def record_countersign_candidates(
|
|
329
|
+
self,
|
|
330
|
+
*,
|
|
331
|
+
profile: str,
|
|
332
|
+
app_key: str,
|
|
333
|
+
apply_id: int,
|
|
334
|
+
page_size: int = 20,
|
|
335
|
+
page_num: int = 1,
|
|
336
|
+
audit_node_id: int = 0,
|
|
337
|
+
search_key: str | None = None,
|
|
338
|
+
) -> dict[str, Any]:
|
|
339
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
340
|
+
if audit_node_id <= 0:
|
|
341
|
+
raise_tool_error(QingflowApiError.config_error("audit_node_id must be positive"))
|
|
342
|
+
|
|
343
|
+
def runner(session_profile, context):
|
|
344
|
+
params: dict[str, Any] = {"pageSize": page_size, "pageNum": page_num, "auditNodeId": audit_node_id}
|
|
345
|
+
if search_key:
|
|
346
|
+
params["searchKey"] = search_key
|
|
347
|
+
result = self.backend.request("GET", context, f"/app/{app_key}/apply/{apply_id}/countersign/member", params=params)
|
|
348
|
+
return {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "apply_id": apply_id, "page": result}
|
|
349
|
+
|
|
350
|
+
return self._run(profile, runner)
|
|
351
|
+
|
|
352
|
+
def record_countersign(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
|
|
353
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
354
|
+
body = self._require_dict(payload)
|
|
355
|
+
self._validate_countersign_payload(body)
|
|
356
|
+
|
|
357
|
+
def runner(session_profile, context):
|
|
358
|
+
result = self.backend.request("POST", context, f"/app/{app_key}/apply/{apply_id}/countersign", json_body=body)
|
|
359
|
+
return {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "apply_id": apply_id, "result": result}
|
|
360
|
+
|
|
361
|
+
return self._run(profile, runner)
|
|
362
|
+
|
|
363
|
+
def _require_app_and_apply(self, app_key: str, apply_id: int) -> None:
|
|
364
|
+
if not app_key:
|
|
365
|
+
raise_tool_error(QingflowApiError.config_error("app_key is required"))
|
|
366
|
+
if apply_id <= 0:
|
|
367
|
+
raise_tool_error(QingflowApiError.config_error("apply_id must be positive"))
|
|
368
|
+
|
|
369
|
+
def _validate_comment_payload(self, payload: dict[str, Any]) -> None:
|
|
370
|
+
comment_detail = payload.get("commentDetail")
|
|
371
|
+
if not isinstance(comment_detail, dict):
|
|
372
|
+
raise_tool_error(QingflowApiError.config_error("payload.commentDetail must be an object"))
|
|
373
|
+
if not comment_detail.get("commentMsg"):
|
|
374
|
+
raise_tool_error(QingflowApiError.config_error("payload.commentDetail.commentMsg is required"))
|
|
375
|
+
|
|
376
|
+
def _normalize_approval_payload(self, profile: str, context, app_key: str, apply_id: int, payload: dict[str, Any]) -> JSONObject: # type: ignore[no-untyped-def]
|
|
377
|
+
body: JSONObject = dict(payload)
|
|
378
|
+
self._normalize_alias(body, "auditFeedback", "audit_feedback")
|
|
379
|
+
self._normalize_alias(body, "uploadFileSize", "upload_file_size")
|
|
380
|
+
self._normalize_alias(body, "handSignImageUrl", "hand_sign_image_url")
|
|
381
|
+
self._normalize_alias(body, "beingSaveSignature", "being_save_signature")
|
|
382
|
+
self._normalize_alias(body, "applyId", "apply_id")
|
|
383
|
+
self._normalize_alias(body, "formId", "form_id")
|
|
384
|
+
|
|
385
|
+
node_id = self._extract_node_id(body)
|
|
386
|
+
body["nodeId"] = node_id
|
|
387
|
+
body["applyId"] = self._match_or_fill_int(body, field_name="applyId", expected_value=apply_id)
|
|
388
|
+
body["formId"] = self._resolve_form_id(profile, context, app_key, explicit_form_id=body.get("formId"))
|
|
389
|
+
|
|
390
|
+
self._validate_approval_payload(body)
|
|
391
|
+
return body
|
|
392
|
+
|
|
393
|
+
def _extract_node_id(self, payload: JSONObject) -> int:
|
|
394
|
+
node_id = payload.get("nodeId")
|
|
395
|
+
audit_node_id = payload.pop("auditNodeId", None)
|
|
396
|
+
if node_id is None:
|
|
397
|
+
node_id = audit_node_id
|
|
398
|
+
elif audit_node_id is not None and node_id != audit_node_id:
|
|
399
|
+
raise_tool_error(QingflowApiError.config_error("payload.nodeId and payload.auditNodeId must match when both are provided"))
|
|
400
|
+
if not isinstance(node_id, int) or node_id <= 0:
|
|
401
|
+
raise_tool_error(QingflowApiError.config_error("payload.nodeId or payload.auditNodeId must be a positive integer"))
|
|
402
|
+
return node_id
|
|
403
|
+
|
|
404
|
+
def _resolve_form_id(self, profile: str, context, app_key: str, *, explicit_form_id: Any | None) -> int: # type: ignore[no-untyped-def]
|
|
405
|
+
if explicit_form_id is not None:
|
|
406
|
+
if not isinstance(explicit_form_id, int) or explicit_form_id <= 0:
|
|
407
|
+
raise_tool_error(QingflowApiError.config_error("payload.formId must be a positive integer"))
|
|
408
|
+
form_id = self._get_form_id(profile, context, app_key)
|
|
409
|
+
if form_id != explicit_form_id:
|
|
410
|
+
raise_tool_error(
|
|
411
|
+
QingflowApiError.config_error(
|
|
412
|
+
f"payload.formId={explicit_form_id} does not match app_key '{app_key}' formId={form_id}"
|
|
413
|
+
)
|
|
414
|
+
)
|
|
415
|
+
return explicit_form_id
|
|
416
|
+
return self._get_form_id(profile, context, app_key)
|
|
417
|
+
|
|
418
|
+
def _get_form_id(self, profile: str, context, app_key: str) -> int: # type: ignore[no-untyped-def]
|
|
419
|
+
cache_key = f"{profile}:{app_key}"
|
|
420
|
+
cached = self._form_id_cache.get(cache_key)
|
|
421
|
+
if cached is not None:
|
|
422
|
+
return cached
|
|
423
|
+
result = self.backend.request("GET", context, f"/app/{app_key}/baseInfo")
|
|
424
|
+
form_id = result.get("formId") if isinstance(result, dict) else None
|
|
425
|
+
if not isinstance(form_id, int) or form_id <= 0:
|
|
426
|
+
raise_tool_error(QingflowApiError.config_error(f"cannot resolve formId for app_key '{app_key}'"))
|
|
427
|
+
self._form_id_cache[cache_key] = form_id
|
|
428
|
+
return form_id
|
|
429
|
+
|
|
430
|
+
def _match_or_fill_int(self, payload: JSONObject, *, field_name: str, expected_value: int) -> int:
|
|
431
|
+
current = payload.get(field_name)
|
|
432
|
+
if current is None:
|
|
433
|
+
return expected_value
|
|
434
|
+
if not isinstance(current, int) or current <= 0:
|
|
435
|
+
raise_tool_error(QingflowApiError.config_error(f"payload.{field_name} must be a positive integer"))
|
|
436
|
+
if current != expected_value:
|
|
437
|
+
raise_tool_error(QingflowApiError.config_error(f"payload.{field_name}={current} does not match apply_id={expected_value}"))
|
|
438
|
+
return current
|
|
439
|
+
|
|
440
|
+
def _normalize_alias(self, payload: JSONObject, canonical_key: str, alias_key: str) -> None:
|
|
441
|
+
alias_value = payload.pop(alias_key, None)
|
|
442
|
+
if canonical_key not in payload and alias_value is not None:
|
|
443
|
+
payload[canonical_key] = alias_value
|
|
444
|
+
elif alias_value is not None and payload.get(canonical_key) != alias_value:
|
|
445
|
+
raise_tool_error(QingflowApiError.config_error(f"payload.{canonical_key} and payload.{alias_key} must match when both are provided"))
|
|
446
|
+
|
|
447
|
+
def _validate_approval_payload(self, payload: dict[str, Any]) -> None:
|
|
448
|
+
self._reject_unsupported_fields(payload)
|
|
449
|
+
if not isinstance(payload.get("formId"), int) or payload["formId"] <= 0:
|
|
450
|
+
raise_tool_error(QingflowApiError.config_error("payload.formId must be a positive integer"))
|
|
451
|
+
if not isinstance(payload.get("applyId"), int) or payload["applyId"] <= 0:
|
|
452
|
+
raise_tool_error(QingflowApiError.config_error("payload.applyId must be a positive integer"))
|
|
453
|
+
if not isinstance(payload.get("nodeId"), int) or payload["nodeId"] <= 0:
|
|
454
|
+
raise_tool_error(QingflowApiError.config_error("payload.nodeId must be a positive integer"))
|
|
455
|
+
|
|
456
|
+
def _validate_audit_payload(self, payload: dict[str, Any], *, require_uid: bool = False) -> None:
|
|
457
|
+
self._reject_unsupported_fields(payload)
|
|
458
|
+
if require_uid and not payload.get("uid"):
|
|
459
|
+
raise_tool_error(QingflowApiError.config_error("payload.uid is required"))
|
|
460
|
+
|
|
461
|
+
def _validate_countersign_payload(self, payload: dict[str, Any]) -> None:
|
|
462
|
+
self._reject_unsupported_fields(payload)
|
|
463
|
+
members = payload.get("countersignMembers")
|
|
464
|
+
if not isinstance(members, list) or not members:
|
|
465
|
+
raise_tool_error(QingflowApiError.config_error("payload.countersignMembers must be a non-empty array"))
|
|
466
|
+
|
|
467
|
+
def _reject_unsupported_fields(self, payload: dict[str, Any]) -> None:
|
|
468
|
+
if payload.get("handSignImageUrl"):
|
|
469
|
+
raise_tool_error(QingflowApiError.not_supported("NOT_SUPPORTED_IN_V1: handSignImageUrl is not supported"))
|
|
470
|
+
|
|
471
|
+
def _request_route_payload(self, context) -> JSONObject: # type: ignore[no-untyped-def]
|
|
472
|
+
describe_route = getattr(self.backend, "describe_route", None)
|
|
473
|
+
if callable(describe_route):
|
|
474
|
+
payload = describe_route(context)
|
|
475
|
+
if isinstance(payload, dict):
|
|
476
|
+
return payload
|
|
477
|
+
return {
|
|
478
|
+
"base_url": context.base_url,
|
|
479
|
+
"qf_version": context.qf_version,
|
|
480
|
+
"qf_version_source": getattr(context, "qf_version_source", None) or ("context" if getattr(context, "qf_version", None) else "unknown"),
|
|
481
|
+
}
|