@josephyan/qingflow-cli 0.2.0-beta.1000
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -0
- package/docs/local-agent-install.md +309 -0
- package/entry_point.py +13 -0
- package/npm/bin/qingflow.mjs +5 -0
- package/npm/lib/runtime.mjs +346 -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 +37 -0
- package/src/qingflow_mcp/__main__.py +5 -0
- package/src/qingflow_mcp/backend_client.py +649 -0
- package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
- package/src/qingflow_mcp/builder_facade/models.py +1846 -0
- package/src/qingflow_mcp/builder_facade/service.py +16502 -0
- package/src/qingflow_mcp/cli/__init__.py +1 -0
- package/src/qingflow_mcp/cli/commands/__init__.py +18 -0
- package/src/qingflow_mcp/cli/commands/app.py +40 -0
- package/src/qingflow_mcp/cli/commands/auth.py +112 -0
- package/src/qingflow_mcp/cli/commands/builder.py +539 -0
- package/src/qingflow_mcp/cli/commands/chart.py +18 -0
- package/src/qingflow_mcp/cli/commands/common.py +62 -0
- package/src/qingflow_mcp/cli/commands/imports.py +96 -0
- package/src/qingflow_mcp/cli/commands/portal.py +25 -0
- package/src/qingflow_mcp/cli/commands/record.py +331 -0
- package/src/qingflow_mcp/cli/commands/repo.py +80 -0
- package/src/qingflow_mcp/cli/commands/task.py +141 -0
- package/src/qingflow_mcp/cli/commands/view.py +18 -0
- package/src/qingflow_mcp/cli/commands/workspace.py +110 -0
- package/src/qingflow_mcp/cli/context.py +60 -0
- package/src/qingflow_mcp/cli/formatters.py +573 -0
- package/src/qingflow_mcp/cli/json_io.py +50 -0
- package/src/qingflow_mcp/cli/main.py +186 -0
- package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
- package/src/qingflow_mcp/cli/terminal_ui.py +173 -0
- package/src/qingflow_mcp/config.py +407 -0
- package/src/qingflow_mcp/errors.py +66 -0
- package/src/qingflow_mcp/id_utils.py +49 -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/public_surface.py +243 -0
- package/src/qingflow_mcp/repository_store.py +71 -0
- package/src/qingflow_mcp/response_trim.py +841 -0
- package/src/qingflow_mcp/server.py +216 -0
- package/src/qingflow_mcp/server_app_builder.py +543 -0
- package/src/qingflow_mcp/server_app_user.py +386 -0
- package/src/qingflow_mcp/session_store.py +369 -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 +495 -0
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +187 -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 +2398 -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 +855 -0
- package/src/qingflow_mcp/tools/__init__.py +1 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +3449 -0
- package/src/qingflow_mcp/tools/app_tools.py +926 -0
- package/src/qingflow_mcp/tools/approval_tools.py +1062 -0
- package/src/qingflow_mcp/tools/auth_tools.py +1133 -0
- package/src/qingflow_mcp/tools/base.py +281 -0
- package/src/qingflow_mcp/tools/code_block_tools.py +777 -0
- package/src/qingflow_mcp/tools/custom_button_tools.py +202 -0
- package/src/qingflow_mcp/tools/directory_tools.py +675 -0
- package/src/qingflow_mcp/tools/feedback_tools.py +238 -0
- package/src/qingflow_mcp/tools/file_tools.py +409 -0
- package/src/qingflow_mcp/tools/import_tools.py +2223 -0
- package/src/qingflow_mcp/tools/navigation_tools.py +210 -0
- package/src/qingflow_mcp/tools/package_tools.py +326 -0
- package/src/qingflow_mcp/tools/portal_tools.py +158 -0
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +374 -0
- package/src/qingflow_mcp/tools/record_tools.py +14291 -0
- package/src/qingflow_mcp/tools/repository_dev_tools.py +552 -0
- package/src/qingflow_mcp/tools/resource_read_tools.py +503 -0
- package/src/qingflow_mcp/tools/role_tools.py +112 -0
- package/src/qingflow_mcp/tools/solution_tools.py +4054 -0
- package/src/qingflow_mcp/tools/task_context_tools.py +2986 -0
- package/src/qingflow_mcp/tools/task_tools.py +889 -0
- package/src/qingflow_mcp/tools/view_tools.py +335 -0
- package/src/qingflow_mcp/tools/workflow_tools.py +376 -0
- package/src/qingflow_mcp/tools/workspace_tools.py +266 -0
|
@@ -0,0 +1,1062 @@
|
|
|
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, tool_cn_name
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ApprovalTools(ToolBase):
|
|
15
|
+
"""审批工具(中文名:审批数据与候选范围)。
|
|
16
|
+
|
|
17
|
+
类型:审批辅助工具。
|
|
18
|
+
主要职责:
|
|
19
|
+
1. 查询审批相关列表与详情;
|
|
20
|
+
2. 提供审批节点候选范围与运行时上下文辅助;
|
|
21
|
+
3. 为任务执行类工具提供审批侧数据支撑。
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, sessions, backend) -> None: # type: ignore[no-untyped-def]
|
|
25
|
+
"""执行内部辅助逻辑。"""
|
|
26
|
+
super().__init__(sessions, backend)
|
|
27
|
+
self._form_id_cache: dict[str, int] = {}
|
|
28
|
+
|
|
29
|
+
def register(self, mcp: FastMCP) -> None:
|
|
30
|
+
"""注册当前工具到 MCP 服务。"""
|
|
31
|
+
@mcp.tool()
|
|
32
|
+
def record_comment_write(
|
|
33
|
+
profile: str = DEFAULT_PROFILE,
|
|
34
|
+
app_key: str = "",
|
|
35
|
+
record_id: int = 0,
|
|
36
|
+
payload: dict[str, Any] | None = None,
|
|
37
|
+
) -> dict[str, Any]:
|
|
38
|
+
return self.record_comment_write(profile=profile, app_key=app_key, record_id=record_id, payload=payload or {})
|
|
39
|
+
|
|
40
|
+
@mcp.tool()
|
|
41
|
+
def record_comment_list(
|
|
42
|
+
profile: str = DEFAULT_PROFILE,
|
|
43
|
+
app_key: str = "",
|
|
44
|
+
record_id: int = 0,
|
|
45
|
+
page_size: int = 20,
|
|
46
|
+
list_type: int | None = None,
|
|
47
|
+
page_num: int | None = 1,
|
|
48
|
+
) -> dict[str, Any]:
|
|
49
|
+
return self.record_comment_list(
|
|
50
|
+
profile=profile,
|
|
51
|
+
app_key=app_key,
|
|
52
|
+
apply_id=record_id,
|
|
53
|
+
page_size=page_size,
|
|
54
|
+
list_type=list_type,
|
|
55
|
+
page_num=page_num,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
@mcp.tool()
|
|
59
|
+
def record_comment_mentions(
|
|
60
|
+
profile: str = DEFAULT_PROFILE,
|
|
61
|
+
app_key: str = "",
|
|
62
|
+
record_id: int = 0,
|
|
63
|
+
page_size: int = 20,
|
|
64
|
+
page_num: int = 1,
|
|
65
|
+
list_type: int | None = None,
|
|
66
|
+
keyword: str | None = None,
|
|
67
|
+
) -> dict[str, Any]:
|
|
68
|
+
return self.record_comment_mentions(
|
|
69
|
+
profile=profile,
|
|
70
|
+
app_key=app_key,
|
|
71
|
+
record_id=record_id,
|
|
72
|
+
page_size=page_size,
|
|
73
|
+
page_num=page_num,
|
|
74
|
+
list_type=list_type,
|
|
75
|
+
keyword=keyword,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
@mcp.tool()
|
|
79
|
+
def record_comment_mark_read(profile: str = DEFAULT_PROFILE, app_key: str = "", record_id: int = 0) -> dict[str, Any]:
|
|
80
|
+
return self.record_comment_mark_read(profile=profile, app_key=app_key, apply_id=record_id)
|
|
81
|
+
|
|
82
|
+
@mcp.tool(description=self._high_risk_tool_description(operation="approve", target="workflow task"))
|
|
83
|
+
def task_approve(
|
|
84
|
+
profile: str = DEFAULT_PROFILE,
|
|
85
|
+
app_key: str = "",
|
|
86
|
+
record_id: int = 0,
|
|
87
|
+
payload: dict[str, Any] | None = None,
|
|
88
|
+
fields: dict[str, Any] | None = None,
|
|
89
|
+
) -> dict[str, Any]:
|
|
90
|
+
return self.task_approve(profile=profile, app_key=app_key, record_id=record_id, payload=payload or {}, fields=fields or {})
|
|
91
|
+
|
|
92
|
+
@mcp.tool(description=self._high_risk_tool_description(operation="reject", target="workflow task"))
|
|
93
|
+
def task_reject(
|
|
94
|
+
profile: str = DEFAULT_PROFILE,
|
|
95
|
+
app_key: str = "",
|
|
96
|
+
record_id: int = 0,
|
|
97
|
+
payload: dict[str, Any] | None = None,
|
|
98
|
+
fields: dict[str, Any] | None = None,
|
|
99
|
+
) -> dict[str, Any]:
|
|
100
|
+
return self.task_reject(profile=profile, app_key=app_key, record_id=record_id, payload=payload or {}, fields=fields or {})
|
|
101
|
+
|
|
102
|
+
@mcp.tool()
|
|
103
|
+
def task_rollback_candidates(
|
|
104
|
+
profile: str = DEFAULT_PROFILE,
|
|
105
|
+
app_key: str = "",
|
|
106
|
+
record_id: int = 0,
|
|
107
|
+
workflow_node_id: int = 0,
|
|
108
|
+
) -> dict[str, Any]:
|
|
109
|
+
return self.task_rollback_candidates(profile=profile, app_key=app_key, record_id=record_id, workflow_node_id=workflow_node_id)
|
|
110
|
+
|
|
111
|
+
@mcp.tool()
|
|
112
|
+
def task_rollback(
|
|
113
|
+
profile: str = DEFAULT_PROFILE,
|
|
114
|
+
app_key: str = "",
|
|
115
|
+
record_id: int = 0,
|
|
116
|
+
payload: dict[str, Any] | None = None,
|
|
117
|
+
fields: dict[str, Any] | None = None,
|
|
118
|
+
) -> dict[str, Any]:
|
|
119
|
+
return self.task_rollback(profile=profile, app_key=app_key, record_id=record_id, payload=payload or {}, fields=fields or {})
|
|
120
|
+
|
|
121
|
+
@mcp.tool()
|
|
122
|
+
def task_transfer(
|
|
123
|
+
profile: str = DEFAULT_PROFILE,
|
|
124
|
+
app_key: str = "",
|
|
125
|
+
record_id: int = 0,
|
|
126
|
+
payload: dict[str, Any] | None = None,
|
|
127
|
+
fields: dict[str, Any] | None = None,
|
|
128
|
+
) -> dict[str, Any]:
|
|
129
|
+
return self.task_transfer(profile=profile, app_key=app_key, record_id=record_id, payload=payload or {}, fields=fields or {})
|
|
130
|
+
|
|
131
|
+
@mcp.tool()
|
|
132
|
+
def task_transfer_candidates(
|
|
133
|
+
profile: str = DEFAULT_PROFILE,
|
|
134
|
+
app_key: str = "",
|
|
135
|
+
record_id: int = 0,
|
|
136
|
+
page_size: int = 20,
|
|
137
|
+
page_num: int = 1,
|
|
138
|
+
workflow_node_id: int = 0,
|
|
139
|
+
keyword: str | None = None,
|
|
140
|
+
) -> dict[str, Any]:
|
|
141
|
+
return self.task_transfer_candidates(
|
|
142
|
+
profile=profile,
|
|
143
|
+
app_key=app_key,
|
|
144
|
+
record_id=record_id,
|
|
145
|
+
page_size=page_size,
|
|
146
|
+
page_num=page_num,
|
|
147
|
+
workflow_node_id=workflow_node_id,
|
|
148
|
+
keyword=keyword,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
@tool_cn_name("写评论")
|
|
152
|
+
def record_comment_write(self, *, profile: str, app_key: str, record_id: int, payload: dict[str, Any]) -> dict[str, Any]:
|
|
153
|
+
"""执行记录相关逻辑。"""
|
|
154
|
+
raw = self.record_comment_add(profile=profile, app_key=app_key, apply_id=record_id, payload=payload)
|
|
155
|
+
return self._public_action_response(
|
|
156
|
+
raw,
|
|
157
|
+
action="record_comment_write",
|
|
158
|
+
resource={"app_key": app_key, "record_id": record_id},
|
|
159
|
+
selection={},
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
@tool_cn_name("评论提及")
|
|
163
|
+
def record_comment_mentions(
|
|
164
|
+
self,
|
|
165
|
+
*,
|
|
166
|
+
profile: str,
|
|
167
|
+
app_key: str,
|
|
168
|
+
record_id: int,
|
|
169
|
+
page_size: int = 20,
|
|
170
|
+
page_num: int = 1,
|
|
171
|
+
list_type: int | None = None,
|
|
172
|
+
keyword: str | None = None,
|
|
173
|
+
) -> dict[str, Any]:
|
|
174
|
+
"""执行记录相关逻辑。"""
|
|
175
|
+
raw = self.record_comment_mention_candidates(
|
|
176
|
+
profile=profile,
|
|
177
|
+
app_key=app_key,
|
|
178
|
+
apply_id=record_id,
|
|
179
|
+
page_size=page_size,
|
|
180
|
+
page_num=page_num,
|
|
181
|
+
list_type=list_type,
|
|
182
|
+
keyword=keyword,
|
|
183
|
+
)
|
|
184
|
+
items = _approval_page_items(raw.get("page"))
|
|
185
|
+
return self._public_page_response(
|
|
186
|
+
raw,
|
|
187
|
+
items=items,
|
|
188
|
+
pagination={
|
|
189
|
+
"page": page_num,
|
|
190
|
+
"page_size": page_size,
|
|
191
|
+
"returned_items": len(items),
|
|
192
|
+
"page_amount": _approval_page_amount(raw.get("page")),
|
|
193
|
+
"reported_total": _approval_page_total(raw.get("page")),
|
|
194
|
+
},
|
|
195
|
+
selection={"app_key": app_key, "record_id": record_id, "list_type": list_type, "keyword": keyword},
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
@tool_cn_name("任务通过")
|
|
199
|
+
def task_approve(self, *, profile: str, app_key: str, record_id: int, payload: dict[str, Any], fields: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
200
|
+
"""执行任务相关逻辑。"""
|
|
201
|
+
if fields:
|
|
202
|
+
return self._delegate_task_action(
|
|
203
|
+
profile=profile,
|
|
204
|
+
app_key=app_key,
|
|
205
|
+
record_id=record_id,
|
|
206
|
+
action="approve",
|
|
207
|
+
payload=payload,
|
|
208
|
+
fields=fields,
|
|
209
|
+
public_action="task_approve",
|
|
210
|
+
)
|
|
211
|
+
raw = self.record_approve(profile=profile, app_key=app_key, apply_id=record_id, payload=payload)
|
|
212
|
+
return self._public_action_response(
|
|
213
|
+
raw,
|
|
214
|
+
action="task_approve",
|
|
215
|
+
resource={"app_key": app_key, "record_id": record_id},
|
|
216
|
+
selection={},
|
|
217
|
+
human_review=True,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
@tool_cn_name("任务拒绝")
|
|
221
|
+
def task_reject(self, *, profile: str, app_key: str, record_id: int, payload: dict[str, Any], fields: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
222
|
+
"""执行任务相关逻辑。"""
|
|
223
|
+
if fields:
|
|
224
|
+
return self._delegate_task_action(
|
|
225
|
+
profile=profile,
|
|
226
|
+
app_key=app_key,
|
|
227
|
+
record_id=record_id,
|
|
228
|
+
action="reject",
|
|
229
|
+
payload=payload,
|
|
230
|
+
fields=fields,
|
|
231
|
+
public_action="task_reject",
|
|
232
|
+
)
|
|
233
|
+
raw = self.record_reject(profile=profile, app_key=app_key, apply_id=record_id, payload=payload)
|
|
234
|
+
return self._public_action_response(
|
|
235
|
+
raw,
|
|
236
|
+
action="task_reject",
|
|
237
|
+
resource={"app_key": app_key, "record_id": record_id},
|
|
238
|
+
selection={},
|
|
239
|
+
human_review=True,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
@tool_cn_name("任务退回候选")
|
|
243
|
+
def task_rollback_candidates(self, *, profile: str, app_key: str, record_id: int, workflow_node_id: int) -> dict[str, Any]:
|
|
244
|
+
"""执行任务相关逻辑。"""
|
|
245
|
+
raw = self.record_rollback_candidates(profile=profile, app_key=app_key, apply_id=record_id, audit_node_id=workflow_node_id)
|
|
246
|
+
items = _approval_page_items(raw.get("result"))
|
|
247
|
+
return self._public_page_response(
|
|
248
|
+
raw,
|
|
249
|
+
items=items,
|
|
250
|
+
pagination={"returned_items": len(items)},
|
|
251
|
+
selection={"app_key": app_key, "record_id": record_id, "workflow_node_id": workflow_node_id},
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
@tool_cn_name("任务退回")
|
|
255
|
+
def task_rollback(self, *, profile: str, app_key: str, record_id: int, payload: dict[str, Any], fields: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
256
|
+
"""执行任务相关逻辑。"""
|
|
257
|
+
if fields:
|
|
258
|
+
return self._delegate_task_action(
|
|
259
|
+
profile=profile,
|
|
260
|
+
app_key=app_key,
|
|
261
|
+
record_id=record_id,
|
|
262
|
+
action="rollback",
|
|
263
|
+
payload=payload,
|
|
264
|
+
fields=fields,
|
|
265
|
+
public_action="task_rollback",
|
|
266
|
+
)
|
|
267
|
+
raw = self.record_rollback(profile=profile, app_key=app_key, apply_id=record_id, payload=payload)
|
|
268
|
+
return self._public_action_response(
|
|
269
|
+
raw,
|
|
270
|
+
action="task_rollback",
|
|
271
|
+
resource={"app_key": app_key, "record_id": record_id},
|
|
272
|
+
selection={},
|
|
273
|
+
human_review=True,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
@tool_cn_name("任务转交")
|
|
277
|
+
def task_transfer(self, *, profile: str, app_key: str, record_id: int, payload: dict[str, Any], fields: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
278
|
+
"""执行任务相关逻辑。"""
|
|
279
|
+
if fields:
|
|
280
|
+
return self._delegate_task_action(
|
|
281
|
+
profile=profile,
|
|
282
|
+
app_key=app_key,
|
|
283
|
+
record_id=record_id,
|
|
284
|
+
action="transfer",
|
|
285
|
+
payload=payload,
|
|
286
|
+
fields=fields,
|
|
287
|
+
public_action="task_transfer",
|
|
288
|
+
)
|
|
289
|
+
self._raise_if_self_transfer(profile=profile, payload=payload)
|
|
290
|
+
raw = self.record_transfer(profile=profile, app_key=app_key, apply_id=record_id, payload=payload)
|
|
291
|
+
return self._public_action_response(
|
|
292
|
+
raw,
|
|
293
|
+
action="task_transfer",
|
|
294
|
+
resource={"app_key": app_key, "record_id": record_id},
|
|
295
|
+
selection={},
|
|
296
|
+
human_review=True,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
@tool_cn_name("任务仅保存")
|
|
300
|
+
def task_save_only(
|
|
301
|
+
self,
|
|
302
|
+
*,
|
|
303
|
+
profile: str,
|
|
304
|
+
app_key: str,
|
|
305
|
+
record_id: int,
|
|
306
|
+
workflow_node_id: int,
|
|
307
|
+
fields: dict[str, Any],
|
|
308
|
+
) -> dict[str, Any]:
|
|
309
|
+
"""执行任务相关逻辑。"""
|
|
310
|
+
return self._delegate_task_action(
|
|
311
|
+
profile=profile,
|
|
312
|
+
app_key=app_key,
|
|
313
|
+
record_id=record_id,
|
|
314
|
+
action="save_only",
|
|
315
|
+
payload={},
|
|
316
|
+
fields=fields,
|
|
317
|
+
public_action="task_save_only",
|
|
318
|
+
workflow_node_id=workflow_node_id,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
@tool_cn_name("任务转交候选")
|
|
322
|
+
def task_transfer_candidates(
|
|
323
|
+
self,
|
|
324
|
+
*,
|
|
325
|
+
profile: str,
|
|
326
|
+
app_key: str,
|
|
327
|
+
record_id: int,
|
|
328
|
+
page_size: int = 20,
|
|
329
|
+
page_num: int = 1,
|
|
330
|
+
workflow_node_id: int = 0,
|
|
331
|
+
keyword: str | None = None,
|
|
332
|
+
) -> dict[str, Any]:
|
|
333
|
+
"""执行任务相关逻辑。"""
|
|
334
|
+
raw = self.record_transfer_candidates(
|
|
335
|
+
profile=profile,
|
|
336
|
+
app_key=app_key,
|
|
337
|
+
apply_id=record_id,
|
|
338
|
+
page_size=page_size,
|
|
339
|
+
page_num=page_num,
|
|
340
|
+
audit_node_id=workflow_node_id,
|
|
341
|
+
keyword=keyword,
|
|
342
|
+
)
|
|
343
|
+
original_items = _approval_page_items(raw.get("page"))
|
|
344
|
+
items = self._filter_self_transfer_candidates(profile=profile, items=original_items)
|
|
345
|
+
filtered_count = max(len(original_items) - len(items), 0)
|
|
346
|
+
return self._public_page_response(
|
|
347
|
+
raw,
|
|
348
|
+
items=items,
|
|
349
|
+
pagination={
|
|
350
|
+
"page": page_num,
|
|
351
|
+
"page_size": page_size,
|
|
352
|
+
"returned_items": len(items),
|
|
353
|
+
"page_amount": _approval_page_amount(raw.get("page")),
|
|
354
|
+
"reported_total": max(_approval_page_total(raw.get("page")) - filtered_count, 0),
|
|
355
|
+
},
|
|
356
|
+
selection={"app_key": app_key, "record_id": record_id, "workflow_node_id": workflow_node_id, "keyword": keyword},
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
@tool_cn_name("新增评论")
|
|
360
|
+
def record_comment_add(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
|
|
361
|
+
"""执行记录相关逻辑。"""
|
|
362
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
363
|
+
self._validate_comment_payload(payload)
|
|
364
|
+
|
|
365
|
+
def runner(session_profile, context):
|
|
366
|
+
result = self.backend.request("POST", context, f"/app/{app_key}/apply/{apply_id}/comment", json_body=payload)
|
|
367
|
+
return {
|
|
368
|
+
"profile": profile,
|
|
369
|
+
"ws_id": session_profile.selected_ws_id,
|
|
370
|
+
"app_key": app_key,
|
|
371
|
+
"apply_id": apply_id,
|
|
372
|
+
"result": result,
|
|
373
|
+
"request_route": self._request_route_payload(context),
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return self._run(profile, runner)
|
|
377
|
+
|
|
378
|
+
@tool_cn_name("评论列表")
|
|
379
|
+
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]:
|
|
380
|
+
"""执行记录相关逻辑。"""
|
|
381
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
382
|
+
|
|
383
|
+
def runner(session_profile, context):
|
|
384
|
+
params: dict[str, Any] = {"pageSize": page_size}
|
|
385
|
+
if list_type is not None:
|
|
386
|
+
params["listType"] = list_type
|
|
387
|
+
if page_num is not None:
|
|
388
|
+
params["pageNum"] = page_num
|
|
389
|
+
result = self.backend.request("GET", context, f"/app/{app_key}/apply/{apply_id}/comment", params=params)
|
|
390
|
+
return {
|
|
391
|
+
"profile": profile,
|
|
392
|
+
"ws_id": session_profile.selected_ws_id,
|
|
393
|
+
"app_key": app_key,
|
|
394
|
+
"apply_id": apply_id,
|
|
395
|
+
"list_type": list_type,
|
|
396
|
+
"list_type_label": get_record_list_type_label(list_type),
|
|
397
|
+
"page": result,
|
|
398
|
+
"request_route": self._request_route_payload(context),
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
raw = self._run(profile, runner)
|
|
402
|
+
items = _approval_page_items(raw.get("page"))
|
|
403
|
+
return self._public_page_response(
|
|
404
|
+
raw,
|
|
405
|
+
items=items,
|
|
406
|
+
pagination={
|
|
407
|
+
"page": page_num,
|
|
408
|
+
"page_size": page_size,
|
|
409
|
+
"returned_items": len(items),
|
|
410
|
+
"page_amount": _approval_page_amount(raw.get("page")),
|
|
411
|
+
"reported_total": _approval_page_total(raw.get("page")),
|
|
412
|
+
},
|
|
413
|
+
selection={"app_key": app_key, "record_id": apply_id, "list_type": list_type},
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
@tool_cn_name("评论提及候选")
|
|
417
|
+
def record_comment_mention_candidates(
|
|
418
|
+
self,
|
|
419
|
+
*,
|
|
420
|
+
profile: str,
|
|
421
|
+
app_key: str,
|
|
422
|
+
apply_id: int,
|
|
423
|
+
page_size: int = 20,
|
|
424
|
+
page_num: int = 1,
|
|
425
|
+
list_type: int | None = None,
|
|
426
|
+
keyword: str | None = None,
|
|
427
|
+
) -> dict[str, Any]:
|
|
428
|
+
"""执行记录相关逻辑。"""
|
|
429
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
430
|
+
|
|
431
|
+
def runner(session_profile, context):
|
|
432
|
+
params: dict[str, Any] = {"pageSize": page_size, "pageNum": page_num}
|
|
433
|
+
if list_type is not None:
|
|
434
|
+
params["listType"] = list_type
|
|
435
|
+
if keyword:
|
|
436
|
+
params["keyword"] = keyword
|
|
437
|
+
result = self.backend.request("GET", context, f"/app/{app_key}/apply/{apply_id}/comment/member", params=params)
|
|
438
|
+
return {
|
|
439
|
+
"profile": profile,
|
|
440
|
+
"ws_id": session_profile.selected_ws_id,
|
|
441
|
+
"app_key": app_key,
|
|
442
|
+
"apply_id": apply_id,
|
|
443
|
+
"list_type": list_type,
|
|
444
|
+
"list_type_label": get_record_list_type_label(list_type),
|
|
445
|
+
"page": result,
|
|
446
|
+
"request_route": self._request_route_payload(context),
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return self._run(profile, runner)
|
|
450
|
+
|
|
451
|
+
@tool_cn_name("评论标记已读")
|
|
452
|
+
def record_comment_mark_read(self, *, profile: str, app_key: str, apply_id: int) -> dict[str, Any]:
|
|
453
|
+
"""执行记录相关逻辑。"""
|
|
454
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
455
|
+
|
|
456
|
+
def runner(session_profile, context):
|
|
457
|
+
result = self.backend.request("POST", context, f"/app/{app_key}/apply/{apply_id}/comment/read")
|
|
458
|
+
return {
|
|
459
|
+
"profile": profile,
|
|
460
|
+
"ws_id": session_profile.selected_ws_id,
|
|
461
|
+
"app_key": app_key,
|
|
462
|
+
"apply_id": apply_id,
|
|
463
|
+
"result": result,
|
|
464
|
+
"request_route": self._request_route_payload(context),
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
raw = self._run(profile, runner)
|
|
468
|
+
return self._public_action_response(
|
|
469
|
+
raw,
|
|
470
|
+
action="record_comment_mark_read",
|
|
471
|
+
resource={"app_key": app_key, "record_id": apply_id},
|
|
472
|
+
selection={},
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
@tool_cn_name("评论统计")
|
|
476
|
+
def record_comment_stats(self, *, profile: str, app_key: str, apply_id: int) -> dict[str, Any]:
|
|
477
|
+
"""执行记录相关逻辑。"""
|
|
478
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
479
|
+
|
|
480
|
+
def runner(session_profile, context):
|
|
481
|
+
result = self.backend.request("GET", context, f"/app/{app_key}/apply/{apply_id}/comment/statistic")
|
|
482
|
+
return {
|
|
483
|
+
"profile": profile,
|
|
484
|
+
"ws_id": session_profile.selected_ws_id,
|
|
485
|
+
"app_key": app_key,
|
|
486
|
+
"apply_id": apply_id,
|
|
487
|
+
"result": result,
|
|
488
|
+
"request_route": self._request_route_payload(context),
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return self._run(profile, runner)
|
|
492
|
+
|
|
493
|
+
@tool_cn_name("记录通过")
|
|
494
|
+
def record_approve(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
|
|
495
|
+
"""执行记录相关逻辑。"""
|
|
496
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
497
|
+
body = self._require_dict(payload)
|
|
498
|
+
|
|
499
|
+
def runner(session_profile, context):
|
|
500
|
+
approval_body = self._normalize_approval_payload(profile, context, app_key, apply_id, body)
|
|
501
|
+
result = self.backend.request("POST", context, "/workflow/engine/approval/approve", json_body=approval_body)
|
|
502
|
+
return {
|
|
503
|
+
"profile": profile,
|
|
504
|
+
"ws_id": session_profile.selected_ws_id,
|
|
505
|
+
"app_key": app_key,
|
|
506
|
+
"apply_id": apply_id,
|
|
507
|
+
"form_id": approval_body["formId"],
|
|
508
|
+
"node_id": approval_body["nodeId"],
|
|
509
|
+
"result": result,
|
|
510
|
+
"request_route": self._request_route_payload(context),
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return self._run(profile, runner)
|
|
514
|
+
|
|
515
|
+
@tool_cn_name("记录拒绝")
|
|
516
|
+
def record_reject(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
|
|
517
|
+
"""执行记录相关逻辑。"""
|
|
518
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
519
|
+
body = self._require_dict(payload)
|
|
520
|
+
|
|
521
|
+
def runner(session_profile, context):
|
|
522
|
+
approval_body = self._normalize_approval_payload(profile, context, app_key, apply_id, body)
|
|
523
|
+
result = self.backend.request("POST", context, "/workflow/engine/approval/reject", json_body=approval_body)
|
|
524
|
+
return {
|
|
525
|
+
"profile": profile,
|
|
526
|
+
"ws_id": session_profile.selected_ws_id,
|
|
527
|
+
"app_key": app_key,
|
|
528
|
+
"apply_id": apply_id,
|
|
529
|
+
"form_id": approval_body["formId"],
|
|
530
|
+
"node_id": approval_body["nodeId"],
|
|
531
|
+
"result": result,
|
|
532
|
+
"request_route": self._request_route_payload(context),
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return self._run(profile, runner)
|
|
536
|
+
|
|
537
|
+
@tool_cn_name("记录退回候选")
|
|
538
|
+
def record_rollback_candidates(self, *, profile: str, app_key: str, apply_id: int, audit_node_id: int) -> dict[str, Any]:
|
|
539
|
+
"""执行记录相关逻辑。"""
|
|
540
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
541
|
+
if audit_node_id <= 0:
|
|
542
|
+
raise_tool_error(QingflowApiError.config_error("audit_node_id must be positive"))
|
|
543
|
+
|
|
544
|
+
def runner(session_profile, context):
|
|
545
|
+
result = self.backend.request(
|
|
546
|
+
"GET",
|
|
547
|
+
context,
|
|
548
|
+
f"/app/{app_key}/apply/{apply_id}/revertNode",
|
|
549
|
+
params={"auditNodeId": audit_node_id},
|
|
550
|
+
)
|
|
551
|
+
return {
|
|
552
|
+
"profile": profile,
|
|
553
|
+
"ws_id": session_profile.selected_ws_id,
|
|
554
|
+
"app_key": app_key,
|
|
555
|
+
"apply_id": apply_id,
|
|
556
|
+
"result": result,
|
|
557
|
+
"request_route": self._request_route_payload(context),
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return self._run(profile, runner)
|
|
561
|
+
|
|
562
|
+
@tool_cn_name("记录退回")
|
|
563
|
+
def record_rollback(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
|
|
564
|
+
"""执行记录相关逻辑。"""
|
|
565
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
566
|
+
body = self._require_dict(payload)
|
|
567
|
+
self._validate_audit_payload(body)
|
|
568
|
+
|
|
569
|
+
def runner(session_profile, context):
|
|
570
|
+
result = self.backend.request("POST", context, f"/app/{app_key}/apply/{apply_id}/rollback", json_body=body)
|
|
571
|
+
return {
|
|
572
|
+
"profile": profile,
|
|
573
|
+
"ws_id": session_profile.selected_ws_id,
|
|
574
|
+
"app_key": app_key,
|
|
575
|
+
"apply_id": apply_id,
|
|
576
|
+
"result": result,
|
|
577
|
+
"request_route": self._request_route_payload(context),
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return self._run(profile, runner)
|
|
581
|
+
|
|
582
|
+
@tool_cn_name("记录转交")
|
|
583
|
+
def record_transfer(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
|
|
584
|
+
"""执行记录相关逻辑。"""
|
|
585
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
586
|
+
body = self._require_dict(payload)
|
|
587
|
+
self._validate_audit_payload(body, require_uid=True)
|
|
588
|
+
|
|
589
|
+
def runner(session_profile, context):
|
|
590
|
+
result = self.backend.request("POST", context, f"/app/{app_key}/apply/{apply_id}/transfer", json_body=body)
|
|
591
|
+
return {
|
|
592
|
+
"profile": profile,
|
|
593
|
+
"ws_id": session_profile.selected_ws_id,
|
|
594
|
+
"app_key": app_key,
|
|
595
|
+
"apply_id": apply_id,
|
|
596
|
+
"result": result,
|
|
597
|
+
"request_route": self._request_route_payload(context),
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return self._run(profile, runner)
|
|
601
|
+
|
|
602
|
+
@tool_cn_name("记录转交候选")
|
|
603
|
+
def record_transfer_candidates(
|
|
604
|
+
self,
|
|
605
|
+
*,
|
|
606
|
+
profile: str,
|
|
607
|
+
app_key: str,
|
|
608
|
+
apply_id: int,
|
|
609
|
+
page_size: int = 20,
|
|
610
|
+
page_num: int = 1,
|
|
611
|
+
audit_node_id: int = 0,
|
|
612
|
+
keyword: str | None = None,
|
|
613
|
+
) -> dict[str, Any]:
|
|
614
|
+
"""执行记录相关逻辑。"""
|
|
615
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
616
|
+
if audit_node_id <= 0:
|
|
617
|
+
raise_tool_error(QingflowApiError.config_error("audit_node_id must be positive"))
|
|
618
|
+
|
|
619
|
+
def runner(session_profile, context):
|
|
620
|
+
params: dict[str, Any] = {"pageSize": page_size, "pageNum": page_num, "auditNodeId": audit_node_id}
|
|
621
|
+
if keyword:
|
|
622
|
+
params["keyword"] = keyword
|
|
623
|
+
result = self.backend.request("GET", context, f"/app/{app_key}/apply/{apply_id}/transfer/member", params=params)
|
|
624
|
+
return {
|
|
625
|
+
"profile": profile,
|
|
626
|
+
"ws_id": session_profile.selected_ws_id,
|
|
627
|
+
"app_key": app_key,
|
|
628
|
+
"apply_id": apply_id,
|
|
629
|
+
"page": result,
|
|
630
|
+
"request_route": self._request_route_payload(context),
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return self._run(profile, runner)
|
|
634
|
+
|
|
635
|
+
@tool_cn_name("记录改派信息")
|
|
636
|
+
def record_reassign_get(self, *, profile: str, app_key: str, apply_id: int) -> dict[str, Any]:
|
|
637
|
+
"""执行记录相关逻辑。"""
|
|
638
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
639
|
+
|
|
640
|
+
def runner(session_profile, context):
|
|
641
|
+
result = self.backend.request("GET", context, f"/app/{app_key}/apply/{apply_id}/reassign")
|
|
642
|
+
return {
|
|
643
|
+
"profile": profile,
|
|
644
|
+
"ws_id": session_profile.selected_ws_id,
|
|
645
|
+
"app_key": app_key,
|
|
646
|
+
"apply_id": apply_id,
|
|
647
|
+
"result": result,
|
|
648
|
+
"request_route": self._request_route_payload(context),
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return self._run(profile, runner)
|
|
652
|
+
|
|
653
|
+
@tool_cn_name("记录改派")
|
|
654
|
+
def record_reassign(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
|
|
655
|
+
"""执行记录相关逻辑。"""
|
|
656
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
657
|
+
body = self._require_dict(payload)
|
|
658
|
+
|
|
659
|
+
def runner(session_profile, context):
|
|
660
|
+
result = self.backend.request("POST", context, f"/app/{app_key}/apply/{apply_id}/reassign", json_body=body)
|
|
661
|
+
return {
|
|
662
|
+
"profile": profile,
|
|
663
|
+
"ws_id": session_profile.selected_ws_id,
|
|
664
|
+
"app_key": app_key,
|
|
665
|
+
"apply_id": apply_id,
|
|
666
|
+
"result": result,
|
|
667
|
+
"request_route": self._request_route_payload(context),
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return self._run(profile, runner)
|
|
671
|
+
|
|
672
|
+
@tool_cn_name("会签候选")
|
|
673
|
+
def record_countersign_candidates(
|
|
674
|
+
self,
|
|
675
|
+
*,
|
|
676
|
+
profile: str,
|
|
677
|
+
app_key: str,
|
|
678
|
+
apply_id: int,
|
|
679
|
+
page_size: int = 20,
|
|
680
|
+
page_num: int = 1,
|
|
681
|
+
audit_node_id: int = 0,
|
|
682
|
+
search_key: str | None = None,
|
|
683
|
+
) -> dict[str, Any]:
|
|
684
|
+
"""执行记录相关逻辑。"""
|
|
685
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
686
|
+
if audit_node_id <= 0:
|
|
687
|
+
raise_tool_error(QingflowApiError.config_error("audit_node_id must be positive"))
|
|
688
|
+
|
|
689
|
+
def runner(session_profile, context):
|
|
690
|
+
params: dict[str, Any] = {"pageSize": page_size, "pageNum": page_num, "auditNodeId": audit_node_id}
|
|
691
|
+
if search_key:
|
|
692
|
+
params["searchKey"] = search_key
|
|
693
|
+
result = self.backend.request("GET", context, f"/app/{app_key}/apply/{apply_id}/countersign/member", params=params)
|
|
694
|
+
return {
|
|
695
|
+
"profile": profile,
|
|
696
|
+
"ws_id": session_profile.selected_ws_id,
|
|
697
|
+
"app_key": app_key,
|
|
698
|
+
"apply_id": apply_id,
|
|
699
|
+
"page": result,
|
|
700
|
+
"request_route": self._request_route_payload(context),
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return self._run(profile, runner)
|
|
704
|
+
|
|
705
|
+
@tool_cn_name("会签")
|
|
706
|
+
def record_countersign(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
|
|
707
|
+
"""执行记录相关逻辑。"""
|
|
708
|
+
self._require_app_and_apply(app_key, apply_id)
|
|
709
|
+
body = self._require_dict(payload)
|
|
710
|
+
self._validate_countersign_payload(body)
|
|
711
|
+
|
|
712
|
+
def runner(session_profile, context):
|
|
713
|
+
result = self.backend.request("POST", context, f"/app/{app_key}/apply/{apply_id}/countersign", json_body=body)
|
|
714
|
+
return {
|
|
715
|
+
"profile": profile,
|
|
716
|
+
"ws_id": session_profile.selected_ws_id,
|
|
717
|
+
"app_key": app_key,
|
|
718
|
+
"apply_id": apply_id,
|
|
719
|
+
"result": result,
|
|
720
|
+
"request_route": self._request_route_payload(context),
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return self._run(profile, runner)
|
|
724
|
+
|
|
725
|
+
def _request_route_payload(self, context) -> JSONObject: # type: ignore[no-untyped-def]
|
|
726
|
+
"""执行内部辅助逻辑。"""
|
|
727
|
+
describe_route = getattr(self.backend, "describe_route", None)
|
|
728
|
+
if callable(describe_route):
|
|
729
|
+
payload = describe_route(context)
|
|
730
|
+
if isinstance(payload, dict):
|
|
731
|
+
return payload
|
|
732
|
+
return {
|
|
733
|
+
"base_url": getattr(context, "base_url", None),
|
|
734
|
+
"qf_version": getattr(context, "qf_version", None),
|
|
735
|
+
"qf_version_source": getattr(context, "qf_version_source", None) or ("context" if getattr(context, "qf_version", None) else "unknown"),
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
def _require_app_and_apply(self, app_key: str, apply_id: int) -> None:
|
|
739
|
+
"""执行内部辅助逻辑。"""
|
|
740
|
+
if not app_key:
|
|
741
|
+
raise_tool_error(QingflowApiError.config_error("app_key is required"))
|
|
742
|
+
if apply_id <= 0:
|
|
743
|
+
raise_tool_error(QingflowApiError.config_error("apply_id must be positive"))
|
|
744
|
+
|
|
745
|
+
def _validate_comment_payload(self, payload: dict[str, Any]) -> None:
|
|
746
|
+
"""执行内部辅助逻辑。"""
|
|
747
|
+
comment_detail = payload.get("commentDetail")
|
|
748
|
+
if not isinstance(comment_detail, dict):
|
|
749
|
+
raise_tool_error(QingflowApiError.config_error("payload.commentDetail must be an object"))
|
|
750
|
+
if not comment_detail.get("commentMsg"):
|
|
751
|
+
raise_tool_error(QingflowApiError.config_error("payload.commentDetail.commentMsg is required"))
|
|
752
|
+
|
|
753
|
+
def _normalize_approval_payload(self, profile: str, context, app_key: str, apply_id: int, payload: dict[str, Any]) -> JSONObject: # type: ignore[no-untyped-def]
|
|
754
|
+
"""执行内部辅助逻辑。"""
|
|
755
|
+
body: JSONObject = dict(payload)
|
|
756
|
+
self._normalize_alias(body, "auditFeedback", "audit_feedback")
|
|
757
|
+
self._normalize_alias(body, "uploadFileSize", "upload_file_size")
|
|
758
|
+
self._normalize_alias(body, "handSignImageUrl", "hand_sign_image_url")
|
|
759
|
+
self._normalize_alias(body, "beingSaveSignature", "being_save_signature")
|
|
760
|
+
self._normalize_alias(body, "applyId", "apply_id")
|
|
761
|
+
self._normalize_alias(body, "formId", "form_id")
|
|
762
|
+
|
|
763
|
+
node_id = self._extract_node_id(body)
|
|
764
|
+
body["nodeId"] = self._resolve_actionable_node_id(context, app_key, apply_id, node_id)
|
|
765
|
+
body["applyId"] = self._match_or_fill_int(body, field_name="applyId", expected_value=apply_id)
|
|
766
|
+
body["formId"] = self._resolve_form_id(profile, context, app_key, explicit_form_id=body.get("formId"))
|
|
767
|
+
if body.get("answers") is None:
|
|
768
|
+
body["answers"] = self._fetch_current_todo_answers(context, app_key, apply_id, body["nodeId"])
|
|
769
|
+
|
|
770
|
+
self._validate_approval_payload(body)
|
|
771
|
+
return body
|
|
772
|
+
|
|
773
|
+
def _extract_node_id(self, payload: JSONObject) -> int:
|
|
774
|
+
"""执行内部辅助逻辑。"""
|
|
775
|
+
node_id = payload.get("nodeId")
|
|
776
|
+
audit_node_id = payload.pop("auditNodeId", None)
|
|
777
|
+
if node_id is None:
|
|
778
|
+
node_id = audit_node_id
|
|
779
|
+
elif audit_node_id is not None and node_id != audit_node_id:
|
|
780
|
+
raise_tool_error(QingflowApiError.config_error("payload.nodeId and payload.auditNodeId must match when both are provided"))
|
|
781
|
+
if not isinstance(node_id, int) or node_id <= 0:
|
|
782
|
+
raise_tool_error(QingflowApiError.config_error("payload.nodeId or payload.auditNodeId must be a positive integer"))
|
|
783
|
+
return node_id
|
|
784
|
+
|
|
785
|
+
def _resolve_form_id(self, profile: str, context, app_key: str, *, explicit_form_id: Any | None) -> int: # type: ignore[no-untyped-def]
|
|
786
|
+
"""执行内部辅助逻辑。"""
|
|
787
|
+
if explicit_form_id is not None:
|
|
788
|
+
if not isinstance(explicit_form_id, int) or explicit_form_id <= 0:
|
|
789
|
+
raise_tool_error(QingflowApiError.config_error("payload.formId must be a positive integer"))
|
|
790
|
+
form_id = self._get_form_id(profile, context, app_key)
|
|
791
|
+
if form_id != explicit_form_id:
|
|
792
|
+
raise_tool_error(
|
|
793
|
+
QingflowApiError.config_error(
|
|
794
|
+
f"payload.formId={explicit_form_id} does not match app_key '{app_key}' formId={form_id}"
|
|
795
|
+
)
|
|
796
|
+
)
|
|
797
|
+
return explicit_form_id
|
|
798
|
+
return self._get_form_id(profile, context, app_key)
|
|
799
|
+
|
|
800
|
+
def _get_form_id(self, profile: str, context, app_key: str) -> int: # type: ignore[no-untyped-def]
|
|
801
|
+
"""执行内部辅助逻辑。"""
|
|
802
|
+
cache_key = f"{profile}:{app_key}"
|
|
803
|
+
cached = self._form_id_cache.get(cache_key)
|
|
804
|
+
if cached is not None:
|
|
805
|
+
return cached
|
|
806
|
+
result = self.backend.request("GET", context, f"/app/{app_key}/baseInfo")
|
|
807
|
+
form_id = result.get("formId") if isinstance(result, dict) else None
|
|
808
|
+
if not isinstance(form_id, int) or form_id <= 0:
|
|
809
|
+
raise_tool_error(QingflowApiError.config_error(f"cannot resolve formId for app_key '{app_key}'"))
|
|
810
|
+
self._form_id_cache[cache_key] = form_id
|
|
811
|
+
return form_id
|
|
812
|
+
|
|
813
|
+
def _match_or_fill_int(self, payload: JSONObject, *, field_name: str, expected_value: int) -> int:
|
|
814
|
+
"""执行内部辅助逻辑。"""
|
|
815
|
+
current = payload.get(field_name)
|
|
816
|
+
if current is None:
|
|
817
|
+
return expected_value
|
|
818
|
+
if not isinstance(current, int) or current <= 0:
|
|
819
|
+
raise_tool_error(QingflowApiError.config_error(f"payload.{field_name} must be a positive integer"))
|
|
820
|
+
if current != expected_value:
|
|
821
|
+
raise_tool_error(QingflowApiError.config_error(f"payload.{field_name}={current} does not match apply_id={expected_value}"))
|
|
822
|
+
return current
|
|
823
|
+
|
|
824
|
+
def _normalize_alias(self, payload: JSONObject, canonical_key: str, alias_key: str) -> None:
|
|
825
|
+
"""执行内部辅助逻辑。"""
|
|
826
|
+
alias_value = payload.pop(alias_key, None)
|
|
827
|
+
if canonical_key not in payload and alias_value is not None:
|
|
828
|
+
payload[canonical_key] = alias_value
|
|
829
|
+
elif alias_value is not None and payload.get(canonical_key) != alias_value:
|
|
830
|
+
raise_tool_error(QingflowApiError.config_error(f"payload.{canonical_key} and payload.{alias_key} must match when both are provided"))
|
|
831
|
+
|
|
832
|
+
def _resolve_actionable_node_id(self, context, app_key: str, apply_id: int, node_id: int) -> int: # type: ignore[no-untyped-def]
|
|
833
|
+
"""执行内部辅助逻辑。"""
|
|
834
|
+
infos = self.backend.request(
|
|
835
|
+
"GET",
|
|
836
|
+
context,
|
|
837
|
+
f"/app/{app_key}/apply/{apply_id}/auditInfo",
|
|
838
|
+
params={"type": 1},
|
|
839
|
+
)
|
|
840
|
+
if not isinstance(infos, list) or not infos:
|
|
841
|
+
raise_tool_error(
|
|
842
|
+
QingflowApiError.config_error(
|
|
843
|
+
f"apply_id={apply_id} is not currently actionable for the logged-in user in todo list"
|
|
844
|
+
)
|
|
845
|
+
)
|
|
846
|
+
actionable_node_ids = {
|
|
847
|
+
candidate
|
|
848
|
+
for item in infos
|
|
849
|
+
if isinstance(item, dict)
|
|
850
|
+
for candidate in (item.get("auditNodeId"), item.get("nodeId"))
|
|
851
|
+
if isinstance(candidate, int) and candidate > 0
|
|
852
|
+
}
|
|
853
|
+
if node_id not in actionable_node_ids:
|
|
854
|
+
raise_tool_error(
|
|
855
|
+
QingflowApiError.config_error(
|
|
856
|
+
f"payload.nodeId={node_id} is not an actionable todo node for apply_id={apply_id}"
|
|
857
|
+
)
|
|
858
|
+
)
|
|
859
|
+
return node_id
|
|
860
|
+
|
|
861
|
+
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]
|
|
862
|
+
"""执行内部辅助逻辑。"""
|
|
863
|
+
detail = self.backend.request(
|
|
864
|
+
"GET",
|
|
865
|
+
context,
|
|
866
|
+
f"/app/{app_key}/apply/{apply_id}",
|
|
867
|
+
params={"role": 3, "listType": 1, "auditNodeId": node_id},
|
|
868
|
+
)
|
|
869
|
+
answers = detail.get("answers") if isinstance(detail, dict) else None
|
|
870
|
+
if not isinstance(answers, list):
|
|
871
|
+
raise_tool_error(
|
|
872
|
+
QingflowApiError.config_error(
|
|
873
|
+
f"cannot resolve current answers for apply_id={apply_id} nodeId={node_id}"
|
|
874
|
+
)
|
|
875
|
+
)
|
|
876
|
+
normalized_answers: list[dict[str, Any]] = []
|
|
877
|
+
for item in answers:
|
|
878
|
+
if isinstance(item, dict):
|
|
879
|
+
normalized_answers.append(dict(item))
|
|
880
|
+
return normalized_answers
|
|
881
|
+
|
|
882
|
+
def _validate_approval_payload(self, payload: dict[str, Any]) -> None:
|
|
883
|
+
"""执行内部辅助逻辑。"""
|
|
884
|
+
self._reject_unsupported_fields(payload)
|
|
885
|
+
if not isinstance(payload.get("formId"), int) or payload["formId"] <= 0:
|
|
886
|
+
raise_tool_error(QingflowApiError.config_error("payload.formId must be a positive integer"))
|
|
887
|
+
if not isinstance(payload.get("applyId"), int) or payload["applyId"] <= 0:
|
|
888
|
+
raise_tool_error(QingflowApiError.config_error("payload.applyId must be a positive integer"))
|
|
889
|
+
if not isinstance(payload.get("nodeId"), int) or payload["nodeId"] <= 0:
|
|
890
|
+
raise_tool_error(QingflowApiError.config_error("payload.nodeId must be a positive integer"))
|
|
891
|
+
answers = payload.get("answers")
|
|
892
|
+
if answers is not None and not isinstance(answers, list):
|
|
893
|
+
raise_tool_error(QingflowApiError.config_error("payload.answers must be an array when provided"))
|
|
894
|
+
|
|
895
|
+
def _validate_audit_payload(self, payload: dict[str, Any], *, require_uid: bool = False) -> None:
|
|
896
|
+
"""执行内部辅助逻辑。"""
|
|
897
|
+
self._reject_unsupported_fields(payload)
|
|
898
|
+
if require_uid and not payload.get("uid"):
|
|
899
|
+
raise_tool_error(QingflowApiError.config_error("payload.uid is required"))
|
|
900
|
+
|
|
901
|
+
def _validate_countersign_payload(self, payload: dict[str, Any]) -> None:
|
|
902
|
+
"""执行内部辅助逻辑。"""
|
|
903
|
+
self._reject_unsupported_fields(payload)
|
|
904
|
+
members = payload.get("countersignMembers")
|
|
905
|
+
if not isinstance(members, list) or not members:
|
|
906
|
+
raise_tool_error(QingflowApiError.config_error("payload.countersignMembers must be a non-empty array"))
|
|
907
|
+
|
|
908
|
+
def _reject_unsupported_fields(self, payload: dict[str, Any]) -> None:
|
|
909
|
+
"""执行内部辅助逻辑。"""
|
|
910
|
+
if payload.get("handSignImageUrl"):
|
|
911
|
+
raise_tool_error(QingflowApiError.not_supported("NOT_SUPPORTED_IN_V1: handSignImageUrl is not supported"))
|
|
912
|
+
|
|
913
|
+
def _extract_transfer_target_uid(self, payload: dict[str, Any]) -> int | None:
|
|
914
|
+
"""执行内部辅助逻辑。"""
|
|
915
|
+
for key in ("uid", "target_member_id", "targetMemberId"):
|
|
916
|
+
value = payload.get(key)
|
|
917
|
+
if isinstance(value, int) and value > 0:
|
|
918
|
+
return value
|
|
919
|
+
return None
|
|
920
|
+
|
|
921
|
+
def _raise_if_self_transfer(self, *, profile: str, payload: dict[str, Any]) -> None:
|
|
922
|
+
"""执行内部辅助逻辑。"""
|
|
923
|
+
target_uid = self._extract_transfer_target_uid(payload)
|
|
924
|
+
if target_uid is None:
|
|
925
|
+
return
|
|
926
|
+
session_profile = self.sessions.get_profile(profile)
|
|
927
|
+
if session_profile is not None and target_uid == session_profile.uid:
|
|
928
|
+
raise_tool_error(
|
|
929
|
+
QingflowApiError.config_error(
|
|
930
|
+
"task transfer does not support transferring to the current user; choose another transfer member"
|
|
931
|
+
)
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
def _filter_self_transfer_candidates(self, *, profile: str, items: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
935
|
+
"""执行内部辅助逻辑。"""
|
|
936
|
+
session_profile = self.sessions.get_profile(profile)
|
|
937
|
+
if session_profile is None:
|
|
938
|
+
return items
|
|
939
|
+
current_uid = session_profile.uid
|
|
940
|
+
return [item for item in items if item.get("uid") != current_uid]
|
|
941
|
+
|
|
942
|
+
def _delegate_task_action(
|
|
943
|
+
self,
|
|
944
|
+
*,
|
|
945
|
+
profile: str,
|
|
946
|
+
app_key: str,
|
|
947
|
+
record_id: int,
|
|
948
|
+
action: str,
|
|
949
|
+
payload: dict[str, Any],
|
|
950
|
+
fields: dict[str, Any],
|
|
951
|
+
public_action: str,
|
|
952
|
+
workflow_node_id: int | None = None,
|
|
953
|
+
) -> dict[str, Any]:
|
|
954
|
+
"""执行内部辅助逻辑。"""
|
|
955
|
+
from .task_context_tools import TaskContextTools
|
|
956
|
+
|
|
957
|
+
node_id = workflow_node_id
|
|
958
|
+
if node_id is None:
|
|
959
|
+
node_payload = dict(payload or {})
|
|
960
|
+
node_id = self._extract_node_id(node_payload)
|
|
961
|
+
delegated = TaskContextTools(self.sessions, self.backend).task_action_execute(
|
|
962
|
+
profile=profile,
|
|
963
|
+
app_key=app_key,
|
|
964
|
+
record_id=record_id,
|
|
965
|
+
workflow_node_id=node_id,
|
|
966
|
+
action=action,
|
|
967
|
+
payload=payload or {},
|
|
968
|
+
fields=fields or {},
|
|
969
|
+
)
|
|
970
|
+
response = dict(delegated)
|
|
971
|
+
data = response.get("data")
|
|
972
|
+
if isinstance(data, dict):
|
|
973
|
+
payload_data = dict(data)
|
|
974
|
+
payload_data["action"] = public_action
|
|
975
|
+
response["data"] = payload_data
|
|
976
|
+
return response
|
|
977
|
+
|
|
978
|
+
def _public_page_response(
|
|
979
|
+
self,
|
|
980
|
+
raw: dict[str, Any],
|
|
981
|
+
*,
|
|
982
|
+
items: list[dict[str, Any]],
|
|
983
|
+
pagination: dict[str, Any],
|
|
984
|
+
selection: dict[str, Any],
|
|
985
|
+
) -> dict[str, Any]:
|
|
986
|
+
"""执行内部辅助逻辑。"""
|
|
987
|
+
response = dict(raw)
|
|
988
|
+
response["ok"] = bool(raw.get("ok", True))
|
|
989
|
+
response["warnings"] = []
|
|
990
|
+
response["output_profile"] = "normal"
|
|
991
|
+
response["data"] = {
|
|
992
|
+
"items": items,
|
|
993
|
+
"pagination": pagination,
|
|
994
|
+
"selection": selection,
|
|
995
|
+
}
|
|
996
|
+
return response
|
|
997
|
+
|
|
998
|
+
def _public_action_response(
|
|
999
|
+
self,
|
|
1000
|
+
raw: dict[str, Any],
|
|
1001
|
+
*,
|
|
1002
|
+
action: str,
|
|
1003
|
+
resource: dict[str, Any],
|
|
1004
|
+
selection: dict[str, Any],
|
|
1005
|
+
human_review: bool = False,
|
|
1006
|
+
) -> dict[str, Any]:
|
|
1007
|
+
"""执行内部辅助逻辑。"""
|
|
1008
|
+
response = dict(raw)
|
|
1009
|
+
response["ok"] = bool(raw.get("ok", True))
|
|
1010
|
+
response["warnings"] = []
|
|
1011
|
+
response["output_profile"] = "normal"
|
|
1012
|
+
response["data"] = {
|
|
1013
|
+
"action": action,
|
|
1014
|
+
"resource": resource,
|
|
1015
|
+
"selection": selection,
|
|
1016
|
+
"result": raw.get("result"),
|
|
1017
|
+
"human_review": human_review,
|
|
1018
|
+
}
|
|
1019
|
+
return response
|
|
1020
|
+
|
|
1021
|
+
def _request_route_payload(self, context) -> JSONObject: # type: ignore[no-untyped-def]
|
|
1022
|
+
"""执行内部辅助逻辑。"""
|
|
1023
|
+
describe_route = getattr(self.backend, "describe_route", None)
|
|
1024
|
+
if callable(describe_route):
|
|
1025
|
+
payload = describe_route(context)
|
|
1026
|
+
if isinstance(payload, dict):
|
|
1027
|
+
return payload
|
|
1028
|
+
return {
|
|
1029
|
+
"base_url": context.base_url,
|
|
1030
|
+
"qf_version": context.qf_version,
|
|
1031
|
+
"qf_version_source": getattr(context, "qf_version_source", None) or ("context" if getattr(context, "qf_version", None) else "unknown"),
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
def _approval_page_items(payload: Any) -> list[dict[str, Any]]:
|
|
1036
|
+
if isinstance(payload, list):
|
|
1037
|
+
return [item for item in payload if isinstance(item, dict)]
|
|
1038
|
+
if not isinstance(payload, dict):
|
|
1039
|
+
return []
|
|
1040
|
+
for key in ("list", "items", "rows", "result"):
|
|
1041
|
+
value = payload.get(key)
|
|
1042
|
+
if isinstance(value, list):
|
|
1043
|
+
return [item for item in value if isinstance(item, dict)]
|
|
1044
|
+
for container_key in ("data", "page"):
|
|
1045
|
+
nested = payload.get(container_key)
|
|
1046
|
+
if isinstance(nested, dict):
|
|
1047
|
+
nested_items = _approval_page_items(nested)
|
|
1048
|
+
if nested_items:
|
|
1049
|
+
return nested_items
|
|
1050
|
+
return []
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
def _approval_page_amount(payload: Any) -> Any:
|
|
1054
|
+
if isinstance(payload, dict):
|
|
1055
|
+
return payload.get("pageAmount", payload.get("page_amount"))
|
|
1056
|
+
return None
|
|
1057
|
+
|
|
1058
|
+
|
|
1059
|
+
def _approval_page_total(payload: Any) -> Any:
|
|
1060
|
+
if isinstance(payload, dict):
|
|
1061
|
+
return payload.get("total", payload.get("count"))
|
|
1062
|
+
return None
|