@qingflow-tech/qingflow-app-builder-mcp 1.0.2 → 1.0.4
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 +2 -2
- package/docs/local-agent-install.md +9 -3
- package/npm/lib/runtime.mjs +10 -3
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +88 -184
- package/skills/qingflow-app-builder/references/create-app.md +15 -34
- package/skills/qingflow-app-builder/references/gotchas.md +3 -3
- package/skills/qingflow-app-builder/references/solution-playbooks.md +1 -2
- package/skills/qingflow-app-builder/references/tool-selection.md +9 -10
- package/src/qingflow_mcp/__init__.py +33 -1
- package/src/qingflow_mcp/backend_client.py +109 -0
- package/src/qingflow_mcp/builder_facade/button_style_catalog.py +282 -0
- package/src/qingflow_mcp/builder_facade/models.py +58 -9
- package/src/qingflow_mcp/builder_facade/service.py +1711 -240
- package/src/qingflow_mcp/cli/commands/__init__.py +2 -1
- package/src/qingflow_mcp/cli/commands/app.py +47 -1
- package/src/qingflow_mcp/cli/commands/auth.py +63 -0
- package/src/qingflow_mcp/cli/commands/builder.py +11 -3
- package/src/qingflow_mcp/cli/commands/exports.py +111 -0
- package/src/qingflow_mcp/cli/commands/record.py +5 -5
- package/src/qingflow_mcp/cli/commands/task.py +701 -27
- package/src/qingflow_mcp/cli/commands/workspace.py +84 -0
- package/src/qingflow_mcp/cli/context.py +3 -0
- package/src/qingflow_mcp/cli/formatters.py +424 -50
- package/src/qingflow_mcp/cli/interaction.py +72 -0
- package/src/qingflow_mcp/cli/main.py +11 -1
- package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
- package/src/qingflow_mcp/cli/terminal_ui.py +218 -0
- package/src/qingflow_mcp/config.py +1 -1
- package/src/qingflow_mcp/errors.py +4 -4
- package/src/qingflow_mcp/export_store.py +14 -0
- package/src/qingflow_mcp/id_utils.py +49 -0
- package/src/qingflow_mcp/public_surface.py +16 -1
- package/src/qingflow_mcp/response_trim.py +394 -9
- package/src/qingflow_mcp/server.py +26 -0
- package/src/qingflow_mcp/server_app_builder.py +15 -1
- package/src/qingflow_mcp/server_app_user.py +113 -0
- package/src/qingflow_mcp/session_store.py +126 -21
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +2 -2
- package/src/qingflow_mcp/solution/executor.py +2 -2
- package/src/qingflow_mcp/tools/ai_builder_tools.py +107 -34
- package/src/qingflow_mcp/tools/app_tools.py +1 -0
- package/src/qingflow_mcp/tools/auth_tools.py +243 -9
- package/src/qingflow_mcp/tools/base.py +6 -2
- package/src/qingflow_mcp/tools/code_block_tools.py +2 -2
- package/src/qingflow_mcp/tools/custom_button_tools.py +0 -2
- package/src/qingflow_mcp/tools/export_tools.py +1565 -0
- package/src/qingflow_mcp/tools/import_tools.py +78 -4
- package/src/qingflow_mcp/tools/record_tools.py +551 -165
- package/src/qingflow_mcp/tools/resource_read_tools.py +154 -33
- package/src/qingflow_mcp/tools/task_context_tools.py +917 -141
- package/src/qingflow_mcp/tools/workspace_tools.py +141 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import re
|
|
4
5
|
from typing import Any
|
|
5
6
|
from uuid import uuid4
|
|
6
7
|
|
|
@@ -9,6 +10,7 @@ from mcp.server.fastmcp import FastMCP
|
|
|
9
10
|
from ..backend_client import BackendRequestContext
|
|
10
11
|
from ..config import DEFAULT_PROFILE
|
|
11
12
|
from ..errors import QingflowApiError, raise_tool_error
|
|
13
|
+
from ..id_utils import ids_equal, normalize_positive_id_int, normalize_positive_id_text, stringify_backend_id
|
|
12
14
|
from ..json_types import JSONObject
|
|
13
15
|
from .approval_tools import ApprovalTools, _approval_page_amount, _approval_page_items, _approval_page_total
|
|
14
16
|
from .base import ToolBase, tool_cn_name
|
|
@@ -52,10 +54,16 @@ class TaskContextTools(ToolBase):
|
|
|
52
54
|
self._task_tools = TaskTools(sessions, backend)
|
|
53
55
|
self._approval_tools = ApprovalTools(sessions, backend)
|
|
54
56
|
self._record_tools = RecordTools(sessions, backend)
|
|
57
|
+
self._app_name_cache: dict[str, str | None] = {}
|
|
55
58
|
|
|
56
59
|
def register(self, mcp: FastMCP) -> None:
|
|
57
60
|
"""注册当前工具到 MCP 服务。"""
|
|
58
|
-
@mcp.tool(
|
|
61
|
+
@mcp.tool(
|
|
62
|
+
description=(
|
|
63
|
+
"List workflow tasks. `query` first uses backend task search; if the backend returns zero rows, "
|
|
64
|
+
"public task_list falls back to local matching on app_name, workflow_node_name, app_key, and record_id."
|
|
65
|
+
)
|
|
66
|
+
)
|
|
59
67
|
def task_list(
|
|
60
68
|
profile: str = DEFAULT_PROFILE,
|
|
61
69
|
task_box: str = "todo",
|
|
@@ -80,14 +88,16 @@ class TaskContextTools(ToolBase):
|
|
|
80
88
|
@mcp.tool()
|
|
81
89
|
def task_get(
|
|
82
90
|
profile: str = DEFAULT_PROFILE,
|
|
91
|
+
task_id: str = "",
|
|
83
92
|
app_key: str = "",
|
|
84
|
-
record_id:
|
|
93
|
+
record_id: str = "",
|
|
85
94
|
workflow_node_id: int = 0,
|
|
86
95
|
include_candidates: bool = True,
|
|
87
96
|
include_associated_reports: bool = True,
|
|
88
97
|
) -> dict[str, Any]:
|
|
89
98
|
return self.task_get(
|
|
90
99
|
profile=profile,
|
|
100
|
+
task_id=task_id,
|
|
91
101
|
app_key=app_key,
|
|
92
102
|
record_id=record_id,
|
|
93
103
|
workflow_node_id=workflow_node_id,
|
|
@@ -98,8 +108,9 @@ class TaskContextTools(ToolBase):
|
|
|
98
108
|
@mcp.tool(description=self._high_risk_tool_description(operation="execute", target="workflow task action"))
|
|
99
109
|
def task_action_execute(
|
|
100
110
|
profile: str = DEFAULT_PROFILE,
|
|
111
|
+
task_id: str = "",
|
|
101
112
|
app_key: str = "",
|
|
102
|
-
record_id:
|
|
113
|
+
record_id: str = "",
|
|
103
114
|
workflow_node_id: int = 0,
|
|
104
115
|
action: str = "",
|
|
105
116
|
payload: dict[str, Any] | None = None,
|
|
@@ -107,6 +118,7 @@ class TaskContextTools(ToolBase):
|
|
|
107
118
|
) -> dict[str, Any]:
|
|
108
119
|
return self.task_action_execute(
|
|
109
120
|
profile=profile,
|
|
121
|
+
task_id=task_id,
|
|
110
122
|
app_key=app_key,
|
|
111
123
|
record_id=record_id,
|
|
112
124
|
workflow_node_id=workflow_node_id,
|
|
@@ -118,8 +130,9 @@ class TaskContextTools(ToolBase):
|
|
|
118
130
|
@mcp.tool()
|
|
119
131
|
def task_associated_report_detail_get(
|
|
120
132
|
profile: str = DEFAULT_PROFILE,
|
|
133
|
+
task_id: str = "",
|
|
121
134
|
app_key: str = "",
|
|
122
|
-
record_id:
|
|
135
|
+
record_id: str = "",
|
|
123
136
|
workflow_node_id: int = 0,
|
|
124
137
|
report_id: int = 0,
|
|
125
138
|
page: int = 1,
|
|
@@ -127,6 +140,7 @@ class TaskContextTools(ToolBase):
|
|
|
127
140
|
) -> dict[str, Any]:
|
|
128
141
|
return self.task_associated_report_detail_get(
|
|
129
142
|
profile=profile,
|
|
143
|
+
task_id=task_id,
|
|
130
144
|
app_key=app_key,
|
|
131
145
|
record_id=record_id,
|
|
132
146
|
workflow_node_id=workflow_node_id,
|
|
@@ -138,12 +152,14 @@ class TaskContextTools(ToolBase):
|
|
|
138
152
|
@mcp.tool()
|
|
139
153
|
def task_workflow_log_get(
|
|
140
154
|
profile: str = DEFAULT_PROFILE,
|
|
155
|
+
task_id: str = "",
|
|
141
156
|
app_key: str = "",
|
|
142
|
-
record_id:
|
|
157
|
+
record_id: str = "",
|
|
143
158
|
workflow_node_id: int = 0,
|
|
144
159
|
) -> dict[str, Any]:
|
|
145
160
|
return self.task_workflow_log_get(
|
|
146
161
|
profile=profile,
|
|
162
|
+
task_id=task_id,
|
|
147
163
|
app_key=app_key,
|
|
148
164
|
record_id=record_id,
|
|
149
165
|
workflow_node_id=workflow_node_id,
|
|
@@ -163,44 +179,63 @@ class TaskContextTools(ToolBase):
|
|
|
163
179
|
page_size: int,
|
|
164
180
|
) -> dict[str, Any]:
|
|
165
181
|
"""执行任务相关逻辑。"""
|
|
166
|
-
|
|
167
|
-
normalized_status = self._task_tools._flow_status_to_process_status(flow_status)
|
|
168
|
-
raw = self._task_tools.task_list(
|
|
182
|
+
response = self._list_normalized_task_items(
|
|
169
183
|
profile=profile,
|
|
170
|
-
|
|
171
|
-
|
|
184
|
+
task_box=task_box,
|
|
185
|
+
flow_status=flow_status,
|
|
172
186
|
app_key=app_key,
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
187
|
+
workflow_node_id=workflow_node_id,
|
|
188
|
+
query=query,
|
|
189
|
+
page=page,
|
|
176
190
|
page_size=page_size,
|
|
177
|
-
create_time_asc=None,
|
|
178
191
|
)
|
|
179
|
-
|
|
180
|
-
items = [
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
192
|
+
warnings: list[dict[str, Any]] = []
|
|
193
|
+
items = response["items"] if isinstance(response.get("items"), list) else []
|
|
194
|
+
page_amount = response.get("page_amount")
|
|
195
|
+
reported_total = response.get("reported_total")
|
|
196
|
+
if query and not items:
|
|
197
|
+
fallback = self._task_list_local_query_fallback(
|
|
198
|
+
profile=profile,
|
|
199
|
+
task_box=task_box,
|
|
200
|
+
flow_status=flow_status,
|
|
201
|
+
app_key=app_key,
|
|
202
|
+
workflow_node_id=workflow_node_id,
|
|
203
|
+
query=query,
|
|
204
|
+
page=page,
|
|
205
|
+
page_size=page_size,
|
|
206
|
+
)
|
|
207
|
+
if fallback is not None:
|
|
208
|
+
items = fallback["items"]
|
|
209
|
+
returned_items = len(items)
|
|
210
|
+
page_amount = fallback["page_amount"]
|
|
211
|
+
reported_total = fallback["reported_total"]
|
|
212
|
+
warnings.append(
|
|
213
|
+
{
|
|
214
|
+
"code": "TASK_LIST_QUERY_FALLBACK_APPLIED",
|
|
215
|
+
"message": (
|
|
216
|
+
"backend searchKey returned zero tasks; task_list fell back to local matching on "
|
|
217
|
+
"app_name, workflow_node_name, app_key, and record_id."
|
|
218
|
+
),
|
|
219
|
+
}
|
|
220
|
+
)
|
|
221
|
+
public_items = [self._public_task_item(item) for item in items]
|
|
185
222
|
return {
|
|
186
223
|
"profile": profile,
|
|
187
|
-
"ws_id": raw.get("ws_id"),
|
|
224
|
+
"ws_id": response.get("raw", {}).get("ws_id") if isinstance(response.get("raw"), dict) else None,
|
|
188
225
|
"ok": True,
|
|
189
|
-
"request_route": raw.get("request_route"),
|
|
190
|
-
"warnings":
|
|
226
|
+
"request_route": response.get("raw", {}).get("request_route") if isinstance(response.get("raw"), dict) else None,
|
|
227
|
+
"warnings": warnings,
|
|
191
228
|
"output_profile": "normal",
|
|
192
229
|
"data": {
|
|
193
|
-
"items":
|
|
230
|
+
"items": public_items,
|
|
194
231
|
"pagination": {
|
|
195
232
|
"page": page,
|
|
196
233
|
"page_size": page_size,
|
|
197
|
-
"returned_items": len(
|
|
198
|
-
"page_amount":
|
|
199
|
-
"reported_total":
|
|
234
|
+
"returned_items": len(public_items),
|
|
235
|
+
"page_amount": page_amount,
|
|
236
|
+
"reported_total": reported_total,
|
|
200
237
|
},
|
|
201
238
|
"selection": {
|
|
202
|
-
"task_box": task_box,
|
|
203
|
-
"flow_status": flow_status,
|
|
204
239
|
"app_key": app_key,
|
|
205
240
|
"workflow_node_id": workflow_node_id,
|
|
206
241
|
"query": query,
|
|
@@ -213,26 +248,44 @@ class TaskContextTools(ToolBase):
|
|
|
213
248
|
self,
|
|
214
249
|
*,
|
|
215
250
|
profile: str,
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
251
|
+
task_id: Any = None,
|
|
252
|
+
app_key: str = "",
|
|
253
|
+
record_id: Any = "",
|
|
254
|
+
workflow_node_id: int = 0,
|
|
255
|
+
include_candidates: bool = True,
|
|
256
|
+
include_associated_reports: bool = True,
|
|
221
257
|
) -> dict[str, Any]:
|
|
222
258
|
"""执行任务相关逻辑。"""
|
|
223
|
-
|
|
259
|
+
if task_id in (None, ""):
|
|
260
|
+
normalize_positive_id_int(record_id, field_name="record_id")
|
|
224
261
|
|
|
225
262
|
def runner(session_profile, context):
|
|
226
|
-
|
|
263
|
+
locator = self._resolve_task_locator_input(
|
|
227
264
|
profile=profile,
|
|
228
|
-
|
|
265
|
+
task_id=task_id,
|
|
229
266
|
app_key=app_key,
|
|
230
267
|
record_id=record_id,
|
|
231
268
|
workflow_node_id=workflow_node_id,
|
|
269
|
+
)
|
|
270
|
+
task_id_text = locator["task_id"]
|
|
271
|
+
resolved_app_key = str(locator["app_key"])
|
|
272
|
+
resolved_record_id = int(locator["record_id"])
|
|
273
|
+
resolved_workflow_node_id = int(locator["workflow_node_id"])
|
|
274
|
+
self._require_app_record_and_node(resolved_app_key, resolved_record_id, resolved_workflow_node_id)
|
|
275
|
+
data = self._build_task_context(
|
|
276
|
+
profile=profile,
|
|
277
|
+
context=context,
|
|
278
|
+
app_key=resolved_app_key,
|
|
279
|
+
record_id=resolved_record_id,
|
|
280
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
232
281
|
include_candidates=include_candidates,
|
|
233
282
|
include_associated_reports=include_associated_reports,
|
|
234
283
|
current_uid=session_profile.uid,
|
|
235
284
|
)
|
|
285
|
+
data = self._compact_task_get_context(data)
|
|
286
|
+
task_payload = data.get("task")
|
|
287
|
+
if isinstance(task_payload, dict) and task_id_text is not None:
|
|
288
|
+
task_payload["task_id"] = task_id_text
|
|
236
289
|
return {
|
|
237
290
|
"profile": profile,
|
|
238
291
|
"ws_id": session_profile.selected_ws_id,
|
|
@@ -251,7 +304,7 @@ class TaskContextTools(ToolBase):
|
|
|
251
304
|
*,
|
|
252
305
|
profile: str,
|
|
253
306
|
app_key: str,
|
|
254
|
-
record_id:
|
|
307
|
+
record_id: Any,
|
|
255
308
|
workflow_node_id: int,
|
|
256
309
|
fields: dict[str, Any] | None = None,
|
|
257
310
|
) -> dict[str, Any]:
|
|
@@ -274,15 +327,18 @@ class TaskContextTools(ToolBase):
|
|
|
274
327
|
self,
|
|
275
328
|
*,
|
|
276
329
|
profile: str,
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
330
|
+
task_id: Any = None,
|
|
331
|
+
app_key: str = "",
|
|
332
|
+
record_id: Any = "",
|
|
333
|
+
workflow_node_id: int = 0,
|
|
280
334
|
action: str,
|
|
281
335
|
payload: dict[str, Any],
|
|
282
336
|
fields: dict[str, Any] | None = None,
|
|
283
337
|
) -> dict[str, Any]:
|
|
284
338
|
"""执行任务相关逻辑。"""
|
|
285
|
-
|
|
339
|
+
if task_id in (None, ""):
|
|
340
|
+
normalize_positive_id_int(record_id, field_name="record_id")
|
|
341
|
+
|
|
286
342
|
normalized_action = (action or "").strip().lower()
|
|
287
343
|
if normalized_action not in {"approve", "reject", "rollback", "transfer", "urge", "save_only"}:
|
|
288
344
|
raise_tool_error(
|
|
@@ -300,13 +356,27 @@ class TaskContextTools(ToolBase):
|
|
|
300
356
|
)
|
|
301
357
|
|
|
302
358
|
def runner(session_profile, context):
|
|
359
|
+
locator = self._resolve_task_locator_input(
|
|
360
|
+
profile=profile,
|
|
361
|
+
task_id=task_id,
|
|
362
|
+
app_key=app_key,
|
|
363
|
+
record_id=record_id,
|
|
364
|
+
workflow_node_id=workflow_node_id,
|
|
365
|
+
)
|
|
366
|
+
task_id_text = locator["task_id"]
|
|
367
|
+
resolved_app_key = str(locator["app_key"])
|
|
368
|
+
resolved_record_id = int(locator["record_id"])
|
|
369
|
+
resolved_record_id_text = str(locator["record_id_text"] or "")
|
|
370
|
+
resolved_workflow_node_id = int(locator["workflow_node_id"])
|
|
371
|
+
record_id_text = resolved_record_id_text
|
|
372
|
+
self._require_app_record_and_node(resolved_app_key, resolved_record_id, resolved_workflow_node_id)
|
|
303
373
|
try:
|
|
304
374
|
task_context = self._build_task_context(
|
|
305
375
|
profile=profile,
|
|
306
376
|
context=context,
|
|
307
|
-
app_key=
|
|
308
|
-
record_id=
|
|
309
|
-
workflow_node_id=
|
|
377
|
+
app_key=resolved_app_key,
|
|
378
|
+
record_id=resolved_record_id,
|
|
379
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
310
380
|
include_candidates=False,
|
|
311
381
|
include_associated_reports=False,
|
|
312
382
|
current_uid=session_profile.uid,
|
|
@@ -317,12 +387,13 @@ class TaskContextTools(ToolBase):
|
|
|
317
387
|
profile=profile,
|
|
318
388
|
session_profile=session_profile,
|
|
319
389
|
context=context,
|
|
320
|
-
app_key=
|
|
321
|
-
record_id=
|
|
322
|
-
workflow_node_id=
|
|
390
|
+
app_key=resolved_app_key,
|
|
391
|
+
record_id=resolved_record_id,
|
|
392
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
323
393
|
action=normalized_action,
|
|
324
394
|
source_error=error,
|
|
325
395
|
before_apply_status=None,
|
|
396
|
+
task_id=task_id_text,
|
|
326
397
|
)
|
|
327
398
|
raise
|
|
328
399
|
if normalized_action == "save_only" and not field_updates:
|
|
@@ -351,7 +422,7 @@ class TaskContextTools(ToolBase):
|
|
|
351
422
|
raise_tool_error(QingflowApiError.config_error(message))
|
|
352
423
|
raise_tool_error(
|
|
353
424
|
QingflowApiError.config_error(
|
|
354
|
-
f"task action '{normalized_action}' is not currently available for app_key='{
|
|
425
|
+
f"task action '{normalized_action}' is not currently available for app_key='{resolved_app_key}' record_id={record_id_text} workflow_node_id={resolved_workflow_node_id}"
|
|
355
426
|
)
|
|
356
427
|
)
|
|
357
428
|
feedback_required_for = capabilities.get("action_constraints", {}).get("feedback_required_for") or []
|
|
@@ -372,9 +443,9 @@ class TaskContextTools(ToolBase):
|
|
|
372
443
|
prepared_fields = self._prepare_task_field_update(
|
|
373
444
|
profile=profile,
|
|
374
445
|
context=context,
|
|
375
|
-
app_key=
|
|
376
|
-
record_id=
|
|
377
|
-
workflow_node_id=
|
|
446
|
+
app_key=resolved_app_key,
|
|
447
|
+
record_id=resolved_record_id,
|
|
448
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
378
449
|
task_context=task_context,
|
|
379
450
|
fields=field_updates,
|
|
380
451
|
)
|
|
@@ -384,16 +455,16 @@ class TaskContextTools(ToolBase):
|
|
|
384
455
|
runtime_baseline = self._capture_task_runtime_baseline(
|
|
385
456
|
profile=profile,
|
|
386
457
|
context=context,
|
|
387
|
-
app_key=
|
|
388
|
-
record_id=
|
|
389
|
-
workflow_node_id=
|
|
458
|
+
app_key=resolved_app_key,
|
|
459
|
+
record_id=resolved_record_id,
|
|
460
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
390
461
|
)
|
|
391
462
|
try:
|
|
392
463
|
raw = self._execute_task_action(
|
|
393
464
|
profile=profile,
|
|
394
|
-
app_key=
|
|
395
|
-
record_id=
|
|
396
|
-
workflow_node_id=
|
|
465
|
+
app_key=resolved_app_key,
|
|
466
|
+
record_id=resolved_record_id,
|
|
467
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
397
468
|
normalized_action=normalized_action,
|
|
398
469
|
payload=body,
|
|
399
470
|
prepared_fields=prepared_fields,
|
|
@@ -404,21 +475,22 @@ class TaskContextTools(ToolBase):
|
|
|
404
475
|
profile=profile,
|
|
405
476
|
session_profile=session_profile,
|
|
406
477
|
context=context,
|
|
407
|
-
app_key=
|
|
408
|
-
record_id=
|
|
409
|
-
workflow_node_id=
|
|
478
|
+
app_key=resolved_app_key,
|
|
479
|
+
record_id=resolved_record_id,
|
|
480
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
410
481
|
action=normalized_action,
|
|
411
482
|
source_error=error,
|
|
412
483
|
before_apply_status=before_apply_status,
|
|
484
|
+
task_id=task_id_text,
|
|
413
485
|
)
|
|
414
486
|
raise
|
|
415
487
|
|
|
416
488
|
if normalized_action == "save_only":
|
|
417
489
|
verification, warnings = self._verify_task_save_only(
|
|
418
490
|
context=context,
|
|
419
|
-
app_key=
|
|
420
|
-
record_id=
|
|
421
|
-
workflow_node_id=
|
|
491
|
+
app_key=resolved_app_key,
|
|
492
|
+
record_id=resolved_record_id,
|
|
493
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
422
494
|
before_apply_status=before_apply_status,
|
|
423
495
|
expected_answers=((prepared_fields or {}).get("normalized_answers") or []),
|
|
424
496
|
task_context=task_context,
|
|
@@ -430,9 +502,9 @@ class TaskContextTools(ToolBase):
|
|
|
430
502
|
verification, warnings = self._verify_task_action_runtime(
|
|
431
503
|
profile=profile,
|
|
432
504
|
context=context,
|
|
433
|
-
app_key=
|
|
434
|
-
record_id=
|
|
435
|
-
workflow_node_id=
|
|
505
|
+
app_key=resolved_app_key,
|
|
506
|
+
record_id=resolved_record_id,
|
|
507
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
436
508
|
action=normalized_action,
|
|
437
509
|
before_apply_status=before_apply_status,
|
|
438
510
|
runtime_baseline=runtime_baseline,
|
|
@@ -440,7 +512,7 @@ class TaskContextTools(ToolBase):
|
|
|
440
512
|
runtime_verified = bool(verification.get("runtime_continuation_verified"))
|
|
441
513
|
status = "success" if runtime_verified else "partial_success"
|
|
442
514
|
error_code = None if runtime_verified else "WORKFLOW_CONTINUATION_UNVERIFIED"
|
|
443
|
-
|
|
515
|
+
result = {
|
|
444
516
|
"profile": raw.get("profile", profile),
|
|
445
517
|
"ws_id": raw.get("ws_id", session_profile.selected_ws_id),
|
|
446
518
|
"ok": bool(raw.get("ok", True)) and status != "failed",
|
|
@@ -453,9 +525,9 @@ class TaskContextTools(ToolBase):
|
|
|
453
525
|
"data": {
|
|
454
526
|
"action": normalized_action,
|
|
455
527
|
"resource": {
|
|
456
|
-
"app_key":
|
|
457
|
-
"record_id":
|
|
458
|
-
"workflow_node_id":
|
|
528
|
+
"app_key": resolved_app_key,
|
|
529
|
+
"record_id": record_id_text,
|
|
530
|
+
"workflow_node_id": resolved_workflow_node_id,
|
|
459
531
|
},
|
|
460
532
|
"selection": {"action": normalized_action},
|
|
461
533
|
"result": raw.get("result"),
|
|
@@ -463,6 +535,11 @@ class TaskContextTools(ToolBase):
|
|
|
463
535
|
"field_update_applied": bool(field_updates),
|
|
464
536
|
},
|
|
465
537
|
}
|
|
538
|
+
if task_id_text is not None:
|
|
539
|
+
resource = result["data"].get("resource")
|
|
540
|
+
if isinstance(resource, dict):
|
|
541
|
+
resource["task_id"] = task_id_text
|
|
542
|
+
return result
|
|
466
543
|
|
|
467
544
|
return self._run(profile, runner)
|
|
468
545
|
|
|
@@ -471,7 +548,7 @@ class TaskContextTools(ToolBase):
|
|
|
471
548
|
*,
|
|
472
549
|
profile: str,
|
|
473
550
|
app_key: str,
|
|
474
|
-
record_id:
|
|
551
|
+
record_id: Any,
|
|
475
552
|
workflow_node_id: int,
|
|
476
553
|
normalized_action: str,
|
|
477
554
|
payload: dict[str, Any],
|
|
@@ -635,12 +712,12 @@ class TaskContextTools(ToolBase):
|
|
|
635
712
|
todo_items = self._safe_task_list_items(profile=profile, task_box="todo", app_key=app_key)
|
|
636
713
|
initiated_items = self._safe_task_list_items(profile=profile, task_box="initiated", app_key=app_key)
|
|
637
714
|
downstream_todo_detected = any(
|
|
638
|
-
|
|
715
|
+
ids_equal(item.get("record_id"), record_id) and int(item.get("workflow_node_id") or 0) != workflow_node_id
|
|
639
716
|
for item in todo_items
|
|
640
717
|
if isinstance(item, dict)
|
|
641
718
|
)
|
|
642
719
|
initiated_visible = any(
|
|
643
|
-
|
|
720
|
+
ids_equal(item.get("record_id"), record_id)
|
|
644
721
|
for item in initiated_items
|
|
645
722
|
if isinstance(item, dict)
|
|
646
723
|
)
|
|
@@ -657,7 +734,7 @@ class TaskContextTools(ToolBase):
|
|
|
657
734
|
int(item.get("workflow_node_id") or 0)
|
|
658
735
|
for item in todo_items
|
|
659
736
|
if isinstance(item, dict)
|
|
660
|
-
and
|
|
737
|
+
and ids_equal(item.get("record_id"), record_id)
|
|
661
738
|
and int(item.get("workflow_node_id") or 0) != workflow_node_id
|
|
662
739
|
}
|
|
663
740
|
workflow_log_digest = self._workflow_log_digest(log_items)
|
|
@@ -675,6 +752,25 @@ class TaskContextTools(ToolBase):
|
|
|
675
752
|
or verification.get("downstream_todo_changed")
|
|
676
753
|
or verification.get("workflow_log_advanced")
|
|
677
754
|
)
|
|
755
|
+
record_state_error = verification.get("record_state_error")
|
|
756
|
+
runtime_consumed_after_action = bool(
|
|
757
|
+
runtime_verified
|
|
758
|
+
and isinstance(record_state_error, dict)
|
|
759
|
+
and record_state_error.get("backend_code") == 46001
|
|
760
|
+
)
|
|
761
|
+
if runtime_consumed_after_action:
|
|
762
|
+
verification["record_state_scope"] = "current_node_runtime"
|
|
763
|
+
verification["record_state_unavailable_reason"] = "runtime_consumed_after_action"
|
|
764
|
+
verification["record_state_unavailability_expected"] = True
|
|
765
|
+
warnings.append(
|
|
766
|
+
{
|
|
767
|
+
"code": "TASK_RUNTIME_CONSUMED_AFTER_ACTION",
|
|
768
|
+
"message": (
|
|
769
|
+
"the current workflow node runtime is no longer readable after the action (backend 46001), "
|
|
770
|
+
"which usually means the node has been consumed and the workflow has already continued."
|
|
771
|
+
),
|
|
772
|
+
}
|
|
773
|
+
)
|
|
678
774
|
verification["runtime_continuation_verified"] = runtime_verified
|
|
679
775
|
if not runtime_verified:
|
|
680
776
|
warnings.append(
|
|
@@ -843,7 +939,7 @@ class TaskContextTools(ToolBase):
|
|
|
843
939
|
int(item.get("workflow_node_id") or 0)
|
|
844
940
|
for item in todo_items
|
|
845
941
|
if isinstance(item, dict)
|
|
846
|
-
and
|
|
942
|
+
and ids_equal(item.get("record_id"), record_id)
|
|
847
943
|
and int(item.get("workflow_node_id") or 0) != workflow_node_id
|
|
848
944
|
}
|
|
849
945
|
)
|
|
@@ -861,8 +957,10 @@ class TaskContextTools(ToolBase):
|
|
|
861
957
|
action: str,
|
|
862
958
|
source_error: QingflowApiError,
|
|
863
959
|
before_apply_status: Any,
|
|
960
|
+
task_id: str | None = None,
|
|
864
961
|
) -> dict[str, Any]:
|
|
865
962
|
"""执行内部辅助逻辑。"""
|
|
963
|
+
record_id_text = stringify_backend_id(record_id)
|
|
866
964
|
verification, warnings = self._verify_task_action_runtime(
|
|
867
965
|
profile=profile,
|
|
868
966
|
context=context,
|
|
@@ -881,7 +979,7 @@ class TaskContextTools(ToolBase):
|
|
|
881
979
|
"message": "the task is no longer actionable in the current context; MCP found downstream workflow evidence and treats it as already processed by another actor.",
|
|
882
980
|
}
|
|
883
981
|
)
|
|
884
|
-
|
|
982
|
+
result = {
|
|
885
983
|
"profile": profile,
|
|
886
984
|
"ws_id": session_profile.selected_ws_id,
|
|
887
985
|
"ok": True,
|
|
@@ -895,7 +993,7 @@ class TaskContextTools(ToolBase):
|
|
|
895
993
|
"action": action,
|
|
896
994
|
"resource": {
|
|
897
995
|
"app_key": app_key,
|
|
898
|
-
"record_id":
|
|
996
|
+
"record_id": record_id_text,
|
|
899
997
|
"workflow_node_id": workflow_node_id,
|
|
900
998
|
},
|
|
901
999
|
"selection": {"action": action},
|
|
@@ -903,13 +1001,18 @@ class TaskContextTools(ToolBase):
|
|
|
903
1001
|
"human_review": True,
|
|
904
1002
|
},
|
|
905
1003
|
}
|
|
1004
|
+
if task_id is not None:
|
|
1005
|
+
resource = result["data"].get("resource")
|
|
1006
|
+
if isinstance(resource, dict):
|
|
1007
|
+
resource["task_id"] = task_id
|
|
1008
|
+
return result
|
|
906
1009
|
warnings.append(
|
|
907
1010
|
{
|
|
908
1011
|
"code": "TASK_CONTEXT_VISIBILITY_UNVERIFIED",
|
|
909
1012
|
"message": "the task is no longer actionable, and MCP could not verify from state or workflow logs whether it was already processed.",
|
|
910
1013
|
}
|
|
911
1014
|
)
|
|
912
|
-
|
|
1015
|
+
result = {
|
|
913
1016
|
"profile": profile,
|
|
914
1017
|
"ws_id": session_profile.selected_ws_id,
|
|
915
1018
|
"ok": False,
|
|
@@ -923,7 +1026,7 @@ class TaskContextTools(ToolBase):
|
|
|
923
1026
|
"action": action,
|
|
924
1027
|
"resource": {
|
|
925
1028
|
"app_key": app_key,
|
|
926
|
-
"record_id":
|
|
1029
|
+
"record_id": record_id_text,
|
|
927
1030
|
"workflow_node_id": workflow_node_id,
|
|
928
1031
|
},
|
|
929
1032
|
"selection": {"action": action},
|
|
@@ -936,11 +1039,16 @@ class TaskContextTools(ToolBase):
|
|
|
936
1039
|
},
|
|
937
1040
|
},
|
|
938
1041
|
}
|
|
1042
|
+
if task_id is not None:
|
|
1043
|
+
resource = result["data"].get("resource")
|
|
1044
|
+
if isinstance(resource, dict):
|
|
1045
|
+
resource["task_id"] = task_id
|
|
1046
|
+
return result
|
|
939
1047
|
|
|
940
1048
|
def _safe_task_list_items(self, *, profile: str, task_box: str, app_key: str) -> list[dict[str, Any]]:
|
|
941
1049
|
"""执行内部辅助逻辑。"""
|
|
942
1050
|
try:
|
|
943
|
-
response = self.
|
|
1051
|
+
response = self._list_normalized_task_items(
|
|
944
1052
|
profile=profile,
|
|
945
1053
|
task_box=task_box,
|
|
946
1054
|
flow_status="all",
|
|
@@ -952,38 +1060,260 @@ class TaskContextTools(ToolBase):
|
|
|
952
1060
|
)
|
|
953
1061
|
except QingflowApiError:
|
|
954
1062
|
return []
|
|
955
|
-
|
|
956
|
-
items = data.get("items") if isinstance(data, dict) else None
|
|
1063
|
+
items = response.get("items") if isinstance(response, dict) else None
|
|
957
1064
|
if not isinstance(items, list):
|
|
958
1065
|
return []
|
|
959
1066
|
return [item for item in items if isinstance(item, dict)]
|
|
960
1067
|
|
|
1068
|
+
def _list_normalized_task_items(
|
|
1069
|
+
self,
|
|
1070
|
+
*,
|
|
1071
|
+
profile: str,
|
|
1072
|
+
task_box: str,
|
|
1073
|
+
flow_status: str,
|
|
1074
|
+
app_key: str | None,
|
|
1075
|
+
workflow_node_id: int | None,
|
|
1076
|
+
query: str | None,
|
|
1077
|
+
page: int,
|
|
1078
|
+
page_size: int,
|
|
1079
|
+
) -> dict[str, Any]:
|
|
1080
|
+
normalized_type = self._task_tools._task_box_to_type(task_box)
|
|
1081
|
+
normalized_status = self._task_tools._flow_status_to_process_status(flow_status)
|
|
1082
|
+
raw = self._task_tools.task_list(
|
|
1083
|
+
profile=profile,
|
|
1084
|
+
type=normalized_type,
|
|
1085
|
+
process_status=normalized_status,
|
|
1086
|
+
app_key=app_key,
|
|
1087
|
+
node_id=workflow_node_id,
|
|
1088
|
+
search_key=query,
|
|
1089
|
+
page_num=page,
|
|
1090
|
+
page_size=page_size,
|
|
1091
|
+
create_time_asc=None,
|
|
1092
|
+
)
|
|
1093
|
+
task_page = raw.get("page", {})
|
|
1094
|
+
return {
|
|
1095
|
+
"raw": raw,
|
|
1096
|
+
"items": [self._normalize_task_item(item) for item in _task_page_items(task_page) if isinstance(item, dict)],
|
|
1097
|
+
"page_amount": _task_page_amount(task_page),
|
|
1098
|
+
"reported_total": _task_page_total(task_page),
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
def _task_list_local_query_fallback(
|
|
1102
|
+
self,
|
|
1103
|
+
*,
|
|
1104
|
+
profile: str,
|
|
1105
|
+
task_box: str,
|
|
1106
|
+
flow_status: str,
|
|
1107
|
+
app_key: str | None,
|
|
1108
|
+
workflow_node_id: int | None,
|
|
1109
|
+
query: str,
|
|
1110
|
+
page: int,
|
|
1111
|
+
page_size: int,
|
|
1112
|
+
) -> dict[str, Any] | None:
|
|
1113
|
+
scan_page_size = max(page_size, 100)
|
|
1114
|
+
scan_page = 1
|
|
1115
|
+
page_amount: int | None = None
|
|
1116
|
+
matched_items: list[dict[str, Any]] = []
|
|
1117
|
+
while True:
|
|
1118
|
+
response = self._list_normalized_task_items(
|
|
1119
|
+
profile=profile,
|
|
1120
|
+
task_box=task_box,
|
|
1121
|
+
flow_status=flow_status,
|
|
1122
|
+
app_key=app_key,
|
|
1123
|
+
workflow_node_id=workflow_node_id,
|
|
1124
|
+
query=None,
|
|
1125
|
+
page=scan_page,
|
|
1126
|
+
page_size=scan_page_size,
|
|
1127
|
+
)
|
|
1128
|
+
normalized_items = response.get("items") if isinstance(response.get("items"), list) else []
|
|
1129
|
+
matched_items.extend(item for item in normalized_items if self._task_item_matches_query(item, query))
|
|
1130
|
+
if page_amount is None:
|
|
1131
|
+
coerced_page_amount = _coerce_count(response.get("page_amount"))
|
|
1132
|
+
if coerced_page_amount is not None and coerced_page_amount > 0:
|
|
1133
|
+
page_amount = coerced_page_amount
|
|
1134
|
+
if page_amount is not None and scan_page >= page_amount:
|
|
1135
|
+
break
|
|
1136
|
+
if not normalized_items or len(normalized_items) < scan_page_size:
|
|
1137
|
+
break
|
|
1138
|
+
scan_page += 1
|
|
1139
|
+
if not matched_items:
|
|
1140
|
+
return None
|
|
1141
|
+
start = max(page - 1, 0) * page_size
|
|
1142
|
+
end = start + page_size
|
|
1143
|
+
matched_total = len(matched_items)
|
|
1144
|
+
matched_page_amount = (matched_total + page_size - 1) // page_size if page_size > 0 else 0
|
|
1145
|
+
return {
|
|
1146
|
+
"items": matched_items[start:end],
|
|
1147
|
+
"page_amount": matched_page_amount,
|
|
1148
|
+
"reported_total": matched_total,
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
def _resolve_task_locator_by_task_id(self, *, profile: str, task_id: Any) -> dict[str, Any]:
|
|
1152
|
+
task_id_text = normalize_positive_id_text(task_id, field_name="task_id")
|
|
1153
|
+
searched_task_boxes = ("todo", "initiated", "cc", "done")
|
|
1154
|
+
incomplete_task_boxes: list[str] = []
|
|
1155
|
+
page_size = 100
|
|
1156
|
+
for task_box in searched_task_boxes:
|
|
1157
|
+
page = 1
|
|
1158
|
+
page_amount: int | None = None
|
|
1159
|
+
while True:
|
|
1160
|
+
response = self._list_normalized_task_items(
|
|
1161
|
+
profile=profile,
|
|
1162
|
+
task_box=task_box,
|
|
1163
|
+
flow_status="all",
|
|
1164
|
+
app_key=None,
|
|
1165
|
+
workflow_node_id=None,
|
|
1166
|
+
query=None,
|
|
1167
|
+
page=page,
|
|
1168
|
+
page_size=page_size,
|
|
1169
|
+
)
|
|
1170
|
+
items = response.get("items") if isinstance(response.get("items"), list) else []
|
|
1171
|
+
for item in items:
|
|
1172
|
+
if not isinstance(item, dict) or not ids_equal(item.get("task_id"), task_id_text):
|
|
1173
|
+
continue
|
|
1174
|
+
app_key = str(item.get("app_key") or "").strip()
|
|
1175
|
+
record_id = stringify_backend_id(item.get("record_id"))
|
|
1176
|
+
workflow_node_id = int(item.get("workflow_node_id") or 0)
|
|
1177
|
+
if not app_key or record_id is None or workflow_node_id <= 0:
|
|
1178
|
+
incomplete_task_boxes.append(task_box)
|
|
1179
|
+
continue
|
|
1180
|
+
return {
|
|
1181
|
+
"task_id": task_id_text,
|
|
1182
|
+
"task_box": task_box,
|
|
1183
|
+
"app_key": app_key,
|
|
1184
|
+
"record_id": record_id,
|
|
1185
|
+
"workflow_node_id": workflow_node_id,
|
|
1186
|
+
}
|
|
1187
|
+
if page_amount is None:
|
|
1188
|
+
coerced_page_amount = _coerce_count(response.get("page_amount"))
|
|
1189
|
+
if coerced_page_amount is not None and coerced_page_amount > 0:
|
|
1190
|
+
page_amount = coerced_page_amount
|
|
1191
|
+
if page_amount is not None and page >= page_amount:
|
|
1192
|
+
break
|
|
1193
|
+
if not items or len(items) < page_size:
|
|
1194
|
+
break
|
|
1195
|
+
page += 1
|
|
1196
|
+
if incomplete_task_boxes:
|
|
1197
|
+
searched = ", ".join(incomplete_task_boxes)
|
|
1198
|
+
raise_tool_error(
|
|
1199
|
+
QingflowApiError.config_error(
|
|
1200
|
+
f"task_id={task_id_text} resolved to an incomplete task locator in task_box={searched}; please refresh the task list and retry"
|
|
1201
|
+
)
|
|
1202
|
+
)
|
|
1203
|
+
raise_tool_error(
|
|
1204
|
+
QingflowApiError.config_error(
|
|
1205
|
+
f"task_id={task_id_text} was not found in the current visible task boxes (todo, initiated, cc, done)"
|
|
1206
|
+
)
|
|
1207
|
+
)
|
|
1208
|
+
|
|
1209
|
+
def _resolve_task_locator_input(
|
|
1210
|
+
self,
|
|
1211
|
+
*,
|
|
1212
|
+
profile: str,
|
|
1213
|
+
task_id: Any = None,
|
|
1214
|
+
app_key: str = "",
|
|
1215
|
+
record_id: Any = "",
|
|
1216
|
+
workflow_node_id: int = 0,
|
|
1217
|
+
) -> dict[str, Any]:
|
|
1218
|
+
task_id_text = normalize_positive_id_text(task_id, field_name="task_id") if task_id not in (None, "") else None
|
|
1219
|
+
resolved_app_key = (app_key or "").strip()
|
|
1220
|
+
resolved_record_id: int
|
|
1221
|
+
resolved_workflow_node_id: int
|
|
1222
|
+
if task_id_text is not None:
|
|
1223
|
+
locator = self._resolve_task_locator_by_task_id(profile=profile, task_id=task_id_text)
|
|
1224
|
+
resolved_app_key = str(locator["app_key"])
|
|
1225
|
+
resolved_record_id = normalize_positive_id_int(locator["record_id"], field_name="record_id")
|
|
1226
|
+
resolved_workflow_node_id = int(locator["workflow_node_id"])
|
|
1227
|
+
explicit_app_key = (app_key or "").strip()
|
|
1228
|
+
if explicit_app_key and explicit_app_key != resolved_app_key:
|
|
1229
|
+
raise_tool_error(
|
|
1230
|
+
QingflowApiError.config_error(
|
|
1231
|
+
f"task_id={task_id_text} resolved to app_key='{resolved_app_key}', which does not match app_key='{explicit_app_key}'"
|
|
1232
|
+
)
|
|
1233
|
+
)
|
|
1234
|
+
if record_id not in (None, ""):
|
|
1235
|
+
explicit_record_id = normalize_positive_id_text(record_id, field_name="record_id")
|
|
1236
|
+
if explicit_record_id != stringify_backend_id(resolved_record_id):
|
|
1237
|
+
raise_tool_error(
|
|
1238
|
+
QingflowApiError.config_error(
|
|
1239
|
+
f"task_id={task_id_text} resolved to record_id={resolved_record_id}, which does not match record_id={explicit_record_id}"
|
|
1240
|
+
)
|
|
1241
|
+
)
|
|
1242
|
+
if workflow_node_id not in (None, 0) and int(workflow_node_id) != resolved_workflow_node_id:
|
|
1243
|
+
raise_tool_error(
|
|
1244
|
+
QingflowApiError.config_error(
|
|
1245
|
+
f"task_id={task_id_text} resolved to workflow_node_id={resolved_workflow_node_id}, which does not match workflow_node_id={workflow_node_id}"
|
|
1246
|
+
)
|
|
1247
|
+
)
|
|
1248
|
+
else:
|
|
1249
|
+
resolved_record_id = normalize_positive_id_int(record_id, field_name="record_id")
|
|
1250
|
+
resolved_workflow_node_id = int(workflow_node_id)
|
|
1251
|
+
return {
|
|
1252
|
+
"task_id": task_id_text,
|
|
1253
|
+
"app_key": resolved_app_key,
|
|
1254
|
+
"record_id": resolved_record_id,
|
|
1255
|
+
"record_id_text": stringify_backend_id(resolved_record_id),
|
|
1256
|
+
"workflow_node_id": resolved_workflow_node_id,
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
def _task_item_matches_query(self, item: dict[str, Any], query: str) -> bool:
|
|
1260
|
+
needle = str(query or "").strip().casefold()
|
|
1261
|
+
if not needle:
|
|
1262
|
+
return False
|
|
1263
|
+
for candidate in (
|
|
1264
|
+
item.get("app_name"),
|
|
1265
|
+
item.get("workflow_node_name"),
|
|
1266
|
+
item.get("app_key"),
|
|
1267
|
+
item.get("record_id"),
|
|
1268
|
+
):
|
|
1269
|
+
if candidate in (None, ""):
|
|
1270
|
+
continue
|
|
1271
|
+
if needle in str(candidate).casefold():
|
|
1272
|
+
return True
|
|
1273
|
+
return False
|
|
1274
|
+
|
|
961
1275
|
@tool_cn_name("任务关联报表详情")
|
|
962
1276
|
def task_associated_report_detail_get(
|
|
963
1277
|
self,
|
|
964
1278
|
*,
|
|
965
1279
|
profile: str,
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1280
|
+
task_id: Any = None,
|
|
1281
|
+
app_key: str = "",
|
|
1282
|
+
record_id: Any = "",
|
|
1283
|
+
workflow_node_id: int = 0,
|
|
969
1284
|
report_id: int,
|
|
970
1285
|
page: int,
|
|
971
1286
|
page_size: int,
|
|
972
1287
|
) -> dict[str, Any]:
|
|
973
1288
|
"""执行任务相关逻辑。"""
|
|
974
|
-
|
|
1289
|
+
if task_id in (None, ""):
|
|
1290
|
+
normalize_positive_id_int(record_id, field_name="record_id")
|
|
1291
|
+
|
|
975
1292
|
if report_id <= 0:
|
|
976
1293
|
raise_tool_error(QingflowApiError.config_error("report_id must be positive"))
|
|
977
1294
|
if page <= 0 or page_size <= 0:
|
|
978
1295
|
raise_tool_error(QingflowApiError.config_error("page and page_size must be positive"))
|
|
979
1296
|
|
|
980
1297
|
def runner(session_profile, context):
|
|
981
|
-
|
|
1298
|
+
locator = self._resolve_task_locator_input(
|
|
982
1299
|
profile=profile,
|
|
983
|
-
|
|
1300
|
+
task_id=task_id,
|
|
984
1301
|
app_key=app_key,
|
|
985
1302
|
record_id=record_id,
|
|
986
1303
|
workflow_node_id=workflow_node_id,
|
|
1304
|
+
)
|
|
1305
|
+
task_id_text = locator["task_id"]
|
|
1306
|
+
resolved_app_key = str(locator["app_key"])
|
|
1307
|
+
resolved_record_id = int(locator["record_id"])
|
|
1308
|
+
record_id_text = str(locator["record_id_text"] or "")
|
|
1309
|
+
resolved_workflow_node_id = int(locator["workflow_node_id"])
|
|
1310
|
+
self._require_app_record_and_node(resolved_app_key, resolved_record_id, resolved_workflow_node_id)
|
|
1311
|
+
task_context = self._build_task_context(
|
|
1312
|
+
profile=profile,
|
|
1313
|
+
context=context,
|
|
1314
|
+
app_key=resolved_app_key,
|
|
1315
|
+
record_id=resolved_record_id,
|
|
1316
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
987
1317
|
include_candidates=False,
|
|
988
1318
|
include_associated_reports=True,
|
|
989
1319
|
current_uid=session_profile.uid,
|
|
@@ -992,7 +1322,7 @@ class TaskContextTools(ToolBase):
|
|
|
992
1322
|
if report_item is None:
|
|
993
1323
|
raise_tool_error(
|
|
994
1324
|
QingflowApiError.config_error(
|
|
995
|
-
f"report_id={report_id} is not visible for app_key='{
|
|
1325
|
+
f"report_id={report_id} is not visible for app_key='{resolved_app_key}' record_id={record_id_text} workflow_node_id={resolved_workflow_node_id}"
|
|
996
1326
|
)
|
|
997
1327
|
)
|
|
998
1328
|
association_query = self._build_association_query(
|
|
@@ -1000,15 +1330,17 @@ class TaskContextTools(ToolBase):
|
|
|
1000
1330
|
task_context.get("record", {}).get("answers") or [],
|
|
1001
1331
|
)
|
|
1002
1332
|
selection = {
|
|
1003
|
-
"app_key":
|
|
1004
|
-
"record_id":
|
|
1005
|
-
"workflow_node_id":
|
|
1333
|
+
"app_key": resolved_app_key,
|
|
1334
|
+
"record_id": record_id_text,
|
|
1335
|
+
"workflow_node_id": resolved_workflow_node_id,
|
|
1006
1336
|
"report_id": report_id,
|
|
1007
1337
|
"target_app_key": report_item.get("target_app_key"),
|
|
1008
1338
|
"target_app_name": report_item.get("target_app_name"),
|
|
1009
1339
|
"chart_key": report_item.get("chart_key"),
|
|
1010
1340
|
"chart_name": report_item.get("chart_name"),
|
|
1011
1341
|
}
|
|
1342
|
+
if task_id_text is not None:
|
|
1343
|
+
selection["task_id"] = task_id_text
|
|
1012
1344
|
context_payload = {
|
|
1013
1345
|
"match_rules": report_item.get("match_rules") or [],
|
|
1014
1346
|
"resolved_filters": association_query.get("keyQueValues") or [],
|
|
@@ -1155,20 +1487,35 @@ class TaskContextTools(ToolBase):
|
|
|
1155
1487
|
self,
|
|
1156
1488
|
*,
|
|
1157
1489
|
profile: str,
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1490
|
+
task_id: Any = None,
|
|
1491
|
+
app_key: str = "",
|
|
1492
|
+
record_id: Any = "",
|
|
1493
|
+
workflow_node_id: int = 0,
|
|
1161
1494
|
) -> dict[str, Any]:
|
|
1162
1495
|
"""执行任务相关逻辑。"""
|
|
1163
|
-
|
|
1496
|
+
if task_id in (None, ""):
|
|
1497
|
+
normalize_positive_id_int(record_id, field_name="record_id")
|
|
1164
1498
|
|
|
1165
1499
|
def runner(session_profile, context):
|
|
1166
|
-
|
|
1500
|
+
locator = self._resolve_task_locator_input(
|
|
1167
1501
|
profile=profile,
|
|
1168
|
-
|
|
1502
|
+
task_id=task_id,
|
|
1169
1503
|
app_key=app_key,
|
|
1170
1504
|
record_id=record_id,
|
|
1171
1505
|
workflow_node_id=workflow_node_id,
|
|
1506
|
+
)
|
|
1507
|
+
task_id_text = locator["task_id"]
|
|
1508
|
+
resolved_app_key = str(locator["app_key"])
|
|
1509
|
+
resolved_record_id = int(locator["record_id"])
|
|
1510
|
+
record_id_text = str(locator["record_id_text"] or "")
|
|
1511
|
+
resolved_workflow_node_id = int(locator["workflow_node_id"])
|
|
1512
|
+
self._require_app_record_and_node(resolved_app_key, resolved_record_id, resolved_workflow_node_id)
|
|
1513
|
+
task_context = self._build_task_context(
|
|
1514
|
+
profile=profile,
|
|
1515
|
+
context=context,
|
|
1516
|
+
app_key=resolved_app_key,
|
|
1517
|
+
record_id=resolved_record_id,
|
|
1518
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
1172
1519
|
include_candidates=False,
|
|
1173
1520
|
include_associated_reports=False,
|
|
1174
1521
|
current_uid=session_profile.uid,
|
|
@@ -1177,7 +1524,7 @@ class TaskContextTools(ToolBase):
|
|
|
1177
1524
|
if not visibility.get("audit_record_visible"):
|
|
1178
1525
|
raise_tool_error(
|
|
1179
1526
|
QingflowApiError.config_error(
|
|
1180
|
-
f"workflow logs are not visible for app_key='{
|
|
1527
|
+
f"workflow logs are not visible for app_key='{resolved_app_key}' record_id={record_id_text} workflow_node_id={resolved_workflow_node_id}"
|
|
1181
1528
|
)
|
|
1182
1529
|
)
|
|
1183
1530
|
page = self.backend.request(
|
|
@@ -1185,16 +1532,16 @@ class TaskContextTools(ToolBase):
|
|
|
1185
1532
|
context,
|
|
1186
1533
|
"/application/workflow/node/record",
|
|
1187
1534
|
json_body={
|
|
1188
|
-
"key":
|
|
1189
|
-
"rowRecordId":
|
|
1190
|
-
"nodeId":
|
|
1535
|
+
"key": resolved_app_key,
|
|
1536
|
+
"rowRecordId": resolved_record_id,
|
|
1537
|
+
"nodeId": resolved_workflow_node_id,
|
|
1191
1538
|
"role": 3,
|
|
1192
1539
|
"pageNum": 1,
|
|
1193
1540
|
"pageSize": 200,
|
|
1194
1541
|
},
|
|
1195
1542
|
)
|
|
1196
1543
|
items = self._normalize_workflow_logs(page)
|
|
1197
|
-
|
|
1544
|
+
result = {
|
|
1198
1545
|
"profile": profile,
|
|
1199
1546
|
"ws_id": session_profile.selected_ws_id,
|
|
1200
1547
|
"ok": True,
|
|
@@ -1203,9 +1550,9 @@ class TaskContextTools(ToolBase):
|
|
|
1203
1550
|
"output_profile": "normal",
|
|
1204
1551
|
"data": {
|
|
1205
1552
|
"selection": {
|
|
1206
|
-
"app_key":
|
|
1207
|
-
"record_id":
|
|
1208
|
-
"workflow_node_id":
|
|
1553
|
+
"app_key": resolved_app_key,
|
|
1554
|
+
"record_id": record_id_text,
|
|
1555
|
+
"workflow_node_id": resolved_workflow_node_id,
|
|
1209
1556
|
},
|
|
1210
1557
|
"visibility": {
|
|
1211
1558
|
"audit_record_visible": visibility.get("audit_record_visible"),
|
|
@@ -1214,6 +1561,11 @@ class TaskContextTools(ToolBase):
|
|
|
1214
1561
|
"items": items,
|
|
1215
1562
|
},
|
|
1216
1563
|
}
|
|
1564
|
+
if task_id_text is not None:
|
|
1565
|
+
selection = result["data"].get("selection")
|
|
1566
|
+
if isinstance(selection, dict):
|
|
1567
|
+
selection["task_id"] = task_id_text
|
|
1568
|
+
return result
|
|
1217
1569
|
|
|
1218
1570
|
return self._run(profile, runner)
|
|
1219
1571
|
|
|
@@ -1243,8 +1595,9 @@ class TaskContextTools(ToolBase):
|
|
|
1243
1595
|
f"/app/{app_key}/apply/{record_id}",
|
|
1244
1596
|
params={"role": 3, "listType": 1, "auditNodeId": workflow_node_id},
|
|
1245
1597
|
)
|
|
1598
|
+
app_name = self._task_app_name(context=context, app_key=app_key, detail=detail, node_info=node_info)
|
|
1246
1599
|
associated_report_visible = self._resolve_associated_report_visible(node_info, detail)
|
|
1247
|
-
associated_reports = {"visible": associated_report_visible, "count": 0, "items": []}
|
|
1600
|
+
associated_reports = {"visible": associated_report_visible, "loaded": False, "count": 0, "items": []}
|
|
1248
1601
|
if include_associated_reports and associated_report_visible:
|
|
1249
1602
|
asos_chart_list = self.backend.request(
|
|
1250
1603
|
"GET",
|
|
@@ -1259,11 +1612,21 @@ class TaskContextTools(ToolBase):
|
|
|
1259
1612
|
]
|
|
1260
1613
|
associated_reports = {
|
|
1261
1614
|
"visible": True,
|
|
1615
|
+
"loaded": True,
|
|
1262
1616
|
"count": len(associated_items),
|
|
1263
1617
|
"items": associated_items,
|
|
1264
1618
|
}
|
|
1265
1619
|
rollback_items: list[dict[str, Any]] = []
|
|
1266
1620
|
transfer_items: list[dict[str, Any]] = []
|
|
1621
|
+
transfer_warnings: list[JSONObject] = []
|
|
1622
|
+
transfer_pagination: JSONObject = {
|
|
1623
|
+
"loaded": False,
|
|
1624
|
+
"page_size": 100,
|
|
1625
|
+
"fetched_pages": 0,
|
|
1626
|
+
"reported_total": None,
|
|
1627
|
+
"page_amount": None,
|
|
1628
|
+
"truncated": False,
|
|
1629
|
+
}
|
|
1267
1630
|
if include_candidates:
|
|
1268
1631
|
rollback_result = self.backend.request(
|
|
1269
1632
|
"GET",
|
|
@@ -1272,13 +1635,13 @@ class TaskContextTools(ToolBase):
|
|
|
1272
1635
|
params={"auditNodeId": workflow_node_id},
|
|
1273
1636
|
)
|
|
1274
1637
|
rollback_items = self._rollback_candidate_items(rollback_result)
|
|
1275
|
-
|
|
1276
|
-
"GET",
|
|
1638
|
+
transfer_items, transfer_warnings, transfer_pagination = self._transfer_candidate_items(
|
|
1277
1639
|
context,
|
|
1278
|
-
|
|
1279
|
-
|
|
1640
|
+
app_key=app_key,
|
|
1641
|
+
record_id=record_id,
|
|
1642
|
+
workflow_node_id=workflow_node_id,
|
|
1643
|
+
current_uid=current_uid,
|
|
1280
1644
|
)
|
|
1281
|
-
transfer_items = self._filter_transfer_members(_approval_page_items(transfer_result), current_uid=current_uid)
|
|
1282
1645
|
|
|
1283
1646
|
update_schema_state = self._build_task_update_schema(
|
|
1284
1647
|
profile=profile,
|
|
@@ -1302,10 +1665,12 @@ class TaskContextTools(ToolBase):
|
|
|
1302
1665
|
save_only_source=save_only_source,
|
|
1303
1666
|
)
|
|
1304
1667
|
visibility = self._build_visibility(node_info, detail)
|
|
1668
|
+
record_id_text = stringify_backend_id(record_id)
|
|
1305
1669
|
return {
|
|
1306
1670
|
"task": {
|
|
1307
1671
|
"app_key": app_key,
|
|
1308
|
-
"
|
|
1672
|
+
"app_name": app_name,
|
|
1673
|
+
"record_id": record_id_text,
|
|
1309
1674
|
"workflow_node_id": workflow_node_id,
|
|
1310
1675
|
"workflow_node_name": node_info.get("auditNodeName") or node_info.get("nodeName"),
|
|
1311
1676
|
"actionable": True,
|
|
@@ -1316,7 +1681,7 @@ class TaskContextTools(ToolBase):
|
|
|
1316
1681
|
"raw": dict(node_info),
|
|
1317
1682
|
},
|
|
1318
1683
|
"record": {
|
|
1319
|
-
"apply_id": detail.get("applyId"
|
|
1684
|
+
"apply_id": stringify_backend_id(detail.get("applyId") or record_id),
|
|
1320
1685
|
"apply_status": detail.get("applyStatus"),
|
|
1321
1686
|
"apply_num": detail.get("applyNum"),
|
|
1322
1687
|
"custom_apply_num": detail.get("customApplyNum"),
|
|
@@ -1336,6 +1701,9 @@ class TaskContextTools(ToolBase):
|
|
|
1336
1701
|
"candidates": {
|
|
1337
1702
|
"rollback_nodes": rollback_items,
|
|
1338
1703
|
"transfer_members": transfer_items,
|
|
1704
|
+
"loaded": include_candidates,
|
|
1705
|
+
"transfer_pagination": transfer_pagination,
|
|
1706
|
+
"warnings": transfer_warnings,
|
|
1339
1707
|
},
|
|
1340
1708
|
"workflow_log_summary": {
|
|
1341
1709
|
"visible": visibility["audit_record_visible"],
|
|
@@ -1346,32 +1714,365 @@ class TaskContextTools(ToolBase):
|
|
|
1346
1714
|
"update_schema": update_schema,
|
|
1347
1715
|
}
|
|
1348
1716
|
|
|
1349
|
-
def
|
|
1717
|
+
def _compact_task_get_context(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
1718
|
+
task = data.get("task") if isinstance(data.get("task"), dict) else {}
|
|
1719
|
+
record = data.get("record") if isinstance(data.get("record"), dict) else {}
|
|
1720
|
+
capabilities = data.get("capabilities") if isinstance(data.get("capabilities"), dict) else {}
|
|
1721
|
+
update_schema = data.get("update_schema") if isinstance(data.get("update_schema"), dict) else {}
|
|
1722
|
+
associated_reports = data.get("associated_reports") if isinstance(data.get("associated_reports"), dict) else {}
|
|
1723
|
+
candidates = data.get("candidates") if isinstance(data.get("candidates"), dict) else {}
|
|
1724
|
+
workflow_log = data.get("workflow_log_summary") if isinstance(data.get("workflow_log_summary"), dict) else {}
|
|
1725
|
+
|
|
1726
|
+
available_actions = [
|
|
1727
|
+
str(item)
|
|
1728
|
+
for item in (capabilities.get("available_actions") or [])
|
|
1729
|
+
if str(item).strip()
|
|
1730
|
+
]
|
|
1731
|
+
writable_fields = update_schema.get("writable_fields") if isinstance(update_schema.get("writable_fields"), list) else []
|
|
1732
|
+
rollback_items = [
|
|
1733
|
+
self._compact_rollback_candidate(item)
|
|
1734
|
+
for item in (candidates.get("rollback_nodes") or [])
|
|
1735
|
+
if isinstance(item, dict)
|
|
1736
|
+
]
|
|
1737
|
+
transfer_items = [
|
|
1738
|
+
self._compact_transfer_member(item)
|
|
1739
|
+
for item in (candidates.get("transfer_members") or [])
|
|
1740
|
+
if isinstance(item, dict)
|
|
1741
|
+
]
|
|
1742
|
+
associated_items = [
|
|
1743
|
+
self._compact_associated_report(item)
|
|
1744
|
+
for item in (associated_reports.get("items") or [])
|
|
1745
|
+
if isinstance(item, dict)
|
|
1746
|
+
]
|
|
1747
|
+
transfer_pagination = candidates.get("transfer_pagination") if isinstance(candidates.get("transfer_pagination"), dict) else {}
|
|
1748
|
+
compact: dict[str, Any] = {
|
|
1749
|
+
"task": {
|
|
1750
|
+
"app_key": task.get("app_key"),
|
|
1751
|
+
"app_name": task.get("app_name"),
|
|
1752
|
+
"record_id": stringify_backend_id(task.get("record_id")),
|
|
1753
|
+
"workflow_node_id": task.get("workflow_node_id"),
|
|
1754
|
+
"workflow_node_name": task.get("workflow_node_name"),
|
|
1755
|
+
"initiator": self._compact_initiator(record.get("apply_user")),
|
|
1756
|
+
"actionable": task.get("actionable"),
|
|
1757
|
+
},
|
|
1758
|
+
"record_summary": {
|
|
1759
|
+
"apply_status": record.get("apply_status"),
|
|
1760
|
+
"apply_num": record.get("apply_num"),
|
|
1761
|
+
"custom_apply_num": record.get("custom_apply_num"),
|
|
1762
|
+
"apply_time": record.get("apply_time"),
|
|
1763
|
+
"last_update_time": record.get("last_update_time"),
|
|
1764
|
+
"core_fields": self._task_record_core_fields(record.get("answers") or []),
|
|
1765
|
+
"all_fields": self._task_record_all_fields(record.get("answers") or []),
|
|
1766
|
+
},
|
|
1767
|
+
"available_actions": available_actions,
|
|
1768
|
+
"editable_fields": [
|
|
1769
|
+
self._compact_task_editable_field(item, update_schema)
|
|
1770
|
+
for item in writable_fields
|
|
1771
|
+
if isinstance(item, dict)
|
|
1772
|
+
],
|
|
1773
|
+
"extras": {
|
|
1774
|
+
"workflow_log": {
|
|
1775
|
+
"available": bool(workflow_log.get("available")),
|
|
1776
|
+
"qrobot_log_visible": bool(workflow_log.get("qrobot_log_visible")),
|
|
1777
|
+
"history_count": workflow_log.get("history_count"),
|
|
1778
|
+
},
|
|
1779
|
+
"associated_reports": {
|
|
1780
|
+
"available": bool(associated_reports.get("visible")),
|
|
1781
|
+
"loaded": bool(associated_reports.get("loaded")),
|
|
1782
|
+
"count": len(associated_items),
|
|
1783
|
+
"items": associated_items,
|
|
1784
|
+
},
|
|
1785
|
+
"rollback_candidates": {
|
|
1786
|
+
"available": "rollback" in available_actions,
|
|
1787
|
+
"loaded": bool(candidates.get("loaded")),
|
|
1788
|
+
"count": len(rollback_items),
|
|
1789
|
+
"items": rollback_items,
|
|
1790
|
+
},
|
|
1791
|
+
"transfer_candidates": {
|
|
1792
|
+
"available": "transfer" in available_actions,
|
|
1793
|
+
"loaded": bool(transfer_pagination.get("loaded")),
|
|
1794
|
+
"count": len(transfer_items),
|
|
1795
|
+
"items": transfer_items,
|
|
1796
|
+
"pagination": transfer_pagination,
|
|
1797
|
+
"warnings": candidates.get("warnings") or [],
|
|
1798
|
+
},
|
|
1799
|
+
},
|
|
1800
|
+
}
|
|
1801
|
+
action_metadata = self._compact_task_action_metadata(capabilities)
|
|
1802
|
+
if action_metadata:
|
|
1803
|
+
compact["action_metadata"] = action_metadata
|
|
1804
|
+
editable_metadata = self._compact_task_editable_metadata(update_schema)
|
|
1805
|
+
if editable_metadata:
|
|
1806
|
+
compact["editable_metadata"] = editable_metadata
|
|
1807
|
+
return compact
|
|
1808
|
+
|
|
1809
|
+
def _compact_task_action_metadata(self, capabilities: dict[str, Any]) -> dict[str, Any]:
|
|
1810
|
+
constraints = capabilities.get("action_constraints") if isinstance(capabilities.get("action_constraints"), dict) else {}
|
|
1811
|
+
metadata: dict[str, Any] = {}
|
|
1812
|
+
feedback_required_for = constraints.get("feedback_required_for") if isinstance(constraints.get("feedback_required_for"), list) else []
|
|
1813
|
+
if feedback_required_for:
|
|
1814
|
+
metadata["feedback_required_for"] = feedback_required_for
|
|
1815
|
+
visible_but_unimplemented = capabilities.get("visible_but_unimplemented_actions")
|
|
1816
|
+
if visible_but_unimplemented:
|
|
1817
|
+
metadata["visible_but_unimplemented_actions"] = visible_but_unimplemented
|
|
1818
|
+
if capabilities.get("save_only_source"):
|
|
1819
|
+
metadata["save_only_source"] = capabilities.get("save_only_source")
|
|
1820
|
+
if capabilities.get("warnings"):
|
|
1821
|
+
metadata["warnings"] = capabilities.get("warnings")
|
|
1822
|
+
return metadata
|
|
1823
|
+
|
|
1824
|
+
def _compact_task_editable_metadata(self, update_schema: dict[str, Any]) -> dict[str, Any]:
|
|
1825
|
+
metadata: dict[str, Any] = {}
|
|
1826
|
+
blockers = update_schema.get("blockers") if isinstance(update_schema.get("blockers"), list) else []
|
|
1827
|
+
warnings = update_schema.get("warnings") if isinstance(update_schema.get("warnings"), list) else []
|
|
1828
|
+
if blockers:
|
|
1829
|
+
metadata["blockers"] = blockers
|
|
1830
|
+
if warnings:
|
|
1831
|
+
metadata["warnings"] = warnings
|
|
1832
|
+
return metadata
|
|
1833
|
+
|
|
1834
|
+
def _compact_initiator(self, payload: Any) -> dict[str, Any] | None:
|
|
1835
|
+
if not isinstance(payload, dict):
|
|
1836
|
+
return None
|
|
1837
|
+
compact = {
|
|
1838
|
+
"uid": payload.get("uid"),
|
|
1839
|
+
"displayName": payload.get("displayName") or payload.get("name") or payload.get("nickName"),
|
|
1840
|
+
"email": payload.get("email"),
|
|
1841
|
+
"mobile": payload.get("mobile"),
|
|
1842
|
+
"headImg": payload.get("headImg"),
|
|
1843
|
+
}
|
|
1844
|
+
return {key: value for key, value in compact.items() if value not in (None, "", [])} or None
|
|
1845
|
+
|
|
1846
|
+
def _task_app_name(
|
|
1847
|
+
self,
|
|
1848
|
+
*,
|
|
1849
|
+
context: BackendRequestContext,
|
|
1850
|
+
app_key: str,
|
|
1851
|
+
detail: dict[str, Any],
|
|
1852
|
+
node_info: dict[str, Any],
|
|
1853
|
+
) -> Any:
|
|
1854
|
+
for source in (detail, node_info):
|
|
1855
|
+
for key in ("formTitle", "appName", "worksheetName", "appTitle"):
|
|
1856
|
+
value = source.get(key)
|
|
1857
|
+
if value not in (None, ""):
|
|
1858
|
+
if app_key:
|
|
1859
|
+
self._app_name_cache[app_key] = str(value)
|
|
1860
|
+
return value
|
|
1861
|
+
normalized_app_key = str(app_key or "").strip()
|
|
1862
|
+
if not normalized_app_key:
|
|
1863
|
+
return None
|
|
1864
|
+
if normalized_app_key in self._app_name_cache:
|
|
1865
|
+
return self._app_name_cache[normalized_app_key]
|
|
1866
|
+
resolved = self._resolve_task_app_name_from_base_info(context=context, app_key=normalized_app_key)
|
|
1867
|
+
if resolved is None:
|
|
1868
|
+
resolved = self._resolve_task_app_name_from_visible_apps(context=context, app_key=normalized_app_key)
|
|
1869
|
+
self._app_name_cache[normalized_app_key] = resolved
|
|
1870
|
+
return resolved
|
|
1871
|
+
|
|
1872
|
+
def _resolve_task_app_name_from_base_info(
|
|
1873
|
+
self,
|
|
1874
|
+
*,
|
|
1875
|
+
context: BackendRequestContext,
|
|
1876
|
+
app_key: str,
|
|
1877
|
+
) -> str | None:
|
|
1878
|
+
try:
|
|
1879
|
+
base_info = self.backend.request("GET", context, f"/app/{app_key}/baseInfo")
|
|
1880
|
+
except QingflowApiError:
|
|
1881
|
+
return None
|
|
1882
|
+
if not isinstance(base_info, dict):
|
|
1883
|
+
return None
|
|
1884
|
+
for key in ("formTitle", "title", "appName", "name"):
|
|
1885
|
+
value = str(base_info.get(key) or "").strip()
|
|
1886
|
+
if value:
|
|
1887
|
+
return value
|
|
1888
|
+
return None
|
|
1889
|
+
|
|
1890
|
+
def _resolve_task_app_name_from_visible_apps(
|
|
1891
|
+
self,
|
|
1892
|
+
*,
|
|
1893
|
+
context: BackendRequestContext,
|
|
1894
|
+
app_key: str,
|
|
1895
|
+
) -> str | None:
|
|
1896
|
+
try:
|
|
1897
|
+
visible_apps = self.backend.request("GET", context, "/tag/apps")
|
|
1898
|
+
except QingflowApiError:
|
|
1899
|
+
return None
|
|
1900
|
+
return self._find_task_app_name_in_visible_apps(visible_apps, app_key=app_key)
|
|
1901
|
+
|
|
1902
|
+
def _find_task_app_name_in_visible_apps(self, payload: Any, *, app_key: str) -> str | None:
|
|
1903
|
+
if isinstance(payload, list):
|
|
1904
|
+
for item in payload:
|
|
1905
|
+
resolved = self._find_task_app_name_in_visible_apps(item, app_key=app_key)
|
|
1906
|
+
if resolved:
|
|
1907
|
+
return resolved
|
|
1908
|
+
return None
|
|
1909
|
+
if not isinstance(payload, dict):
|
|
1910
|
+
return None
|
|
1911
|
+
candidate_app_key = str(payload.get("appKey") or payload.get("app_key") or "").strip()
|
|
1912
|
+
if candidate_app_key == app_key:
|
|
1913
|
+
for key in ("formTitle", "title", "appName", "name"):
|
|
1914
|
+
value = str(payload.get(key) or "").strip()
|
|
1915
|
+
if value:
|
|
1916
|
+
return value
|
|
1917
|
+
for value in payload.values():
|
|
1918
|
+
if isinstance(value, (list, dict)):
|
|
1919
|
+
resolved = self._find_task_app_name_in_visible_apps(value, app_key=app_key)
|
|
1920
|
+
if resolved:
|
|
1921
|
+
return resolved
|
|
1922
|
+
return None
|
|
1923
|
+
|
|
1924
|
+
def _task_record_core_fields(self, answers: Any, *, limit: int = 12) -> dict[str, Any]:
|
|
1925
|
+
return self._task_record_field_map(answers, limit=limit, truncate_text=160)
|
|
1926
|
+
|
|
1927
|
+
def _task_record_all_fields(self, answers: Any) -> dict[str, Any]:
|
|
1928
|
+
return self._task_record_field_map(answers, limit=None, truncate_text=None)
|
|
1929
|
+
|
|
1930
|
+
def _task_record_field_map(
|
|
1931
|
+
self,
|
|
1932
|
+
answers: Any,
|
|
1933
|
+
*,
|
|
1934
|
+
limit: int | None,
|
|
1935
|
+
truncate_text: int | None,
|
|
1936
|
+
) -> dict[str, Any]:
|
|
1937
|
+
if not isinstance(answers, list):
|
|
1938
|
+
return {}
|
|
1939
|
+
field_map: dict[str, Any] = {}
|
|
1940
|
+
for answer in answers:
|
|
1941
|
+
if not isinstance(answer, dict):
|
|
1942
|
+
continue
|
|
1943
|
+
title = answer.get("queTitle") or answer.get("title") or answer.get("fieldName")
|
|
1944
|
+
if not title:
|
|
1945
|
+
que_id = answer.get("queId")
|
|
1946
|
+
title = f"field_{que_id}" if que_id not in (None, "") else None
|
|
1947
|
+
if not title:
|
|
1948
|
+
continue
|
|
1949
|
+
table_values = answer.get("tableValues") if isinstance(answer.get("tableValues"), list) else []
|
|
1950
|
+
if table_values:
|
|
1951
|
+
value: Any = f"子表格 {len(table_values)} 行"
|
|
1952
|
+
else:
|
|
1953
|
+
values = self._extract_answer_values(answer)
|
|
1954
|
+
if not values:
|
|
1955
|
+
continue
|
|
1956
|
+
value = values[0] if len(values) == 1 else values
|
|
1957
|
+
if value in (None, "", []):
|
|
1958
|
+
continue
|
|
1959
|
+
field_map[str(title)] = self._compact_task_value(value, truncate_text=truncate_text)
|
|
1960
|
+
if limit is not None and len(field_map) >= limit:
|
|
1961
|
+
break
|
|
1962
|
+
return field_map
|
|
1963
|
+
|
|
1964
|
+
def _compact_task_value(self, value: Any, *, truncate_text: int | None = 160) -> Any:
|
|
1965
|
+
if isinstance(value, list):
|
|
1966
|
+
items = [self._compact_task_value(item, truncate_text=truncate_text) for item in value]
|
|
1967
|
+
if truncate_text is not None:
|
|
1968
|
+
return items[:8]
|
|
1969
|
+
return items
|
|
1970
|
+
text = re.sub(r"<[^>]+>", " ", str(value))
|
|
1971
|
+
text = re.sub(r"\s+", " ", text).strip()
|
|
1972
|
+
if truncate_text is None or len(text) <= truncate_text:
|
|
1973
|
+
return text
|
|
1974
|
+
if truncate_text <= 3:
|
|
1975
|
+
return text[:truncate_text]
|
|
1976
|
+
return text[: truncate_text - 3].rstrip() + "..."
|
|
1977
|
+
|
|
1978
|
+
def _compact_task_editable_field(self, field: dict[str, Any], update_schema: dict[str, Any]) -> dict[str, Any]:
|
|
1979
|
+
payload_template = update_schema.get("payload_template") if isinstance(update_schema.get("payload_template"), dict) else {}
|
|
1980
|
+
title = field.get("title")
|
|
1981
|
+
compact: dict[str, Any] = {}
|
|
1982
|
+
for key in ("field_id", "title", "kind", "required", "candidate_hint"):
|
|
1983
|
+
if key in field:
|
|
1984
|
+
compact[key] = field.get(key)
|
|
1985
|
+
if title in payload_template:
|
|
1986
|
+
compact["template"] = payload_template.get(title)
|
|
1987
|
+
return compact
|
|
1988
|
+
|
|
1989
|
+
def _compact_associated_report(self, item: dict[str, Any]) -> dict[str, Any]:
|
|
1990
|
+
return {
|
|
1991
|
+
key: value
|
|
1992
|
+
for key, value in {
|
|
1993
|
+
"report_id": item.get("report_id"),
|
|
1994
|
+
"chart_key": item.get("chart_key"),
|
|
1995
|
+
"chart_name": item.get("chart_name"),
|
|
1996
|
+
"graph_type": item.get("graph_type"),
|
|
1997
|
+
"source_type": item.get("source_type"),
|
|
1998
|
+
"target_app_key": item.get("target_app_key"),
|
|
1999
|
+
"target_app_name": item.get("target_app_name"),
|
|
2000
|
+
}.items()
|
|
2001
|
+
if value not in (None, "", [])
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
def _compact_rollback_candidate(self, item: dict[str, Any]) -> dict[str, Any]:
|
|
2005
|
+
return {
|
|
2006
|
+
key: value
|
|
2007
|
+
for key, value in {
|
|
2008
|
+
"workflow_node_id": item.get("auditNodeId") or item.get("nodeId"),
|
|
2009
|
+
"workflow_node_name": item.get("auditNodeName") or item.get("nodeName"),
|
|
2010
|
+
}.items()
|
|
2011
|
+
if value not in (None, "", [])
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
def _compact_transfer_member(self, item: dict[str, Any]) -> dict[str, Any]:
|
|
2015
|
+
uid = item.get("uid")
|
|
2016
|
+
if uid is None:
|
|
2017
|
+
uid = item.get("userId") or item.get("memberId") or item.get("id")
|
|
2018
|
+
return {
|
|
2019
|
+
key: value
|
|
2020
|
+
for key, value in {
|
|
2021
|
+
"uid": uid,
|
|
2022
|
+
"name": item.get("name") or item.get("userName") or item.get("memberName") or item.get("realName"),
|
|
2023
|
+
"email": item.get("email") or item.get("mail"),
|
|
2024
|
+
"department_id": item.get("departmentId") or item.get("deptId"),
|
|
2025
|
+
"department_name": item.get("departmentName") or item.get("deptName"),
|
|
2026
|
+
}.items()
|
|
2027
|
+
if value not in (None, "", [])
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
def _normalize_task_item(self, raw: dict[str, Any]) -> dict[str, Any]:
|
|
1350
2031
|
"""执行内部辅助逻辑。"""
|
|
1351
2032
|
app_key = raw.get("appKey") or raw.get("app_key")
|
|
1352
2033
|
record_id = raw.get("rowRecordId") or raw.get("recordId") or raw.get("applyId")
|
|
1353
2034
|
workflow_node_id = raw.get("nodeId") or raw.get("auditNodeId")
|
|
1354
|
-
apply_user = raw.get("applyUser")
|
|
1355
|
-
if apply_user is None:
|
|
1356
|
-
user_uid = raw.get("applyUserUid")
|
|
1357
|
-
user_name = raw.get("applyUserName")
|
|
1358
|
-
if user_uid is not None or user_name is not None:
|
|
1359
|
-
apply_user = {"uid": user_uid, "name": user_name}
|
|
1360
2035
|
return {
|
|
1361
|
-
"task_id": raw.get("id") or raw.get("taskId") or record_id,
|
|
2036
|
+
"task_id": stringify_backend_id(raw.get("id") or raw.get("taskId") or record_id),
|
|
1362
2037
|
"app_key": app_key,
|
|
1363
2038
|
"app_name": raw.get("formTitle") or raw.get("worksheetName") or raw.get("appName"),
|
|
1364
|
-
"record_id": record_id,
|
|
2039
|
+
"record_id": stringify_backend_id(record_id),
|
|
1365
2040
|
"workflow_node_id": workflow_node_id,
|
|
1366
2041
|
"workflow_node_name": raw.get("nodeName") or raw.get("auditNodeName"),
|
|
1367
|
-
"title": raw.get("title") or raw.get("applyTitle") or raw.get("name") or raw.get("formTitle"),
|
|
1368
|
-
"apply_user": apply_user,
|
|
1369
2042
|
"apply_time": raw.get("applyTime") or raw.get("receiveTime"),
|
|
1370
|
-
"
|
|
1371
|
-
|
|
1372
|
-
|
|
2043
|
+
"summary_fields": self._normalize_task_summary_fields(raw.get("dataSnapshot")),
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
def _public_task_item(self, item: dict[str, Any]) -> dict[str, Any]:
|
|
2047
|
+
return {
|
|
2048
|
+
"task_id": item.get("task_id"),
|
|
2049
|
+
"app_name": item.get("app_name"),
|
|
2050
|
+
"workflow_node_name": item.get("workflow_node_name"),
|
|
2051
|
+
"apply_time": item.get("apply_time"),
|
|
2052
|
+
"summary_fields": item.get("summary_fields") if isinstance(item.get("summary_fields"), list) else [],
|
|
1373
2053
|
}
|
|
1374
2054
|
|
|
2055
|
+
def _normalize_task_summary_fields(self, raw: Any) -> list[dict[str, Any]]:
|
|
2056
|
+
"""执行内部辅助逻辑。"""
|
|
2057
|
+
if not isinstance(raw, list):
|
|
2058
|
+
return []
|
|
2059
|
+
summary_fields: list[dict[str, Any]] = []
|
|
2060
|
+
for item in raw:
|
|
2061
|
+
if not isinstance(item, dict):
|
|
2062
|
+
continue
|
|
2063
|
+
summary_field: dict[str, Any] = {
|
|
2064
|
+
"field_id": item.get("fieldId"),
|
|
2065
|
+
"title": item.get("fieldTitle"),
|
|
2066
|
+
"type": item.get("fieldType"),
|
|
2067
|
+
"answer": item.get("fieldAnswer"),
|
|
2068
|
+
"desensitized": self._coerce_bool(item.get("beingDesensitized")),
|
|
2069
|
+
}
|
|
2070
|
+
associated_field_type = item.get("associatedQueType")
|
|
2071
|
+
if associated_field_type is not None:
|
|
2072
|
+
summary_field["associated_field_type"] = associated_field_type
|
|
2073
|
+
summary_fields.append(summary_field)
|
|
2074
|
+
return summary_fields
|
|
2075
|
+
|
|
1375
2076
|
def _select_task_node(self, infos: Any, workflow_node_id: int, *, app_key: str, record_id: int) -> dict[str, Any]:
|
|
1376
2077
|
"""执行内部辅助逻辑。"""
|
|
1377
2078
|
if not isinstance(infos, list) or not infos:
|
|
@@ -1454,6 +2155,7 @@ class TaskContextTools(ToolBase):
|
|
|
1454
2155
|
current_answers: Any,
|
|
1455
2156
|
) -> dict[str, Any]:
|
|
1456
2157
|
"""执行内部辅助逻辑。"""
|
|
2158
|
+
record_id_text = stringify_backend_id(record_id)
|
|
1457
2159
|
try:
|
|
1458
2160
|
app_schema = self._record_tools._get_form_schema(profile, context, app_key, force_refresh=False)
|
|
1459
2161
|
except QingflowApiError as error:
|
|
@@ -1470,7 +2172,7 @@ class TaskContextTools(ToolBase):
|
|
|
1470
2172
|
],
|
|
1471
2173
|
"selection": {
|
|
1472
2174
|
"app_key": app_key,
|
|
1473
|
-
"record_id":
|
|
2175
|
+
"record_id": record_id_text,
|
|
1474
2176
|
"workflow_node_id": workflow_node_id,
|
|
1475
2177
|
},
|
|
1476
2178
|
"transport_error": {
|
|
@@ -1524,16 +2226,16 @@ class TaskContextTools(ToolBase):
|
|
|
1524
2226
|
write_hints = self._record_tools._schema_write_hints(editable_field)
|
|
1525
2227
|
if not bool(write_hints.get("writable")):
|
|
1526
2228
|
continue
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
linkage_payloads_by_field_id=linkage_payloads_by_field_id,
|
|
1535
|
-
)
|
|
2229
|
+
writable_field = self._record_tools._ready_schema_field_payload(
|
|
2230
|
+
profile,
|
|
2231
|
+
context,
|
|
2232
|
+
editable_field,
|
|
2233
|
+
ws_id=context.ws_id,
|
|
2234
|
+
required_override=False,
|
|
2235
|
+
linkage_payloads_by_field_id=linkage_payloads_by_field_id,
|
|
1536
2236
|
)
|
|
2237
|
+
writable_field.setdefault("field_id", editable_field.que_id)
|
|
2238
|
+
writable_fields.append(writable_field)
|
|
1537
2239
|
blockers: list[str] = []
|
|
1538
2240
|
if not writable_fields:
|
|
1539
2241
|
blockers.append("NO_TASK_EDITABLE_FIELDS")
|
|
@@ -1555,7 +2257,7 @@ class TaskContextTools(ToolBase):
|
|
|
1555
2257
|
"warnings": schema_warnings,
|
|
1556
2258
|
"selection": {
|
|
1557
2259
|
"app_key": app_key,
|
|
1558
|
-
"record_id":
|
|
2260
|
+
"record_id": record_id_text,
|
|
1559
2261
|
"workflow_node_id": workflow_node_id,
|
|
1560
2262
|
},
|
|
1561
2263
|
}
|
|
@@ -1926,11 +2628,85 @@ class TaskContextTools(ToolBase):
|
|
|
1926
2628
|
for item in items:
|
|
1927
2629
|
if not isinstance(item, dict):
|
|
1928
2630
|
continue
|
|
1929
|
-
|
|
2631
|
+
uid = _coerce_count(item.get("uid") or item.get("userId") or item.get("memberId") or item.get("id"))
|
|
2632
|
+
if current_uid is not None and uid == current_uid:
|
|
1930
2633
|
continue
|
|
1931
2634
|
filtered.append(item)
|
|
1932
2635
|
return filtered
|
|
1933
2636
|
|
|
2637
|
+
def _transfer_candidate_items(
|
|
2638
|
+
self,
|
|
2639
|
+
context: BackendRequestContext,
|
|
2640
|
+
*,
|
|
2641
|
+
app_key: str,
|
|
2642
|
+
record_id: int,
|
|
2643
|
+
workflow_node_id: int,
|
|
2644
|
+
current_uid: int | None,
|
|
2645
|
+
) -> tuple[list[dict[str, Any]], list[JSONObject], JSONObject]:
|
|
2646
|
+
page_size = 100
|
|
2647
|
+
max_pages = 100
|
|
2648
|
+
page_num = 1
|
|
2649
|
+
fetched_pages = 0
|
|
2650
|
+
fetched_raw_count = 0
|
|
2651
|
+
page_amount: int | None = None
|
|
2652
|
+
reported_total: int | None = None
|
|
2653
|
+
items: list[dict[str, Any]] = []
|
|
2654
|
+
seen_member_keys: set[str] = set()
|
|
2655
|
+
warnings: list[JSONObject] = []
|
|
2656
|
+
|
|
2657
|
+
while page_num <= max_pages:
|
|
2658
|
+
result = self.backend.request(
|
|
2659
|
+
"GET",
|
|
2660
|
+
context,
|
|
2661
|
+
f"/app/{app_key}/apply/{record_id}/transfer/member",
|
|
2662
|
+
params={"pageNum": page_num, "pageSize": page_size, "auditNodeId": workflow_node_id},
|
|
2663
|
+
)
|
|
2664
|
+
fetched_pages += 1
|
|
2665
|
+
raw_items = _approval_page_items(result)
|
|
2666
|
+
fetched_raw_count += len(raw_items)
|
|
2667
|
+
if page_amount is None:
|
|
2668
|
+
page_amount = _coerce_count(_approval_page_amount(result))
|
|
2669
|
+
if reported_total is None:
|
|
2670
|
+
reported_total = _coerce_count(_approval_page_total(result))
|
|
2671
|
+
for item in self._filter_transfer_members(raw_items, current_uid=current_uid):
|
|
2672
|
+
member_key = self._transfer_member_dedupe_key(item)
|
|
2673
|
+
if member_key in seen_member_keys:
|
|
2674
|
+
continue
|
|
2675
|
+
seen_member_keys.add(member_key)
|
|
2676
|
+
items.append(item)
|
|
2677
|
+
if not raw_items:
|
|
2678
|
+
break
|
|
2679
|
+
if page_amount is not None and page_num >= page_amount:
|
|
2680
|
+
break
|
|
2681
|
+
if reported_total is not None and fetched_raw_count >= reported_total:
|
|
2682
|
+
break
|
|
2683
|
+
page_num += 1
|
|
2684
|
+
truncated = page_num > max_pages
|
|
2685
|
+
if truncated:
|
|
2686
|
+
warnings.append(
|
|
2687
|
+
{
|
|
2688
|
+
"code": "TRANSFER_CANDIDATES_TRUNCATED",
|
|
2689
|
+
"message": "transfer candidates reached the MCP safety page cap; returned candidates may be incomplete.",
|
|
2690
|
+
"max_pages": max_pages,
|
|
2691
|
+
"page_size": page_size,
|
|
2692
|
+
}
|
|
2693
|
+
)
|
|
2694
|
+
pagination: JSONObject = {
|
|
2695
|
+
"loaded": True,
|
|
2696
|
+
"page_size": page_size,
|
|
2697
|
+
"fetched_pages": fetched_pages,
|
|
2698
|
+
"reported_total": reported_total,
|
|
2699
|
+
"page_amount": page_amount,
|
|
2700
|
+
"truncated": truncated,
|
|
2701
|
+
}
|
|
2702
|
+
return items, warnings, pagination
|
|
2703
|
+
|
|
2704
|
+
def _transfer_member_dedupe_key(self, item: dict[str, Any]) -> str:
|
|
2705
|
+
uid = item.get("uid") or item.get("userId") or item.get("memberId") or item.get("id")
|
|
2706
|
+
if uid not in (None, ""):
|
|
2707
|
+
return f"uid:{uid}"
|
|
2708
|
+
return json.dumps(item, ensure_ascii=False, sort_keys=True, default=str)
|
|
2709
|
+
|
|
1934
2710
|
def _find_associated_report(self, task_context: dict[str, Any], report_id: int) -> dict[str, Any] | None:
|
|
1935
2711
|
"""执行内部辅助逻辑。"""
|
|
1936
2712
|
associated_reports = ((task_context.get("associated_reports") or {}).get("items") or [])
|