@josephyan/qingflow-app-user-mcp 0.2.0-beta.993 → 0.2.0-beta.995
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/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/cli/commands/task.py +65 -20
- package/src/qingflow_mcp/cli/formatters.py +72 -20
- package/src/qingflow_mcp/public_surface.py +7 -1
- package/src/qingflow_mcp/server.py +4 -0
- package/src/qingflow_mcp/server_app_user.py +4 -0
- package/src/qingflow_mcp/tools/task_context_tools.py +356 -124
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.
|
|
6
|
+
npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.995
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.
|
|
12
|
+
npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.995 qingflow-app-user-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -23,27 +23,40 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
23
23
|
list_parser.add_argument("--page-size", type=int, default=20)
|
|
24
24
|
list_parser.set_defaults(handler=_handle_list, format_hint="task_list")
|
|
25
25
|
|
|
26
|
-
get = task_subparsers.add_parser("get", help="
|
|
27
|
-
get.add_argument("--
|
|
28
|
-
get.add_argument("--
|
|
29
|
-
get.add_argument("--
|
|
26
|
+
get = task_subparsers.add_parser("get", help="读取待办详情;推荐直接传 --task-id")
|
|
27
|
+
get.add_argument("--task-id")
|
|
28
|
+
get.add_argument("--app-key")
|
|
29
|
+
get.add_argument("--record-id")
|
|
30
|
+
get.add_argument("--workflow-node-id", type=int)
|
|
30
31
|
get.add_argument("--include-candidates", action=argparse.BooleanOptionalAction, default=True)
|
|
31
32
|
get.add_argument("--include-associated-reports", action=argparse.BooleanOptionalAction, default=True)
|
|
32
33
|
get.set_defaults(handler=_handle_get, format_hint="task_get")
|
|
33
34
|
|
|
34
35
|
action = task_subparsers.add_parser("action", help="执行待办动作")
|
|
35
|
-
action.add_argument("--
|
|
36
|
-
action.add_argument("--
|
|
37
|
-
action.add_argument("--
|
|
36
|
+
action.add_argument("--task-id")
|
|
37
|
+
action.add_argument("--app-key")
|
|
38
|
+
action.add_argument("--record-id")
|
|
39
|
+
action.add_argument("--workflow-node-id", type=int)
|
|
38
40
|
action.add_argument("--action", required=True)
|
|
39
41
|
action.add_argument("--payload-file")
|
|
40
42
|
action.add_argument("--fields-file")
|
|
41
43
|
action.set_defaults(handler=_handle_action, format_hint="")
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
report = task_subparsers.add_parser("report", help="读取待办关联报表详情;推荐直接传 --task-id")
|
|
46
|
+
report.add_argument("--task-id")
|
|
47
|
+
report.add_argument("--app-key")
|
|
48
|
+
report.add_argument("--record-id")
|
|
49
|
+
report.add_argument("--workflow-node-id", type=int)
|
|
50
|
+
report.add_argument("--report-id", required=True, type=int)
|
|
51
|
+
report.add_argument("--page", type=int, default=1)
|
|
52
|
+
report.add_argument("--page-size", type=int, default=20)
|
|
53
|
+
report.set_defaults(handler=_handle_report, format_hint="task_associated_report_detail_get")
|
|
54
|
+
|
|
55
|
+
log = task_subparsers.add_parser("log", help="读取流程日志;推荐直接传 --task-id")
|
|
56
|
+
log.add_argument("--task-id")
|
|
57
|
+
log.add_argument("--app-key")
|
|
58
|
+
log.add_argument("--record-id")
|
|
59
|
+
log.add_argument("--workflow-node-id", type=int)
|
|
47
60
|
log.set_defaults(handler=_handle_log, format_hint="")
|
|
48
61
|
|
|
49
62
|
|
|
@@ -61,22 +74,32 @@ def _handle_list(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
61
74
|
|
|
62
75
|
|
|
63
76
|
def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
|
|
77
|
+
if not args.task_id and not (args.app_key and args.record_id and args.workflow_node_id):
|
|
78
|
+
raise RuntimeError(
|
|
79
|
+
'{"category":"config","message":"task get requires --task-id, or --app-key together with --record-id and --workflow-node-id"}'
|
|
80
|
+
)
|
|
64
81
|
return context.task.task_get(
|
|
65
82
|
profile=args.profile,
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
83
|
+
task_id=args.task_id,
|
|
84
|
+
app_key=args.app_key or "",
|
|
85
|
+
record_id=args.record_id or "",
|
|
86
|
+
workflow_node_id=int(args.workflow_node_id or 0),
|
|
69
87
|
include_candidates=bool(args.include_candidates),
|
|
70
88
|
include_associated_reports=bool(args.include_associated_reports),
|
|
71
89
|
)
|
|
72
90
|
|
|
73
91
|
|
|
74
92
|
def _handle_action(args: argparse.Namespace, context: CliContext) -> dict:
|
|
93
|
+
if not args.task_id and not (args.app_key and args.record_id and args.workflow_node_id):
|
|
94
|
+
raise RuntimeError(
|
|
95
|
+
'{"category":"config","message":"task action requires --task-id, or --app-key together with --record-id and --workflow-node-id"}'
|
|
96
|
+
)
|
|
75
97
|
return context.task.task_action_execute(
|
|
76
98
|
profile=args.profile,
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
99
|
+
task_id=args.task_id,
|
|
100
|
+
app_key=args.app_key or "",
|
|
101
|
+
record_id=args.record_id or "",
|
|
102
|
+
workflow_node_id=int(args.workflow_node_id or 0),
|
|
80
103
|
action=args.action,
|
|
81
104
|
payload=load_object_arg(args.payload_file, option_name="--payload-file") or {},
|
|
82
105
|
fields=load_object_arg(args.fields_file, option_name="--fields-file") or {},
|
|
@@ -84,9 +107,31 @@ def _handle_action(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
84
107
|
|
|
85
108
|
|
|
86
109
|
def _handle_log(args: argparse.Namespace, context: CliContext) -> dict:
|
|
110
|
+
if not args.task_id and not (args.app_key and args.record_id and args.workflow_node_id):
|
|
111
|
+
raise RuntimeError(
|
|
112
|
+
'{"category":"config","message":"task log requires --task-id, or --app-key together with --record-id and --workflow-node-id"}'
|
|
113
|
+
)
|
|
87
114
|
return context.task.task_workflow_log_get(
|
|
88
115
|
profile=args.profile,
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
116
|
+
task_id=args.task_id,
|
|
117
|
+
app_key=args.app_key or "",
|
|
118
|
+
record_id=args.record_id or "",
|
|
119
|
+
workflow_node_id=int(args.workflow_node_id or 0),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _handle_report(args: argparse.Namespace, context: CliContext) -> dict:
|
|
124
|
+
if not args.task_id and not (args.app_key and args.record_id and args.workflow_node_id):
|
|
125
|
+
raise RuntimeError(
|
|
126
|
+
'{"category":"config","message":"task report requires --task-id, or --app-key together with --record-id and --workflow-node-id"}'
|
|
127
|
+
)
|
|
128
|
+
return context.task.task_associated_report_detail_get(
|
|
129
|
+
profile=args.profile,
|
|
130
|
+
task_id=args.task_id,
|
|
131
|
+
app_key=args.app_key or "",
|
|
132
|
+
record_id=args.record_id or "",
|
|
133
|
+
workflow_node_id=int(args.workflow_node_id or 0),
|
|
134
|
+
report_id=int(args.report_id),
|
|
135
|
+
page=int(args.page),
|
|
136
|
+
page_size=int(args.page_size),
|
|
92
137
|
)
|
|
@@ -173,20 +173,24 @@ def _format_record_list(result: dict[str, Any]) -> str:
|
|
|
173
173
|
def _format_task_list(result: dict[str, Any]) -> str:
|
|
174
174
|
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
175
175
|
items = data.get("items") if isinstance(data.get("items"), list) else []
|
|
176
|
-
|
|
176
|
+
lines = ["Tasks"]
|
|
177
177
|
for item in items:
|
|
178
178
|
if not isinstance(item, dict):
|
|
179
179
|
continue
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
180
|
+
header_parts = [
|
|
181
|
+
str(item.get("task_id") or "-"),
|
|
182
|
+
str(item.get("app_name") or item.get("app_key") or "-"),
|
|
183
|
+
str(item.get("workflow_node_name") or "-"),
|
|
184
|
+
]
|
|
185
|
+
apply_time = item.get("apply_time")
|
|
186
|
+
if apply_time not in (None, ""):
|
|
187
|
+
header_parts.append(str(apply_time))
|
|
188
|
+
lines.append("- " + " / ".join(header_parts))
|
|
189
|
+
summary_fields = item.get("summary_fields") if isinstance(item.get("summary_fields"), list) else []
|
|
190
|
+
for summary in summary_fields:
|
|
191
|
+
if not isinstance(summary, dict):
|
|
192
|
+
continue
|
|
193
|
+
lines.append(f" {summary.get('title') or '-'}: {summary.get('answer') or '-'}")
|
|
190
194
|
_append_warnings(lines, result.get("warnings"))
|
|
191
195
|
return "\n".join(lines) + "\n"
|
|
192
196
|
|
|
@@ -200,15 +204,20 @@ def _format_task_get(result: dict[str, Any]) -> str:
|
|
|
200
204
|
extras = data.get("extras") if isinstance(data.get("extras"), dict) else {}
|
|
201
205
|
initiator = task.get("initiator") if isinstance(task.get("initiator"), dict) else {}
|
|
202
206
|
initiator_label = initiator.get("displayName") or initiator.get("email") or "-"
|
|
203
|
-
lines = [
|
|
204
|
-
|
|
205
|
-
f"
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
207
|
+
lines = []
|
|
208
|
+
if task.get("task_id") not in (None, ""):
|
|
209
|
+
lines.append(f"Task ID: {task.get('task_id')}")
|
|
210
|
+
lines.extend(
|
|
211
|
+
[
|
|
212
|
+
f"Locator: {task.get('app_key') or '-'} / {task.get('record_id') or '-'} / {task.get('workflow_node_id') or '-'}",
|
|
213
|
+
f"Node: {task.get('workflow_node_name') or '-'}",
|
|
214
|
+
f"App: {task.get('app_name') or '-'}",
|
|
215
|
+
f"Initiator: {initiator_label}",
|
|
216
|
+
f"Apply Status: {record_summary.get('apply_status')}",
|
|
217
|
+
f"Available Actions: {', '.join(str(item) for item in available_actions) or '-'}",
|
|
218
|
+
f"Editable Fields: {len(editable_fields)}",
|
|
219
|
+
]
|
|
220
|
+
)
|
|
212
221
|
core_fields = record_summary.get("core_fields") if isinstance(record_summary.get("core_fields"), dict) else {}
|
|
213
222
|
if core_fields:
|
|
214
223
|
lines.append("Core Fields:")
|
|
@@ -240,6 +249,48 @@ def _format_task_get(result: dict[str, Any]) -> str:
|
|
|
240
249
|
return "\n".join(lines) + "\n"
|
|
241
250
|
|
|
242
251
|
|
|
252
|
+
def _format_task_associated_report_detail(result: dict[str, Any]) -> str:
|
|
253
|
+
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
254
|
+
selection = data.get("selection") if isinstance(data.get("selection"), dict) else {}
|
|
255
|
+
result_type = str(data.get("result_type") or "-")
|
|
256
|
+
context = data.get("context") if isinstance(data.get("context"), dict) else {}
|
|
257
|
+
lines = []
|
|
258
|
+
if selection.get("task_id") not in (None, ""):
|
|
259
|
+
lines.append(f"Task ID: {selection.get('task_id')}")
|
|
260
|
+
lines.extend(
|
|
261
|
+
[
|
|
262
|
+
f"Report: {selection.get('chart_name') or '-'} ({selection.get('report_id') or '-'})",
|
|
263
|
+
f"Type: {result_type}",
|
|
264
|
+
]
|
|
265
|
+
)
|
|
266
|
+
if result_type == "view_list":
|
|
267
|
+
result_payload = data.get("result") if isinstance(data.get("result"), dict) else {}
|
|
268
|
+
items = result_payload.get("items") if isinstance(result_payload.get("items"), list) else []
|
|
269
|
+
lines.append(f"Returned Records: {len(items)}")
|
|
270
|
+
for item in items[:10]:
|
|
271
|
+
if isinstance(item, dict):
|
|
272
|
+
lines.append(json.dumps(item, ensure_ascii=False))
|
|
273
|
+
if len(items) > 10:
|
|
274
|
+
lines.append(f"... {len(items) - 10} more")
|
|
275
|
+
elif result_type == "chart_data":
|
|
276
|
+
result_payload = data.get("result") if isinstance(data.get("result"), dict) else {}
|
|
277
|
+
summary = result_payload.get("summary") if isinstance(result_payload.get("summary"), dict) else {}
|
|
278
|
+
rows = result_payload.get("rows") if isinstance(result_payload.get("rows"), list) else []
|
|
279
|
+
if summary:
|
|
280
|
+
lines.append(f"Summary: {json.dumps(summary, ensure_ascii=False)}")
|
|
281
|
+
lines.append(f"Rows: {len(rows)}")
|
|
282
|
+
for row in rows[:10]:
|
|
283
|
+
if isinstance(row, dict):
|
|
284
|
+
lines.append(json.dumps(row, ensure_ascii=False))
|
|
285
|
+
if len(rows) > 10:
|
|
286
|
+
lines.append(f"... {len(rows) - 10} more")
|
|
287
|
+
resolved_filters = context.get("resolved_filters") if isinstance(context.get("resolved_filters"), list) else []
|
|
288
|
+
if resolved_filters:
|
|
289
|
+
lines.append(f"Resolved Filters: {len(resolved_filters)}")
|
|
290
|
+
_append_warnings(lines, result.get("warnings"))
|
|
291
|
+
return "\n".join(lines) + "\n"
|
|
292
|
+
|
|
293
|
+
|
|
243
294
|
def _format_import_verify(result: dict[str, Any]) -> str:
|
|
244
295
|
lines = [
|
|
245
296
|
f"App Key: {result.get('app_key') or '-'}",
|
|
@@ -375,6 +426,7 @@ _FORMATTERS = {
|
|
|
375
426
|
"record_list": _format_record_list,
|
|
376
427
|
"task_list": _format_task_list,
|
|
377
428
|
"task_get": _format_task_get,
|
|
429
|
+
"task_associated_report_detail_get": _format_task_associated_report_detail,
|
|
378
430
|
"import_verify": _format_import_verify,
|
|
379
431
|
"import_status": _format_import_status,
|
|
380
432
|
"builder_summary": _format_builder_summary,
|
|
@@ -94,7 +94,13 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
|
|
|
94
94
|
PublicToolSpec(USER_DOMAIN, "task_list", ("task_list",), ("task", "list"), cli_show_effective_context=True),
|
|
95
95
|
PublicToolSpec(USER_DOMAIN, "task_get", ("task_get",), ("task", "get"), cli_show_effective_context=True),
|
|
96
96
|
PublicToolSpec(USER_DOMAIN, "task_action_execute", ("task_action_execute",), ("task", "action"), cli_show_effective_context=True, cli_context_write=True),
|
|
97
|
-
PublicToolSpec(
|
|
97
|
+
PublicToolSpec(
|
|
98
|
+
USER_DOMAIN,
|
|
99
|
+
"task_associated_report_detail_get",
|
|
100
|
+
("task_associated_report_detail_get",),
|
|
101
|
+
("task", "report"),
|
|
102
|
+
cli_show_effective_context=True,
|
|
103
|
+
),
|
|
98
104
|
PublicToolSpec(USER_DOMAIN, "task_workflow_log_get", ("task_workflow_log_get",), ("task", "log"), cli_show_effective_context=True),
|
|
99
105
|
PublicToolSpec(USER_DOMAIN, "directory_search", ("directory_search",), cli_public=False),
|
|
100
106
|
PublicToolSpec(USER_DOMAIN, "directory_list_internal_users", ("directory_list_internal_users",), cli_public=False),
|
|
@@ -151,6 +151,10 @@ Use `record_code_block_run` when the user wants to execute a form code-block fie
|
|
|
151
151
|
|
|
152
152
|
`task_list -> task_get -> task_action_execute`
|
|
153
153
|
|
|
154
|
+
- `task_list` returns task-card summaries keyed by `task_id`.
|
|
155
|
+
- Prefer `task_get(task_id=...)` for detail reads; MCP resolves the current todo locator internally.
|
|
156
|
+
- `task_action_execute(task_id=..., action=...)` is also supported; MCP resolves the current todo locator internally before calling the real action route.
|
|
157
|
+
- `task_workflow_log_get(task_id=...)` and `task_associated_report_detail_get(task_id=...)` are also supported for the current todo context.
|
|
154
158
|
- Use `task_associated_report_detail_get` for associated view or report details.
|
|
155
159
|
- Use `task_workflow_log_get` for full workflow log history.
|
|
156
160
|
- Task actions operate on `app_key + record_id + workflow_node_id`, not `task_id`.
|
|
@@ -146,6 +146,10 @@ Use `record_code_block_run` when the user wants to execute a form code-block fie
|
|
|
146
146
|
|
|
147
147
|
`task_list -> task_get -> task_action_execute`
|
|
148
148
|
|
|
149
|
+
- `task_list` returns task-card summaries keyed by `task_id`.
|
|
150
|
+
- Prefer `task_get(task_id=...)` for detail reads; MCP resolves the current todo locator internally.
|
|
151
|
+
- `task_action_execute(task_id=..., action=...)` is also supported; MCP resolves the current todo locator internally before calling the real action route.
|
|
152
|
+
- `task_workflow_log_get(task_id=...)` and `task_associated_report_detail_get(task_id=...)` are also supported for the current todo context.
|
|
149
153
|
- Use `task_associated_report_detail_get` for associated view or report details.
|
|
150
154
|
- Use `task_workflow_log_get` for full workflow log history.
|
|
151
155
|
- Task actions operate on `app_key + record_id + workflow_node_id`, not `task_id`.
|
|
@@ -10,7 +10,7 @@ from mcp.server.fastmcp import FastMCP
|
|
|
10
10
|
from ..backend_client import BackendRequestContext
|
|
11
11
|
from ..config import DEFAULT_PROFILE
|
|
12
12
|
from ..errors import QingflowApiError, raise_tool_error
|
|
13
|
-
from ..id_utils import ids_equal, normalize_positive_id_int, stringify_backend_id
|
|
13
|
+
from ..id_utils import ids_equal, normalize_positive_id_int, normalize_positive_id_text, stringify_backend_id
|
|
14
14
|
from ..json_types import JSONObject
|
|
15
15
|
from .approval_tools import ApprovalTools, _approval_page_amount, _approval_page_items, _approval_page_total
|
|
16
16
|
from .base import ToolBase, tool_cn_name
|
|
@@ -87,6 +87,7 @@ class TaskContextTools(ToolBase):
|
|
|
87
87
|
@mcp.tool()
|
|
88
88
|
def task_get(
|
|
89
89
|
profile: str = DEFAULT_PROFILE,
|
|
90
|
+
task_id: str = "",
|
|
90
91
|
app_key: str = "",
|
|
91
92
|
record_id: str = "",
|
|
92
93
|
workflow_node_id: int = 0,
|
|
@@ -95,6 +96,7 @@ class TaskContextTools(ToolBase):
|
|
|
95
96
|
) -> dict[str, Any]:
|
|
96
97
|
return self.task_get(
|
|
97
98
|
profile=profile,
|
|
99
|
+
task_id=task_id,
|
|
98
100
|
app_key=app_key,
|
|
99
101
|
record_id=record_id,
|
|
100
102
|
workflow_node_id=workflow_node_id,
|
|
@@ -105,6 +107,7 @@ class TaskContextTools(ToolBase):
|
|
|
105
107
|
@mcp.tool(description=self._high_risk_tool_description(operation="execute", target="workflow task action"))
|
|
106
108
|
def task_action_execute(
|
|
107
109
|
profile: str = DEFAULT_PROFILE,
|
|
110
|
+
task_id: str = "",
|
|
108
111
|
app_key: str = "",
|
|
109
112
|
record_id: str = "",
|
|
110
113
|
workflow_node_id: int = 0,
|
|
@@ -114,6 +117,7 @@ class TaskContextTools(ToolBase):
|
|
|
114
117
|
) -> dict[str, Any]:
|
|
115
118
|
return self.task_action_execute(
|
|
116
119
|
profile=profile,
|
|
120
|
+
task_id=task_id,
|
|
117
121
|
app_key=app_key,
|
|
118
122
|
record_id=record_id,
|
|
119
123
|
workflow_node_id=workflow_node_id,
|
|
@@ -125,6 +129,7 @@ class TaskContextTools(ToolBase):
|
|
|
125
129
|
@mcp.tool()
|
|
126
130
|
def task_associated_report_detail_get(
|
|
127
131
|
profile: str = DEFAULT_PROFILE,
|
|
132
|
+
task_id: str = "",
|
|
128
133
|
app_key: str = "",
|
|
129
134
|
record_id: str = "",
|
|
130
135
|
workflow_node_id: int = 0,
|
|
@@ -134,6 +139,7 @@ class TaskContextTools(ToolBase):
|
|
|
134
139
|
) -> dict[str, Any]:
|
|
135
140
|
return self.task_associated_report_detail_get(
|
|
136
141
|
profile=profile,
|
|
142
|
+
task_id=task_id,
|
|
137
143
|
app_key=app_key,
|
|
138
144
|
record_id=record_id,
|
|
139
145
|
workflow_node_id=workflow_node_id,
|
|
@@ -145,12 +151,14 @@ class TaskContextTools(ToolBase):
|
|
|
145
151
|
@mcp.tool()
|
|
146
152
|
def task_workflow_log_get(
|
|
147
153
|
profile: str = DEFAULT_PROFILE,
|
|
154
|
+
task_id: str = "",
|
|
148
155
|
app_key: str = "",
|
|
149
156
|
record_id: str = "",
|
|
150
157
|
workflow_node_id: int = 0,
|
|
151
158
|
) -> dict[str, Any]:
|
|
152
159
|
return self.task_workflow_log_get(
|
|
153
160
|
profile=profile,
|
|
161
|
+
task_id=task_id,
|
|
154
162
|
app_key=app_key,
|
|
155
163
|
record_id=record_id,
|
|
156
164
|
workflow_node_id=workflow_node_id,
|
|
@@ -170,29 +178,20 @@ class TaskContextTools(ToolBase):
|
|
|
170
178
|
page_size: int,
|
|
171
179
|
) -> dict[str, Any]:
|
|
172
180
|
"""执行任务相关逻辑。"""
|
|
173
|
-
|
|
174
|
-
normalized_status = self._task_tools._flow_status_to_process_status(flow_status)
|
|
175
|
-
raw = self._task_tools.task_list(
|
|
181
|
+
response = self._list_normalized_task_items(
|
|
176
182
|
profile=profile,
|
|
177
|
-
|
|
178
|
-
|
|
183
|
+
task_box=task_box,
|
|
184
|
+
flow_status=flow_status,
|
|
179
185
|
app_key=app_key,
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
186
|
+
workflow_node_id=workflow_node_id,
|
|
187
|
+
query=query,
|
|
188
|
+
page=page,
|
|
183
189
|
page_size=page_size,
|
|
184
|
-
create_time_asc=None,
|
|
185
190
|
)
|
|
186
|
-
task_page = raw.get("page", {})
|
|
187
191
|
warnings: list[dict[str, Any]] = []
|
|
188
|
-
items = [
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if isinstance(item, dict)
|
|
192
|
-
]
|
|
193
|
-
returned_items = len(items)
|
|
194
|
-
page_amount = _task_page_amount(task_page)
|
|
195
|
-
reported_total = _task_page_total(task_page)
|
|
192
|
+
items = response["items"] if isinstance(response.get("items"), list) else []
|
|
193
|
+
page_amount = response.get("page_amount")
|
|
194
|
+
reported_total = response.get("reported_total")
|
|
196
195
|
if query and not items:
|
|
197
196
|
fallback = self._task_list_local_query_fallback(
|
|
198
197
|
profile=profile,
|
|
@@ -218,25 +217,24 @@ class TaskContextTools(ToolBase):
|
|
|
218
217
|
),
|
|
219
218
|
}
|
|
220
219
|
)
|
|
220
|
+
public_items = [self._public_task_item(item) for item in items]
|
|
221
221
|
return {
|
|
222
222
|
"profile": profile,
|
|
223
|
-
"ws_id": raw.get("ws_id"),
|
|
223
|
+
"ws_id": response.get("raw", {}).get("ws_id") if isinstance(response.get("raw"), dict) else None,
|
|
224
224
|
"ok": True,
|
|
225
|
-
"request_route": raw.get("request_route"),
|
|
225
|
+
"request_route": response.get("raw", {}).get("request_route") if isinstance(response.get("raw"), dict) else None,
|
|
226
226
|
"warnings": warnings,
|
|
227
227
|
"output_profile": "normal",
|
|
228
228
|
"data": {
|
|
229
|
-
"items":
|
|
229
|
+
"items": public_items,
|
|
230
230
|
"pagination": {
|
|
231
231
|
"page": page,
|
|
232
232
|
"page_size": page_size,
|
|
233
|
-
"returned_items":
|
|
233
|
+
"returned_items": len(public_items),
|
|
234
234
|
"page_amount": page_amount,
|
|
235
235
|
"reported_total": reported_total,
|
|
236
236
|
},
|
|
237
237
|
"selection": {
|
|
238
|
-
"task_box": task_box,
|
|
239
|
-
"flow_status": flow_status,
|
|
240
238
|
"app_key": app_key,
|
|
241
239
|
"workflow_node_id": workflow_node_id,
|
|
242
240
|
"query": query,
|
|
@@ -249,28 +247,44 @@ class TaskContextTools(ToolBase):
|
|
|
249
247
|
self,
|
|
250
248
|
*,
|
|
251
249
|
profile: str,
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
250
|
+
task_id: Any = None,
|
|
251
|
+
app_key: str = "",
|
|
252
|
+
record_id: Any = "",
|
|
253
|
+
workflow_node_id: int = 0,
|
|
254
|
+
include_candidates: bool = True,
|
|
255
|
+
include_associated_reports: bool = True,
|
|
257
256
|
) -> dict[str, Any]:
|
|
258
257
|
"""执行任务相关逻辑。"""
|
|
259
|
-
|
|
260
|
-
|
|
258
|
+
if task_id in (None, ""):
|
|
259
|
+
normalize_positive_id_int(record_id, field_name="record_id")
|
|
261
260
|
|
|
262
261
|
def runner(session_profile, context):
|
|
263
|
-
|
|
262
|
+
locator = self._resolve_task_locator_input(
|
|
264
263
|
profile=profile,
|
|
265
|
-
|
|
264
|
+
task_id=task_id,
|
|
266
265
|
app_key=app_key,
|
|
267
|
-
record_id=
|
|
266
|
+
record_id=record_id,
|
|
268
267
|
workflow_node_id=workflow_node_id,
|
|
268
|
+
)
|
|
269
|
+
task_id_text = locator["task_id"]
|
|
270
|
+
resolved_app_key = str(locator["app_key"])
|
|
271
|
+
resolved_record_id = int(locator["record_id"])
|
|
272
|
+
resolved_workflow_node_id = int(locator["workflow_node_id"])
|
|
273
|
+
self._require_app_record_and_node(resolved_app_key, resolved_record_id, resolved_workflow_node_id)
|
|
274
|
+
data = self._build_task_context(
|
|
275
|
+
profile=profile,
|
|
276
|
+
context=context,
|
|
277
|
+
app_key=resolved_app_key,
|
|
278
|
+
record_id=resolved_record_id,
|
|
279
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
269
280
|
include_candidates=include_candidates,
|
|
270
281
|
include_associated_reports=include_associated_reports,
|
|
271
282
|
current_uid=session_profile.uid,
|
|
272
283
|
)
|
|
273
284
|
data = self._compact_task_get_context(data)
|
|
285
|
+
task_payload = data.get("task")
|
|
286
|
+
if isinstance(task_payload, dict) and task_id_text is not None:
|
|
287
|
+
task_payload["task_id"] = task_id_text
|
|
274
288
|
return {
|
|
275
289
|
"profile": profile,
|
|
276
290
|
"ws_id": session_profile.selected_ws_id,
|
|
@@ -312,17 +326,18 @@ class TaskContextTools(ToolBase):
|
|
|
312
326
|
self,
|
|
313
327
|
*,
|
|
314
328
|
profile: str,
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
329
|
+
task_id: Any = None,
|
|
330
|
+
app_key: str = "",
|
|
331
|
+
record_id: Any = "",
|
|
332
|
+
workflow_node_id: int = 0,
|
|
318
333
|
action: str,
|
|
319
334
|
payload: dict[str, Any],
|
|
320
335
|
fields: dict[str, Any] | None = None,
|
|
321
336
|
) -> dict[str, Any]:
|
|
322
337
|
"""执行任务相关逻辑。"""
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
338
|
+
if task_id in (None, ""):
|
|
339
|
+
normalize_positive_id_int(record_id, field_name="record_id")
|
|
340
|
+
|
|
326
341
|
normalized_action = (action or "").strip().lower()
|
|
327
342
|
if normalized_action not in {"approve", "reject", "rollback", "transfer", "urge", "save_only"}:
|
|
328
343
|
raise_tool_error(
|
|
@@ -340,13 +355,27 @@ class TaskContextTools(ToolBase):
|
|
|
340
355
|
)
|
|
341
356
|
|
|
342
357
|
def runner(session_profile, context):
|
|
358
|
+
locator = self._resolve_task_locator_input(
|
|
359
|
+
profile=profile,
|
|
360
|
+
task_id=task_id,
|
|
361
|
+
app_key=app_key,
|
|
362
|
+
record_id=record_id,
|
|
363
|
+
workflow_node_id=workflow_node_id,
|
|
364
|
+
)
|
|
365
|
+
task_id_text = locator["task_id"]
|
|
366
|
+
resolved_app_key = str(locator["app_key"])
|
|
367
|
+
resolved_record_id = int(locator["record_id"])
|
|
368
|
+
resolved_record_id_text = str(locator["record_id_text"] or "")
|
|
369
|
+
resolved_workflow_node_id = int(locator["workflow_node_id"])
|
|
370
|
+
record_id_text = resolved_record_id_text
|
|
371
|
+
self._require_app_record_and_node(resolved_app_key, resolved_record_id, resolved_workflow_node_id)
|
|
343
372
|
try:
|
|
344
373
|
task_context = self._build_task_context(
|
|
345
374
|
profile=profile,
|
|
346
375
|
context=context,
|
|
347
|
-
app_key=
|
|
348
|
-
record_id=
|
|
349
|
-
workflow_node_id=
|
|
376
|
+
app_key=resolved_app_key,
|
|
377
|
+
record_id=resolved_record_id,
|
|
378
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
350
379
|
include_candidates=False,
|
|
351
380
|
include_associated_reports=False,
|
|
352
381
|
current_uid=session_profile.uid,
|
|
@@ -357,12 +386,13 @@ class TaskContextTools(ToolBase):
|
|
|
357
386
|
profile=profile,
|
|
358
387
|
session_profile=session_profile,
|
|
359
388
|
context=context,
|
|
360
|
-
app_key=
|
|
361
|
-
record_id=
|
|
362
|
-
workflow_node_id=
|
|
389
|
+
app_key=resolved_app_key,
|
|
390
|
+
record_id=resolved_record_id,
|
|
391
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
363
392
|
action=normalized_action,
|
|
364
393
|
source_error=error,
|
|
365
394
|
before_apply_status=None,
|
|
395
|
+
task_id=task_id_text,
|
|
366
396
|
)
|
|
367
397
|
raise
|
|
368
398
|
if normalized_action == "save_only" and not field_updates:
|
|
@@ -391,7 +421,7 @@ class TaskContextTools(ToolBase):
|
|
|
391
421
|
raise_tool_error(QingflowApiError.config_error(message))
|
|
392
422
|
raise_tool_error(
|
|
393
423
|
QingflowApiError.config_error(
|
|
394
|
-
f"task action '{normalized_action}' is not currently available for app_key='{
|
|
424
|
+
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}"
|
|
395
425
|
)
|
|
396
426
|
)
|
|
397
427
|
feedback_required_for = capabilities.get("action_constraints", {}).get("feedback_required_for") or []
|
|
@@ -412,9 +442,9 @@ class TaskContextTools(ToolBase):
|
|
|
412
442
|
prepared_fields = self._prepare_task_field_update(
|
|
413
443
|
profile=profile,
|
|
414
444
|
context=context,
|
|
415
|
-
app_key=
|
|
416
|
-
record_id=
|
|
417
|
-
workflow_node_id=
|
|
445
|
+
app_key=resolved_app_key,
|
|
446
|
+
record_id=resolved_record_id,
|
|
447
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
418
448
|
task_context=task_context,
|
|
419
449
|
fields=field_updates,
|
|
420
450
|
)
|
|
@@ -424,16 +454,16 @@ class TaskContextTools(ToolBase):
|
|
|
424
454
|
runtime_baseline = self._capture_task_runtime_baseline(
|
|
425
455
|
profile=profile,
|
|
426
456
|
context=context,
|
|
427
|
-
app_key=
|
|
428
|
-
record_id=
|
|
429
|
-
workflow_node_id=
|
|
457
|
+
app_key=resolved_app_key,
|
|
458
|
+
record_id=resolved_record_id,
|
|
459
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
430
460
|
)
|
|
431
461
|
try:
|
|
432
462
|
raw = self._execute_task_action(
|
|
433
463
|
profile=profile,
|
|
434
|
-
app_key=
|
|
435
|
-
record_id=
|
|
436
|
-
workflow_node_id=
|
|
464
|
+
app_key=resolved_app_key,
|
|
465
|
+
record_id=resolved_record_id,
|
|
466
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
437
467
|
normalized_action=normalized_action,
|
|
438
468
|
payload=body,
|
|
439
469
|
prepared_fields=prepared_fields,
|
|
@@ -444,21 +474,22 @@ class TaskContextTools(ToolBase):
|
|
|
444
474
|
profile=profile,
|
|
445
475
|
session_profile=session_profile,
|
|
446
476
|
context=context,
|
|
447
|
-
app_key=
|
|
448
|
-
record_id=
|
|
449
|
-
workflow_node_id=
|
|
477
|
+
app_key=resolved_app_key,
|
|
478
|
+
record_id=resolved_record_id,
|
|
479
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
450
480
|
action=normalized_action,
|
|
451
481
|
source_error=error,
|
|
452
482
|
before_apply_status=before_apply_status,
|
|
483
|
+
task_id=task_id_text,
|
|
453
484
|
)
|
|
454
485
|
raise
|
|
455
486
|
|
|
456
487
|
if normalized_action == "save_only":
|
|
457
488
|
verification, warnings = self._verify_task_save_only(
|
|
458
489
|
context=context,
|
|
459
|
-
app_key=
|
|
460
|
-
record_id=
|
|
461
|
-
workflow_node_id=
|
|
490
|
+
app_key=resolved_app_key,
|
|
491
|
+
record_id=resolved_record_id,
|
|
492
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
462
493
|
before_apply_status=before_apply_status,
|
|
463
494
|
expected_answers=((prepared_fields or {}).get("normalized_answers") or []),
|
|
464
495
|
task_context=task_context,
|
|
@@ -470,9 +501,9 @@ class TaskContextTools(ToolBase):
|
|
|
470
501
|
verification, warnings = self._verify_task_action_runtime(
|
|
471
502
|
profile=profile,
|
|
472
503
|
context=context,
|
|
473
|
-
app_key=
|
|
474
|
-
record_id=
|
|
475
|
-
workflow_node_id=
|
|
504
|
+
app_key=resolved_app_key,
|
|
505
|
+
record_id=resolved_record_id,
|
|
506
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
476
507
|
action=normalized_action,
|
|
477
508
|
before_apply_status=before_apply_status,
|
|
478
509
|
runtime_baseline=runtime_baseline,
|
|
@@ -480,7 +511,7 @@ class TaskContextTools(ToolBase):
|
|
|
480
511
|
runtime_verified = bool(verification.get("runtime_continuation_verified"))
|
|
481
512
|
status = "success" if runtime_verified else "partial_success"
|
|
482
513
|
error_code = None if runtime_verified else "WORKFLOW_CONTINUATION_UNVERIFIED"
|
|
483
|
-
|
|
514
|
+
result = {
|
|
484
515
|
"profile": raw.get("profile", profile),
|
|
485
516
|
"ws_id": raw.get("ws_id", session_profile.selected_ws_id),
|
|
486
517
|
"ok": bool(raw.get("ok", True)) and status != "failed",
|
|
@@ -493,9 +524,9 @@ class TaskContextTools(ToolBase):
|
|
|
493
524
|
"data": {
|
|
494
525
|
"action": normalized_action,
|
|
495
526
|
"resource": {
|
|
496
|
-
"app_key":
|
|
527
|
+
"app_key": resolved_app_key,
|
|
497
528
|
"record_id": record_id_text,
|
|
498
|
-
"workflow_node_id":
|
|
529
|
+
"workflow_node_id": resolved_workflow_node_id,
|
|
499
530
|
},
|
|
500
531
|
"selection": {"action": normalized_action},
|
|
501
532
|
"result": raw.get("result"),
|
|
@@ -503,6 +534,11 @@ class TaskContextTools(ToolBase):
|
|
|
503
534
|
"field_update_applied": bool(field_updates),
|
|
504
535
|
},
|
|
505
536
|
}
|
|
537
|
+
if task_id_text is not None:
|
|
538
|
+
resource = result["data"].get("resource")
|
|
539
|
+
if isinstance(resource, dict):
|
|
540
|
+
resource["task_id"] = task_id_text
|
|
541
|
+
return result
|
|
506
542
|
|
|
507
543
|
return self._run(profile, runner)
|
|
508
544
|
|
|
@@ -920,6 +956,7 @@ class TaskContextTools(ToolBase):
|
|
|
920
956
|
action: str,
|
|
921
957
|
source_error: QingflowApiError,
|
|
922
958
|
before_apply_status: Any,
|
|
959
|
+
task_id: str | None = None,
|
|
923
960
|
) -> dict[str, Any]:
|
|
924
961
|
"""执行内部辅助逻辑。"""
|
|
925
962
|
record_id_text = stringify_backend_id(record_id)
|
|
@@ -941,7 +978,7 @@ class TaskContextTools(ToolBase):
|
|
|
941
978
|
"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.",
|
|
942
979
|
}
|
|
943
980
|
)
|
|
944
|
-
|
|
981
|
+
result = {
|
|
945
982
|
"profile": profile,
|
|
946
983
|
"ws_id": session_profile.selected_ws_id,
|
|
947
984
|
"ok": True,
|
|
@@ -963,13 +1000,18 @@ class TaskContextTools(ToolBase):
|
|
|
963
1000
|
"human_review": True,
|
|
964
1001
|
},
|
|
965
1002
|
}
|
|
1003
|
+
if task_id is not None:
|
|
1004
|
+
resource = result["data"].get("resource")
|
|
1005
|
+
if isinstance(resource, dict):
|
|
1006
|
+
resource["task_id"] = task_id
|
|
1007
|
+
return result
|
|
966
1008
|
warnings.append(
|
|
967
1009
|
{
|
|
968
1010
|
"code": "TASK_CONTEXT_VISIBILITY_UNVERIFIED",
|
|
969
1011
|
"message": "the task is no longer actionable, and MCP could not verify from state or workflow logs whether it was already processed.",
|
|
970
1012
|
}
|
|
971
1013
|
)
|
|
972
|
-
|
|
1014
|
+
result = {
|
|
973
1015
|
"profile": profile,
|
|
974
1016
|
"ws_id": session_profile.selected_ws_id,
|
|
975
1017
|
"ok": False,
|
|
@@ -996,11 +1038,16 @@ class TaskContextTools(ToolBase):
|
|
|
996
1038
|
},
|
|
997
1039
|
},
|
|
998
1040
|
}
|
|
1041
|
+
if task_id is not None:
|
|
1042
|
+
resource = result["data"].get("resource")
|
|
1043
|
+
if isinstance(resource, dict):
|
|
1044
|
+
resource["task_id"] = task_id
|
|
1045
|
+
return result
|
|
999
1046
|
|
|
1000
1047
|
def _safe_task_list_items(self, *, profile: str, task_box: str, app_key: str) -> list[dict[str, Any]]:
|
|
1001
1048
|
"""执行内部辅助逻辑。"""
|
|
1002
1049
|
try:
|
|
1003
|
-
response = self.
|
|
1050
|
+
response = self._list_normalized_task_items(
|
|
1004
1051
|
profile=profile,
|
|
1005
1052
|
task_box=task_box,
|
|
1006
1053
|
flow_status="all",
|
|
@@ -1012,12 +1059,44 @@ class TaskContextTools(ToolBase):
|
|
|
1012
1059
|
)
|
|
1013
1060
|
except QingflowApiError:
|
|
1014
1061
|
return []
|
|
1015
|
-
|
|
1016
|
-
items = data.get("items") if isinstance(data, dict) else None
|
|
1062
|
+
items = response.get("items") if isinstance(response, dict) else None
|
|
1017
1063
|
if not isinstance(items, list):
|
|
1018
1064
|
return []
|
|
1019
1065
|
return [item for item in items if isinstance(item, dict)]
|
|
1020
1066
|
|
|
1067
|
+
def _list_normalized_task_items(
|
|
1068
|
+
self,
|
|
1069
|
+
*,
|
|
1070
|
+
profile: str,
|
|
1071
|
+
task_box: str,
|
|
1072
|
+
flow_status: str,
|
|
1073
|
+
app_key: str | None,
|
|
1074
|
+
workflow_node_id: int | None,
|
|
1075
|
+
query: str | None,
|
|
1076
|
+
page: int,
|
|
1077
|
+
page_size: int,
|
|
1078
|
+
) -> dict[str, Any]:
|
|
1079
|
+
normalized_type = self._task_tools._task_box_to_type(task_box)
|
|
1080
|
+
normalized_status = self._task_tools._flow_status_to_process_status(flow_status)
|
|
1081
|
+
raw = self._task_tools.task_list(
|
|
1082
|
+
profile=profile,
|
|
1083
|
+
type=normalized_type,
|
|
1084
|
+
process_status=normalized_status,
|
|
1085
|
+
app_key=app_key,
|
|
1086
|
+
node_id=workflow_node_id,
|
|
1087
|
+
search_key=query,
|
|
1088
|
+
page_num=page,
|
|
1089
|
+
page_size=page_size,
|
|
1090
|
+
create_time_asc=None,
|
|
1091
|
+
)
|
|
1092
|
+
task_page = raw.get("page", {})
|
|
1093
|
+
return {
|
|
1094
|
+
"raw": raw,
|
|
1095
|
+
"items": [self._normalize_task_item(item) for item in _task_page_items(task_page) if isinstance(item, dict)],
|
|
1096
|
+
"page_amount": _task_page_amount(task_page),
|
|
1097
|
+
"reported_total": _task_page_total(task_page),
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1021
1100
|
def _task_list_local_query_fallback(
|
|
1022
1101
|
self,
|
|
1023
1102
|
*,
|
|
@@ -1030,39 +1109,30 @@ class TaskContextTools(ToolBase):
|
|
|
1030
1109
|
page: int,
|
|
1031
1110
|
page_size: int,
|
|
1032
1111
|
) -> dict[str, Any] | None:
|
|
1033
|
-
normalized_type = self._task_tools._task_box_to_type(task_box)
|
|
1034
|
-
normalized_status = self._task_tools._flow_status_to_process_status(flow_status)
|
|
1035
1112
|
scan_page_size = max(page_size, 100)
|
|
1036
1113
|
scan_page = 1
|
|
1037
1114
|
page_amount: int | None = None
|
|
1038
1115
|
matched_items: list[dict[str, Any]] = []
|
|
1039
1116
|
while True:
|
|
1040
|
-
|
|
1117
|
+
response = self._list_normalized_task_items(
|
|
1041
1118
|
profile=profile,
|
|
1042
|
-
|
|
1043
|
-
|
|
1119
|
+
task_box=task_box,
|
|
1120
|
+
flow_status=flow_status,
|
|
1044
1121
|
app_key=app_key,
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1122
|
+
workflow_node_id=workflow_node_id,
|
|
1123
|
+
query=None,
|
|
1124
|
+
page=scan_page,
|
|
1048
1125
|
page_size=scan_page_size,
|
|
1049
|
-
create_time_asc=None,
|
|
1050
1126
|
)
|
|
1051
|
-
|
|
1052
|
-
raw_items = _task_page_items(task_page)
|
|
1053
|
-
normalized_items = [
|
|
1054
|
-
self._normalize_task_item(item, task_box=task_box, flow_status=flow_status)
|
|
1055
|
-
for item in raw_items
|
|
1056
|
-
if isinstance(item, dict)
|
|
1057
|
-
]
|
|
1127
|
+
normalized_items = response.get("items") if isinstance(response.get("items"), list) else []
|
|
1058
1128
|
matched_items.extend(item for item in normalized_items if self._task_item_matches_query(item, query))
|
|
1059
1129
|
if page_amount is None:
|
|
1060
|
-
coerced_page_amount = _coerce_count(
|
|
1130
|
+
coerced_page_amount = _coerce_count(response.get("page_amount"))
|
|
1061
1131
|
if coerced_page_amount is not None and coerced_page_amount > 0:
|
|
1062
1132
|
page_amount = coerced_page_amount
|
|
1063
1133
|
if page_amount is not None and scan_page >= page_amount:
|
|
1064
1134
|
break
|
|
1065
|
-
if not
|
|
1135
|
+
if not normalized_items or len(normalized_items) < scan_page_size:
|
|
1066
1136
|
break
|
|
1067
1137
|
scan_page += 1
|
|
1068
1138
|
if not matched_items:
|
|
@@ -1077,6 +1147,106 @@ class TaskContextTools(ToolBase):
|
|
|
1077
1147
|
"reported_total": matched_total,
|
|
1078
1148
|
}
|
|
1079
1149
|
|
|
1150
|
+
def _resolve_todo_task_locator(self, *, profile: str, task_id: Any) -> dict[str, Any]:
|
|
1151
|
+
task_id_text = normalize_positive_id_text(task_id, field_name="task_id")
|
|
1152
|
+
page = 1
|
|
1153
|
+
page_size = 100
|
|
1154
|
+
page_amount: int | None = None
|
|
1155
|
+
while True:
|
|
1156
|
+
response = self._list_normalized_task_items(
|
|
1157
|
+
profile=profile,
|
|
1158
|
+
task_box="todo",
|
|
1159
|
+
flow_status="all",
|
|
1160
|
+
app_key=None,
|
|
1161
|
+
workflow_node_id=None,
|
|
1162
|
+
query=None,
|
|
1163
|
+
page=page,
|
|
1164
|
+
page_size=page_size,
|
|
1165
|
+
)
|
|
1166
|
+
items = response.get("items") if isinstance(response.get("items"), list) else []
|
|
1167
|
+
for item in items:
|
|
1168
|
+
if not isinstance(item, dict) or not ids_equal(item.get("task_id"), task_id_text):
|
|
1169
|
+
continue
|
|
1170
|
+
app_key = str(item.get("app_key") or "").strip()
|
|
1171
|
+
record_id = stringify_backend_id(item.get("record_id"))
|
|
1172
|
+
workflow_node_id = int(item.get("workflow_node_id") or 0)
|
|
1173
|
+
if not app_key or record_id is None or workflow_node_id <= 0:
|
|
1174
|
+
raise_tool_error(
|
|
1175
|
+
QingflowApiError.config_error(
|
|
1176
|
+
f"task_id={task_id_text} resolved to an incomplete task locator; please refresh the todo list and retry"
|
|
1177
|
+
)
|
|
1178
|
+
)
|
|
1179
|
+
return {
|
|
1180
|
+
"task_id": task_id_text,
|
|
1181
|
+
"app_key": app_key,
|
|
1182
|
+
"record_id": record_id,
|
|
1183
|
+
"workflow_node_id": workflow_node_id,
|
|
1184
|
+
}
|
|
1185
|
+
if page_amount is None:
|
|
1186
|
+
coerced_page_amount = _coerce_count(response.get("page_amount"))
|
|
1187
|
+
if coerced_page_amount is not None and coerced_page_amount > 0:
|
|
1188
|
+
page_amount = coerced_page_amount
|
|
1189
|
+
if page_amount is not None and page >= page_amount:
|
|
1190
|
+
break
|
|
1191
|
+
if not items or len(items) < page_size:
|
|
1192
|
+
break
|
|
1193
|
+
page += 1
|
|
1194
|
+
raise_tool_error(
|
|
1195
|
+
QingflowApiError.config_error(
|
|
1196
|
+
f"task_id={task_id_text} was not found in the current todo list; task context tools currently resolve actionable todo tasks by task_id"
|
|
1197
|
+
)
|
|
1198
|
+
)
|
|
1199
|
+
|
|
1200
|
+
def _resolve_task_locator_input(
|
|
1201
|
+
self,
|
|
1202
|
+
*,
|
|
1203
|
+
profile: str,
|
|
1204
|
+
task_id: Any = None,
|
|
1205
|
+
app_key: str = "",
|
|
1206
|
+
record_id: Any = "",
|
|
1207
|
+
workflow_node_id: int = 0,
|
|
1208
|
+
) -> dict[str, Any]:
|
|
1209
|
+
task_id_text = normalize_positive_id_text(task_id, field_name="task_id") if task_id not in (None, "") else None
|
|
1210
|
+
resolved_app_key = (app_key or "").strip()
|
|
1211
|
+
resolved_record_id: int
|
|
1212
|
+
resolved_workflow_node_id: int
|
|
1213
|
+
if task_id_text is not None:
|
|
1214
|
+
locator = self._resolve_todo_task_locator(profile=profile, task_id=task_id_text)
|
|
1215
|
+
resolved_app_key = str(locator["app_key"])
|
|
1216
|
+
resolved_record_id = normalize_positive_id_int(locator["record_id"], field_name="record_id")
|
|
1217
|
+
resolved_workflow_node_id = int(locator["workflow_node_id"])
|
|
1218
|
+
explicit_app_key = (app_key or "").strip()
|
|
1219
|
+
if explicit_app_key and explicit_app_key != resolved_app_key:
|
|
1220
|
+
raise_tool_error(
|
|
1221
|
+
QingflowApiError.config_error(
|
|
1222
|
+
f"task_id={task_id_text} resolved to app_key='{resolved_app_key}', which does not match app_key='{explicit_app_key}'"
|
|
1223
|
+
)
|
|
1224
|
+
)
|
|
1225
|
+
if record_id not in (None, ""):
|
|
1226
|
+
explicit_record_id = normalize_positive_id_text(record_id, field_name="record_id")
|
|
1227
|
+
if explicit_record_id != stringify_backend_id(resolved_record_id):
|
|
1228
|
+
raise_tool_error(
|
|
1229
|
+
QingflowApiError.config_error(
|
|
1230
|
+
f"task_id={task_id_text} resolved to record_id={resolved_record_id}, which does not match record_id={explicit_record_id}"
|
|
1231
|
+
)
|
|
1232
|
+
)
|
|
1233
|
+
if workflow_node_id not in (None, 0) and int(workflow_node_id) != resolved_workflow_node_id:
|
|
1234
|
+
raise_tool_error(
|
|
1235
|
+
QingflowApiError.config_error(
|
|
1236
|
+
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}"
|
|
1237
|
+
)
|
|
1238
|
+
)
|
|
1239
|
+
else:
|
|
1240
|
+
resolved_record_id = normalize_positive_id_int(record_id, field_name="record_id")
|
|
1241
|
+
resolved_workflow_node_id = int(workflow_node_id)
|
|
1242
|
+
return {
|
|
1243
|
+
"task_id": task_id_text,
|
|
1244
|
+
"app_key": resolved_app_key,
|
|
1245
|
+
"record_id": resolved_record_id,
|
|
1246
|
+
"record_id_text": stringify_backend_id(resolved_record_id),
|
|
1247
|
+
"workflow_node_id": resolved_workflow_node_id,
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1080
1250
|
def _task_item_matches_query(self, item: dict[str, Any], query: str) -> bool:
|
|
1081
1251
|
needle = str(query or "").strip().casefold()
|
|
1082
1252
|
if not needle:
|
|
@@ -1098,29 +1268,43 @@ class TaskContextTools(ToolBase):
|
|
|
1098
1268
|
self,
|
|
1099
1269
|
*,
|
|
1100
1270
|
profile: str,
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1271
|
+
task_id: Any = None,
|
|
1272
|
+
app_key: str = "",
|
|
1273
|
+
record_id: Any = "",
|
|
1274
|
+
workflow_node_id: int = 0,
|
|
1104
1275
|
report_id: int,
|
|
1105
1276
|
page: int,
|
|
1106
1277
|
page_size: int,
|
|
1107
1278
|
) -> dict[str, Any]:
|
|
1108
1279
|
"""执行任务相关逻辑。"""
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1280
|
+
if task_id in (None, ""):
|
|
1281
|
+
normalize_positive_id_int(record_id, field_name="record_id")
|
|
1282
|
+
|
|
1112
1283
|
if report_id <= 0:
|
|
1113
1284
|
raise_tool_error(QingflowApiError.config_error("report_id must be positive"))
|
|
1114
1285
|
if page <= 0 or page_size <= 0:
|
|
1115
1286
|
raise_tool_error(QingflowApiError.config_error("page and page_size must be positive"))
|
|
1116
1287
|
|
|
1117
1288
|
def runner(session_profile, context):
|
|
1118
|
-
|
|
1289
|
+
locator = self._resolve_task_locator_input(
|
|
1119
1290
|
profile=profile,
|
|
1120
|
-
|
|
1291
|
+
task_id=task_id,
|
|
1121
1292
|
app_key=app_key,
|
|
1122
|
-
record_id=
|
|
1293
|
+
record_id=record_id,
|
|
1123
1294
|
workflow_node_id=workflow_node_id,
|
|
1295
|
+
)
|
|
1296
|
+
task_id_text = locator["task_id"]
|
|
1297
|
+
resolved_app_key = str(locator["app_key"])
|
|
1298
|
+
resolved_record_id = int(locator["record_id"])
|
|
1299
|
+
record_id_text = str(locator["record_id_text"] or "")
|
|
1300
|
+
resolved_workflow_node_id = int(locator["workflow_node_id"])
|
|
1301
|
+
self._require_app_record_and_node(resolved_app_key, resolved_record_id, resolved_workflow_node_id)
|
|
1302
|
+
task_context = self._build_task_context(
|
|
1303
|
+
profile=profile,
|
|
1304
|
+
context=context,
|
|
1305
|
+
app_key=resolved_app_key,
|
|
1306
|
+
record_id=resolved_record_id,
|
|
1307
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
1124
1308
|
include_candidates=False,
|
|
1125
1309
|
include_associated_reports=True,
|
|
1126
1310
|
current_uid=session_profile.uid,
|
|
@@ -1129,7 +1313,7 @@ class TaskContextTools(ToolBase):
|
|
|
1129
1313
|
if report_item is None:
|
|
1130
1314
|
raise_tool_error(
|
|
1131
1315
|
QingflowApiError.config_error(
|
|
1132
|
-
f"report_id={report_id} is not visible for app_key='{
|
|
1316
|
+
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}"
|
|
1133
1317
|
)
|
|
1134
1318
|
)
|
|
1135
1319
|
association_query = self._build_association_query(
|
|
@@ -1137,15 +1321,17 @@ class TaskContextTools(ToolBase):
|
|
|
1137
1321
|
task_context.get("record", {}).get("answers") or [],
|
|
1138
1322
|
)
|
|
1139
1323
|
selection = {
|
|
1140
|
-
"app_key":
|
|
1324
|
+
"app_key": resolved_app_key,
|
|
1141
1325
|
"record_id": record_id_text,
|
|
1142
|
-
"workflow_node_id":
|
|
1326
|
+
"workflow_node_id": resolved_workflow_node_id,
|
|
1143
1327
|
"report_id": report_id,
|
|
1144
1328
|
"target_app_key": report_item.get("target_app_key"),
|
|
1145
1329
|
"target_app_name": report_item.get("target_app_name"),
|
|
1146
1330
|
"chart_key": report_item.get("chart_key"),
|
|
1147
1331
|
"chart_name": report_item.get("chart_name"),
|
|
1148
1332
|
}
|
|
1333
|
+
if task_id_text is not None:
|
|
1334
|
+
selection["task_id"] = task_id_text
|
|
1149
1335
|
context_payload = {
|
|
1150
1336
|
"match_rules": report_item.get("match_rules") or [],
|
|
1151
1337
|
"resolved_filters": association_query.get("keyQueValues") or [],
|
|
@@ -1292,22 +1478,35 @@ class TaskContextTools(ToolBase):
|
|
|
1292
1478
|
self,
|
|
1293
1479
|
*,
|
|
1294
1480
|
profile: str,
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1481
|
+
task_id: Any = None,
|
|
1482
|
+
app_key: str = "",
|
|
1483
|
+
record_id: Any = "",
|
|
1484
|
+
workflow_node_id: int = 0,
|
|
1298
1485
|
) -> dict[str, Any]:
|
|
1299
1486
|
"""执行任务相关逻辑。"""
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
self._require_app_record_and_node(app_key, record_id_int, workflow_node_id)
|
|
1487
|
+
if task_id in (None, ""):
|
|
1488
|
+
normalize_positive_id_int(record_id, field_name="record_id")
|
|
1303
1489
|
|
|
1304
1490
|
def runner(session_profile, context):
|
|
1305
|
-
|
|
1491
|
+
locator = self._resolve_task_locator_input(
|
|
1306
1492
|
profile=profile,
|
|
1307
|
-
|
|
1493
|
+
task_id=task_id,
|
|
1308
1494
|
app_key=app_key,
|
|
1309
|
-
record_id=
|
|
1495
|
+
record_id=record_id,
|
|
1310
1496
|
workflow_node_id=workflow_node_id,
|
|
1497
|
+
)
|
|
1498
|
+
task_id_text = locator["task_id"]
|
|
1499
|
+
resolved_app_key = str(locator["app_key"])
|
|
1500
|
+
resolved_record_id = int(locator["record_id"])
|
|
1501
|
+
record_id_text = str(locator["record_id_text"] or "")
|
|
1502
|
+
resolved_workflow_node_id = int(locator["workflow_node_id"])
|
|
1503
|
+
self._require_app_record_and_node(resolved_app_key, resolved_record_id, resolved_workflow_node_id)
|
|
1504
|
+
task_context = self._build_task_context(
|
|
1505
|
+
profile=profile,
|
|
1506
|
+
context=context,
|
|
1507
|
+
app_key=resolved_app_key,
|
|
1508
|
+
record_id=resolved_record_id,
|
|
1509
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
1311
1510
|
include_candidates=False,
|
|
1312
1511
|
include_associated_reports=False,
|
|
1313
1512
|
current_uid=session_profile.uid,
|
|
@@ -1316,7 +1515,7 @@ class TaskContextTools(ToolBase):
|
|
|
1316
1515
|
if not visibility.get("audit_record_visible"):
|
|
1317
1516
|
raise_tool_error(
|
|
1318
1517
|
QingflowApiError.config_error(
|
|
1319
|
-
f"workflow logs are not visible for app_key='{
|
|
1518
|
+
f"workflow logs are not visible for app_key='{resolved_app_key}' record_id={record_id_text} workflow_node_id={resolved_workflow_node_id}"
|
|
1320
1519
|
)
|
|
1321
1520
|
)
|
|
1322
1521
|
page = self.backend.request(
|
|
@@ -1324,16 +1523,16 @@ class TaskContextTools(ToolBase):
|
|
|
1324
1523
|
context,
|
|
1325
1524
|
"/application/workflow/node/record",
|
|
1326
1525
|
json_body={
|
|
1327
|
-
"key":
|
|
1328
|
-
"rowRecordId":
|
|
1329
|
-
"nodeId":
|
|
1526
|
+
"key": resolved_app_key,
|
|
1527
|
+
"rowRecordId": resolved_record_id,
|
|
1528
|
+
"nodeId": resolved_workflow_node_id,
|
|
1330
1529
|
"role": 3,
|
|
1331
1530
|
"pageNum": 1,
|
|
1332
1531
|
"pageSize": 200,
|
|
1333
1532
|
},
|
|
1334
1533
|
)
|
|
1335
1534
|
items = self._normalize_workflow_logs(page)
|
|
1336
|
-
|
|
1535
|
+
result = {
|
|
1337
1536
|
"profile": profile,
|
|
1338
1537
|
"ws_id": session_profile.selected_ws_id,
|
|
1339
1538
|
"ok": True,
|
|
@@ -1342,9 +1541,9 @@ class TaskContextTools(ToolBase):
|
|
|
1342
1541
|
"output_profile": "normal",
|
|
1343
1542
|
"data": {
|
|
1344
1543
|
"selection": {
|
|
1345
|
-
"app_key":
|
|
1544
|
+
"app_key": resolved_app_key,
|
|
1346
1545
|
"record_id": record_id_text,
|
|
1347
|
-
"workflow_node_id":
|
|
1546
|
+
"workflow_node_id": resolved_workflow_node_id,
|
|
1348
1547
|
},
|
|
1349
1548
|
"visibility": {
|
|
1350
1549
|
"audit_record_visible": visibility.get("audit_record_visible"),
|
|
@@ -1353,6 +1552,11 @@ class TaskContextTools(ToolBase):
|
|
|
1353
1552
|
"items": items,
|
|
1354
1553
|
},
|
|
1355
1554
|
}
|
|
1555
|
+
if task_id_text is not None:
|
|
1556
|
+
selection = result["data"].get("selection")
|
|
1557
|
+
if isinstance(selection, dict):
|
|
1558
|
+
selection["task_id"] = task_id_text
|
|
1559
|
+
return result
|
|
1356
1560
|
|
|
1357
1561
|
return self._run(profile, runner)
|
|
1358
1562
|
|
|
@@ -1726,7 +1930,7 @@ class TaskContextTools(ToolBase):
|
|
|
1726
1930
|
if value not in (None, "", [])
|
|
1727
1931
|
}
|
|
1728
1932
|
|
|
1729
|
-
def _normalize_task_item(self, raw: dict[str, Any]
|
|
1933
|
+
def _normalize_task_item(self, raw: dict[str, Any]) -> dict[str, Any]:
|
|
1730
1934
|
"""执行内部辅助逻辑。"""
|
|
1731
1935
|
app_key = raw.get("appKey") or raw.get("app_key")
|
|
1732
1936
|
record_id = raw.get("rowRecordId") or raw.get("recordId") or raw.get("applyId")
|
|
@@ -1739,11 +1943,39 @@ class TaskContextTools(ToolBase):
|
|
|
1739
1943
|
"workflow_node_id": workflow_node_id,
|
|
1740
1944
|
"workflow_node_name": raw.get("nodeName") or raw.get("auditNodeName"),
|
|
1741
1945
|
"apply_time": raw.get("applyTime") or raw.get("receiveTime"),
|
|
1742
|
-
"
|
|
1743
|
-
"flow_status": flow_status,
|
|
1744
|
-
"actionable": task_box == "todo" and bool(record_id) and bool(workflow_node_id),
|
|
1946
|
+
"summary_fields": self._normalize_task_summary_fields(raw.get("dataSnapshot")),
|
|
1745
1947
|
}
|
|
1746
1948
|
|
|
1949
|
+
def _public_task_item(self, item: dict[str, Any]) -> dict[str, Any]:
|
|
1950
|
+
return {
|
|
1951
|
+
"task_id": item.get("task_id"),
|
|
1952
|
+
"app_name": item.get("app_name"),
|
|
1953
|
+
"workflow_node_name": item.get("workflow_node_name"),
|
|
1954
|
+
"apply_time": item.get("apply_time"),
|
|
1955
|
+
"summary_fields": item.get("summary_fields") if isinstance(item.get("summary_fields"), list) else [],
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
def _normalize_task_summary_fields(self, raw: Any) -> list[dict[str, Any]]:
|
|
1959
|
+
"""执行内部辅助逻辑。"""
|
|
1960
|
+
if not isinstance(raw, list):
|
|
1961
|
+
return []
|
|
1962
|
+
summary_fields: list[dict[str, Any]] = []
|
|
1963
|
+
for item in raw:
|
|
1964
|
+
if not isinstance(item, dict):
|
|
1965
|
+
continue
|
|
1966
|
+
summary_field: dict[str, Any] = {
|
|
1967
|
+
"field_id": item.get("fieldId"),
|
|
1968
|
+
"title": item.get("fieldTitle"),
|
|
1969
|
+
"type": item.get("fieldType"),
|
|
1970
|
+
"answer": item.get("fieldAnswer"),
|
|
1971
|
+
"desensitized": self._coerce_bool(item.get("beingDesensitized")),
|
|
1972
|
+
}
|
|
1973
|
+
associated_field_type = item.get("associatedQueType")
|
|
1974
|
+
if associated_field_type is not None:
|
|
1975
|
+
summary_field["associated_field_type"] = associated_field_type
|
|
1976
|
+
summary_fields.append(summary_field)
|
|
1977
|
+
return summary_fields
|
|
1978
|
+
|
|
1747
1979
|
def _select_task_node(self, infos: Any, workflow_node_id: int, *, app_key: str, record_id: int) -> dict[str, Any]:
|
|
1748
1980
|
"""执行内部辅助逻辑。"""
|
|
1749
1981
|
if not isinstance(infos, list) or not infos:
|