@josephyan/qingflow-app-user-mcp 0.2.0-beta.994 → 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 +66 -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 +333 -110
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
|
)
|
|
@@ -177,17 +177,15 @@ def _format_task_list(result: dict[str, Any]) -> str:
|
|
|
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
|
-
)
|
|
190
|
-
)
|
|
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))
|
|
191
189
|
summary_fields = item.get("summary_fields") if isinstance(item.get("summary_fields"), list) else []
|
|
192
190
|
for summary in summary_fields:
|
|
193
191
|
if not isinstance(summary, dict):
|
|
@@ -206,15 +204,20 @@ def _format_task_get(result: dict[str, Any]) -> str:
|
|
|
206
204
|
extras = data.get("extras") if isinstance(data.get("extras"), dict) else {}
|
|
207
205
|
initiator = task.get("initiator") if isinstance(task.get("initiator"), dict) else {}
|
|
208
206
|
initiator_label = initiator.get("displayName") or initiator.get("email") or "-"
|
|
209
|
-
lines = [
|
|
210
|
-
|
|
211
|
-
f"
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
+
)
|
|
218
221
|
core_fields = record_summary.get("core_fields") if isinstance(record_summary.get("core_fields"), dict) else {}
|
|
219
222
|
if core_fields:
|
|
220
223
|
lines.append("Core Fields:")
|
|
@@ -246,6 +249,48 @@ def _format_task_get(result: dict[str, Any]) -> str:
|
|
|
246
249
|
return "\n".join(lines) + "\n"
|
|
247
250
|
|
|
248
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
|
+
|
|
249
294
|
def _format_import_verify(result: dict[str, Any]) -> str:
|
|
250
295
|
lines = [
|
|
251
296
|
f"App Key: {result.get('app_key') or '-'}",
|
|
@@ -381,6 +426,7 @@ _FORMATTERS = {
|
|
|
381
426
|
"record_list": _format_record_list,
|
|
382
427
|
"task_list": _format_task_list,
|
|
383
428
|
"task_get": _format_task_get,
|
|
429
|
+
"task_associated_report_detail_get": _format_task_associated_report_detail,
|
|
384
430
|
"import_verify": _format_import_verify,
|
|
385
431
|
"import_status": _format_import_status,
|
|
386
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,25 +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
|
-
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")
|
|
192
195
|
if query and not items:
|
|
193
196
|
fallback = self._task_list_local_query_fallback(
|
|
194
197
|
profile=profile,
|
|
@@ -214,19 +217,20 @@ class TaskContextTools(ToolBase):
|
|
|
214
217
|
),
|
|
215
218
|
}
|
|
216
219
|
)
|
|
220
|
+
public_items = [self._public_task_item(item) for item in items]
|
|
217
221
|
return {
|
|
218
222
|
"profile": profile,
|
|
219
|
-
"ws_id": raw.get("ws_id"),
|
|
223
|
+
"ws_id": response.get("raw", {}).get("ws_id") if isinstance(response.get("raw"), dict) else None,
|
|
220
224
|
"ok": True,
|
|
221
|
-
"request_route": raw.get("request_route"),
|
|
225
|
+
"request_route": response.get("raw", {}).get("request_route") if isinstance(response.get("raw"), dict) else None,
|
|
222
226
|
"warnings": warnings,
|
|
223
227
|
"output_profile": "normal",
|
|
224
228
|
"data": {
|
|
225
|
-
"items":
|
|
229
|
+
"items": public_items,
|
|
226
230
|
"pagination": {
|
|
227
231
|
"page": page,
|
|
228
232
|
"page_size": page_size,
|
|
229
|
-
"returned_items":
|
|
233
|
+
"returned_items": len(public_items),
|
|
230
234
|
"page_amount": page_amount,
|
|
231
235
|
"reported_total": reported_total,
|
|
232
236
|
},
|
|
@@ -243,28 +247,44 @@ class TaskContextTools(ToolBase):
|
|
|
243
247
|
self,
|
|
244
248
|
*,
|
|
245
249
|
profile: str,
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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,
|
|
251
256
|
) -> dict[str, Any]:
|
|
252
257
|
"""执行任务相关逻辑。"""
|
|
253
|
-
|
|
254
|
-
|
|
258
|
+
if task_id in (None, ""):
|
|
259
|
+
normalize_positive_id_int(record_id, field_name="record_id")
|
|
255
260
|
|
|
256
261
|
def runner(session_profile, context):
|
|
257
|
-
|
|
262
|
+
locator = self._resolve_task_locator_input(
|
|
258
263
|
profile=profile,
|
|
259
|
-
|
|
264
|
+
task_id=task_id,
|
|
260
265
|
app_key=app_key,
|
|
261
|
-
record_id=
|
|
266
|
+
record_id=record_id,
|
|
262
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,
|
|
263
280
|
include_candidates=include_candidates,
|
|
264
281
|
include_associated_reports=include_associated_reports,
|
|
265
282
|
current_uid=session_profile.uid,
|
|
266
283
|
)
|
|
267
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
|
|
268
288
|
return {
|
|
269
289
|
"profile": profile,
|
|
270
290
|
"ws_id": session_profile.selected_ws_id,
|
|
@@ -306,17 +326,18 @@ class TaskContextTools(ToolBase):
|
|
|
306
326
|
self,
|
|
307
327
|
*,
|
|
308
328
|
profile: str,
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
329
|
+
task_id: Any = None,
|
|
330
|
+
app_key: str = "",
|
|
331
|
+
record_id: Any = "",
|
|
332
|
+
workflow_node_id: int = 0,
|
|
312
333
|
action: str,
|
|
313
334
|
payload: dict[str, Any],
|
|
314
335
|
fields: dict[str, Any] | None = None,
|
|
315
336
|
) -> dict[str, Any]:
|
|
316
337
|
"""执行任务相关逻辑。"""
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
338
|
+
if task_id in (None, ""):
|
|
339
|
+
normalize_positive_id_int(record_id, field_name="record_id")
|
|
340
|
+
|
|
320
341
|
normalized_action = (action or "").strip().lower()
|
|
321
342
|
if normalized_action not in {"approve", "reject", "rollback", "transfer", "urge", "save_only"}:
|
|
322
343
|
raise_tool_error(
|
|
@@ -334,13 +355,27 @@ class TaskContextTools(ToolBase):
|
|
|
334
355
|
)
|
|
335
356
|
|
|
336
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)
|
|
337
372
|
try:
|
|
338
373
|
task_context = self._build_task_context(
|
|
339
374
|
profile=profile,
|
|
340
375
|
context=context,
|
|
341
|
-
app_key=
|
|
342
|
-
record_id=
|
|
343
|
-
workflow_node_id=
|
|
376
|
+
app_key=resolved_app_key,
|
|
377
|
+
record_id=resolved_record_id,
|
|
378
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
344
379
|
include_candidates=False,
|
|
345
380
|
include_associated_reports=False,
|
|
346
381
|
current_uid=session_profile.uid,
|
|
@@ -351,12 +386,13 @@ class TaskContextTools(ToolBase):
|
|
|
351
386
|
profile=profile,
|
|
352
387
|
session_profile=session_profile,
|
|
353
388
|
context=context,
|
|
354
|
-
app_key=
|
|
355
|
-
record_id=
|
|
356
|
-
workflow_node_id=
|
|
389
|
+
app_key=resolved_app_key,
|
|
390
|
+
record_id=resolved_record_id,
|
|
391
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
357
392
|
action=normalized_action,
|
|
358
393
|
source_error=error,
|
|
359
394
|
before_apply_status=None,
|
|
395
|
+
task_id=task_id_text,
|
|
360
396
|
)
|
|
361
397
|
raise
|
|
362
398
|
if normalized_action == "save_only" and not field_updates:
|
|
@@ -385,7 +421,7 @@ class TaskContextTools(ToolBase):
|
|
|
385
421
|
raise_tool_error(QingflowApiError.config_error(message))
|
|
386
422
|
raise_tool_error(
|
|
387
423
|
QingflowApiError.config_error(
|
|
388
|
-
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}"
|
|
389
425
|
)
|
|
390
426
|
)
|
|
391
427
|
feedback_required_for = capabilities.get("action_constraints", {}).get("feedback_required_for") or []
|
|
@@ -406,9 +442,9 @@ class TaskContextTools(ToolBase):
|
|
|
406
442
|
prepared_fields = self._prepare_task_field_update(
|
|
407
443
|
profile=profile,
|
|
408
444
|
context=context,
|
|
409
|
-
app_key=
|
|
410
|
-
record_id=
|
|
411
|
-
workflow_node_id=
|
|
445
|
+
app_key=resolved_app_key,
|
|
446
|
+
record_id=resolved_record_id,
|
|
447
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
412
448
|
task_context=task_context,
|
|
413
449
|
fields=field_updates,
|
|
414
450
|
)
|
|
@@ -418,16 +454,16 @@ class TaskContextTools(ToolBase):
|
|
|
418
454
|
runtime_baseline = self._capture_task_runtime_baseline(
|
|
419
455
|
profile=profile,
|
|
420
456
|
context=context,
|
|
421
|
-
app_key=
|
|
422
|
-
record_id=
|
|
423
|
-
workflow_node_id=
|
|
457
|
+
app_key=resolved_app_key,
|
|
458
|
+
record_id=resolved_record_id,
|
|
459
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
424
460
|
)
|
|
425
461
|
try:
|
|
426
462
|
raw = self._execute_task_action(
|
|
427
463
|
profile=profile,
|
|
428
|
-
app_key=
|
|
429
|
-
record_id=
|
|
430
|
-
workflow_node_id=
|
|
464
|
+
app_key=resolved_app_key,
|
|
465
|
+
record_id=resolved_record_id,
|
|
466
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
431
467
|
normalized_action=normalized_action,
|
|
432
468
|
payload=body,
|
|
433
469
|
prepared_fields=prepared_fields,
|
|
@@ -438,21 +474,22 @@ class TaskContextTools(ToolBase):
|
|
|
438
474
|
profile=profile,
|
|
439
475
|
session_profile=session_profile,
|
|
440
476
|
context=context,
|
|
441
|
-
app_key=
|
|
442
|
-
record_id=
|
|
443
|
-
workflow_node_id=
|
|
477
|
+
app_key=resolved_app_key,
|
|
478
|
+
record_id=resolved_record_id,
|
|
479
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
444
480
|
action=normalized_action,
|
|
445
481
|
source_error=error,
|
|
446
482
|
before_apply_status=before_apply_status,
|
|
483
|
+
task_id=task_id_text,
|
|
447
484
|
)
|
|
448
485
|
raise
|
|
449
486
|
|
|
450
487
|
if normalized_action == "save_only":
|
|
451
488
|
verification, warnings = self._verify_task_save_only(
|
|
452
489
|
context=context,
|
|
453
|
-
app_key=
|
|
454
|
-
record_id=
|
|
455
|
-
workflow_node_id=
|
|
490
|
+
app_key=resolved_app_key,
|
|
491
|
+
record_id=resolved_record_id,
|
|
492
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
456
493
|
before_apply_status=before_apply_status,
|
|
457
494
|
expected_answers=((prepared_fields or {}).get("normalized_answers") or []),
|
|
458
495
|
task_context=task_context,
|
|
@@ -464,9 +501,9 @@ class TaskContextTools(ToolBase):
|
|
|
464
501
|
verification, warnings = self._verify_task_action_runtime(
|
|
465
502
|
profile=profile,
|
|
466
503
|
context=context,
|
|
467
|
-
app_key=
|
|
468
|
-
record_id=
|
|
469
|
-
workflow_node_id=
|
|
504
|
+
app_key=resolved_app_key,
|
|
505
|
+
record_id=resolved_record_id,
|
|
506
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
470
507
|
action=normalized_action,
|
|
471
508
|
before_apply_status=before_apply_status,
|
|
472
509
|
runtime_baseline=runtime_baseline,
|
|
@@ -474,7 +511,7 @@ class TaskContextTools(ToolBase):
|
|
|
474
511
|
runtime_verified = bool(verification.get("runtime_continuation_verified"))
|
|
475
512
|
status = "success" if runtime_verified else "partial_success"
|
|
476
513
|
error_code = None if runtime_verified else "WORKFLOW_CONTINUATION_UNVERIFIED"
|
|
477
|
-
|
|
514
|
+
result = {
|
|
478
515
|
"profile": raw.get("profile", profile),
|
|
479
516
|
"ws_id": raw.get("ws_id", session_profile.selected_ws_id),
|
|
480
517
|
"ok": bool(raw.get("ok", True)) and status != "failed",
|
|
@@ -487,9 +524,9 @@ class TaskContextTools(ToolBase):
|
|
|
487
524
|
"data": {
|
|
488
525
|
"action": normalized_action,
|
|
489
526
|
"resource": {
|
|
490
|
-
"app_key":
|
|
527
|
+
"app_key": resolved_app_key,
|
|
491
528
|
"record_id": record_id_text,
|
|
492
|
-
"workflow_node_id":
|
|
529
|
+
"workflow_node_id": resolved_workflow_node_id,
|
|
493
530
|
},
|
|
494
531
|
"selection": {"action": normalized_action},
|
|
495
532
|
"result": raw.get("result"),
|
|
@@ -497,6 +534,11 @@ class TaskContextTools(ToolBase):
|
|
|
497
534
|
"field_update_applied": bool(field_updates),
|
|
498
535
|
},
|
|
499
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
|
|
500
542
|
|
|
501
543
|
return self._run(profile, runner)
|
|
502
544
|
|
|
@@ -914,6 +956,7 @@ class TaskContextTools(ToolBase):
|
|
|
914
956
|
action: str,
|
|
915
957
|
source_error: QingflowApiError,
|
|
916
958
|
before_apply_status: Any,
|
|
959
|
+
task_id: str | None = None,
|
|
917
960
|
) -> dict[str, Any]:
|
|
918
961
|
"""执行内部辅助逻辑。"""
|
|
919
962
|
record_id_text = stringify_backend_id(record_id)
|
|
@@ -935,7 +978,7 @@ class TaskContextTools(ToolBase):
|
|
|
935
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.",
|
|
936
979
|
}
|
|
937
980
|
)
|
|
938
|
-
|
|
981
|
+
result = {
|
|
939
982
|
"profile": profile,
|
|
940
983
|
"ws_id": session_profile.selected_ws_id,
|
|
941
984
|
"ok": True,
|
|
@@ -957,13 +1000,18 @@ class TaskContextTools(ToolBase):
|
|
|
957
1000
|
"human_review": True,
|
|
958
1001
|
},
|
|
959
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
|
|
960
1008
|
warnings.append(
|
|
961
1009
|
{
|
|
962
1010
|
"code": "TASK_CONTEXT_VISIBILITY_UNVERIFIED",
|
|
963
1011
|
"message": "the task is no longer actionable, and MCP could not verify from state or workflow logs whether it was already processed.",
|
|
964
1012
|
}
|
|
965
1013
|
)
|
|
966
|
-
|
|
1014
|
+
result = {
|
|
967
1015
|
"profile": profile,
|
|
968
1016
|
"ws_id": session_profile.selected_ws_id,
|
|
969
1017
|
"ok": False,
|
|
@@ -990,11 +1038,16 @@ class TaskContextTools(ToolBase):
|
|
|
990
1038
|
},
|
|
991
1039
|
},
|
|
992
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
|
|
993
1046
|
|
|
994
1047
|
def _safe_task_list_items(self, *, profile: str, task_box: str, app_key: str) -> list[dict[str, Any]]:
|
|
995
1048
|
"""执行内部辅助逻辑。"""
|
|
996
1049
|
try:
|
|
997
|
-
response = self.
|
|
1050
|
+
response = self._list_normalized_task_items(
|
|
998
1051
|
profile=profile,
|
|
999
1052
|
task_box=task_box,
|
|
1000
1053
|
flow_status="all",
|
|
@@ -1006,12 +1059,44 @@ class TaskContextTools(ToolBase):
|
|
|
1006
1059
|
)
|
|
1007
1060
|
except QingflowApiError:
|
|
1008
1061
|
return []
|
|
1009
|
-
|
|
1010
|
-
items = data.get("items") if isinstance(data, dict) else None
|
|
1062
|
+
items = response.get("items") if isinstance(response, dict) else None
|
|
1011
1063
|
if not isinstance(items, list):
|
|
1012
1064
|
return []
|
|
1013
1065
|
return [item for item in items if isinstance(item, dict)]
|
|
1014
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
|
+
|
|
1015
1100
|
def _task_list_local_query_fallback(
|
|
1016
1101
|
self,
|
|
1017
1102
|
*,
|
|
@@ -1024,35 +1109,30 @@ class TaskContextTools(ToolBase):
|
|
|
1024
1109
|
page: int,
|
|
1025
1110
|
page_size: int,
|
|
1026
1111
|
) -> dict[str, Any] | None:
|
|
1027
|
-
normalized_type = self._task_tools._task_box_to_type(task_box)
|
|
1028
|
-
normalized_status = self._task_tools._flow_status_to_process_status(flow_status)
|
|
1029
1112
|
scan_page_size = max(page_size, 100)
|
|
1030
1113
|
scan_page = 1
|
|
1031
1114
|
page_amount: int | None = None
|
|
1032
1115
|
matched_items: list[dict[str, Any]] = []
|
|
1033
1116
|
while True:
|
|
1034
|
-
|
|
1117
|
+
response = self._list_normalized_task_items(
|
|
1035
1118
|
profile=profile,
|
|
1036
|
-
|
|
1037
|
-
|
|
1119
|
+
task_box=task_box,
|
|
1120
|
+
flow_status=flow_status,
|
|
1038
1121
|
app_key=app_key,
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1122
|
+
workflow_node_id=workflow_node_id,
|
|
1123
|
+
query=None,
|
|
1124
|
+
page=scan_page,
|
|
1042
1125
|
page_size=scan_page_size,
|
|
1043
|
-
create_time_asc=None,
|
|
1044
1126
|
)
|
|
1045
|
-
|
|
1046
|
-
raw_items = _task_page_items(task_page)
|
|
1047
|
-
normalized_items = [self._normalize_task_item(item) for item in raw_items if isinstance(item, dict)]
|
|
1127
|
+
normalized_items = response.get("items") if isinstance(response.get("items"), list) else []
|
|
1048
1128
|
matched_items.extend(item for item in normalized_items if self._task_item_matches_query(item, query))
|
|
1049
1129
|
if page_amount is None:
|
|
1050
|
-
coerced_page_amount = _coerce_count(
|
|
1130
|
+
coerced_page_amount = _coerce_count(response.get("page_amount"))
|
|
1051
1131
|
if coerced_page_amount is not None and coerced_page_amount > 0:
|
|
1052
1132
|
page_amount = coerced_page_amount
|
|
1053
1133
|
if page_amount is not None and scan_page >= page_amount:
|
|
1054
1134
|
break
|
|
1055
|
-
if not
|
|
1135
|
+
if not normalized_items or len(normalized_items) < scan_page_size:
|
|
1056
1136
|
break
|
|
1057
1137
|
scan_page += 1
|
|
1058
1138
|
if not matched_items:
|
|
@@ -1067,6 +1147,106 @@ class TaskContextTools(ToolBase):
|
|
|
1067
1147
|
"reported_total": matched_total,
|
|
1068
1148
|
}
|
|
1069
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
|
+
|
|
1070
1250
|
def _task_item_matches_query(self, item: dict[str, Any], query: str) -> bool:
|
|
1071
1251
|
needle = str(query or "").strip().casefold()
|
|
1072
1252
|
if not needle:
|
|
@@ -1088,29 +1268,43 @@ class TaskContextTools(ToolBase):
|
|
|
1088
1268
|
self,
|
|
1089
1269
|
*,
|
|
1090
1270
|
profile: str,
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1271
|
+
task_id: Any = None,
|
|
1272
|
+
app_key: str = "",
|
|
1273
|
+
record_id: Any = "",
|
|
1274
|
+
workflow_node_id: int = 0,
|
|
1094
1275
|
report_id: int,
|
|
1095
1276
|
page: int,
|
|
1096
1277
|
page_size: int,
|
|
1097
1278
|
) -> dict[str, Any]:
|
|
1098
1279
|
"""执行任务相关逻辑。"""
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1280
|
+
if task_id in (None, ""):
|
|
1281
|
+
normalize_positive_id_int(record_id, field_name="record_id")
|
|
1282
|
+
|
|
1102
1283
|
if report_id <= 0:
|
|
1103
1284
|
raise_tool_error(QingflowApiError.config_error("report_id must be positive"))
|
|
1104
1285
|
if page <= 0 or page_size <= 0:
|
|
1105
1286
|
raise_tool_error(QingflowApiError.config_error("page and page_size must be positive"))
|
|
1106
1287
|
|
|
1107
1288
|
def runner(session_profile, context):
|
|
1108
|
-
|
|
1289
|
+
locator = self._resolve_task_locator_input(
|
|
1109
1290
|
profile=profile,
|
|
1110
|
-
|
|
1291
|
+
task_id=task_id,
|
|
1111
1292
|
app_key=app_key,
|
|
1112
|
-
record_id=
|
|
1293
|
+
record_id=record_id,
|
|
1113
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,
|
|
1114
1308
|
include_candidates=False,
|
|
1115
1309
|
include_associated_reports=True,
|
|
1116
1310
|
current_uid=session_profile.uid,
|
|
@@ -1119,7 +1313,7 @@ class TaskContextTools(ToolBase):
|
|
|
1119
1313
|
if report_item is None:
|
|
1120
1314
|
raise_tool_error(
|
|
1121
1315
|
QingflowApiError.config_error(
|
|
1122
|
-
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}"
|
|
1123
1317
|
)
|
|
1124
1318
|
)
|
|
1125
1319
|
association_query = self._build_association_query(
|
|
@@ -1127,15 +1321,17 @@ class TaskContextTools(ToolBase):
|
|
|
1127
1321
|
task_context.get("record", {}).get("answers") or [],
|
|
1128
1322
|
)
|
|
1129
1323
|
selection = {
|
|
1130
|
-
"app_key":
|
|
1324
|
+
"app_key": resolved_app_key,
|
|
1131
1325
|
"record_id": record_id_text,
|
|
1132
|
-
"workflow_node_id":
|
|
1326
|
+
"workflow_node_id": resolved_workflow_node_id,
|
|
1133
1327
|
"report_id": report_id,
|
|
1134
1328
|
"target_app_key": report_item.get("target_app_key"),
|
|
1135
1329
|
"target_app_name": report_item.get("target_app_name"),
|
|
1136
1330
|
"chart_key": report_item.get("chart_key"),
|
|
1137
1331
|
"chart_name": report_item.get("chart_name"),
|
|
1138
1332
|
}
|
|
1333
|
+
if task_id_text is not None:
|
|
1334
|
+
selection["task_id"] = task_id_text
|
|
1139
1335
|
context_payload = {
|
|
1140
1336
|
"match_rules": report_item.get("match_rules") or [],
|
|
1141
1337
|
"resolved_filters": association_query.get("keyQueValues") or [],
|
|
@@ -1282,22 +1478,35 @@ class TaskContextTools(ToolBase):
|
|
|
1282
1478
|
self,
|
|
1283
1479
|
*,
|
|
1284
1480
|
profile: str,
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1481
|
+
task_id: Any = None,
|
|
1482
|
+
app_key: str = "",
|
|
1483
|
+
record_id: Any = "",
|
|
1484
|
+
workflow_node_id: int = 0,
|
|
1288
1485
|
) -> dict[str, Any]:
|
|
1289
1486
|
"""执行任务相关逻辑。"""
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
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")
|
|
1293
1489
|
|
|
1294
1490
|
def runner(session_profile, context):
|
|
1295
|
-
|
|
1491
|
+
locator = self._resolve_task_locator_input(
|
|
1296
1492
|
profile=profile,
|
|
1297
|
-
|
|
1493
|
+
task_id=task_id,
|
|
1298
1494
|
app_key=app_key,
|
|
1299
|
-
record_id=
|
|
1495
|
+
record_id=record_id,
|
|
1300
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,
|
|
1301
1510
|
include_candidates=False,
|
|
1302
1511
|
include_associated_reports=False,
|
|
1303
1512
|
current_uid=session_profile.uid,
|
|
@@ -1306,7 +1515,7 @@ class TaskContextTools(ToolBase):
|
|
|
1306
1515
|
if not visibility.get("audit_record_visible"):
|
|
1307
1516
|
raise_tool_error(
|
|
1308
1517
|
QingflowApiError.config_error(
|
|
1309
|
-
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}"
|
|
1310
1519
|
)
|
|
1311
1520
|
)
|
|
1312
1521
|
page = self.backend.request(
|
|
@@ -1314,16 +1523,16 @@ class TaskContextTools(ToolBase):
|
|
|
1314
1523
|
context,
|
|
1315
1524
|
"/application/workflow/node/record",
|
|
1316
1525
|
json_body={
|
|
1317
|
-
"key":
|
|
1318
|
-
"rowRecordId":
|
|
1319
|
-
"nodeId":
|
|
1526
|
+
"key": resolved_app_key,
|
|
1527
|
+
"rowRecordId": resolved_record_id,
|
|
1528
|
+
"nodeId": resolved_workflow_node_id,
|
|
1320
1529
|
"role": 3,
|
|
1321
1530
|
"pageNum": 1,
|
|
1322
1531
|
"pageSize": 200,
|
|
1323
1532
|
},
|
|
1324
1533
|
)
|
|
1325
1534
|
items = self._normalize_workflow_logs(page)
|
|
1326
|
-
|
|
1535
|
+
result = {
|
|
1327
1536
|
"profile": profile,
|
|
1328
1537
|
"ws_id": session_profile.selected_ws_id,
|
|
1329
1538
|
"ok": True,
|
|
@@ -1332,9 +1541,9 @@ class TaskContextTools(ToolBase):
|
|
|
1332
1541
|
"output_profile": "normal",
|
|
1333
1542
|
"data": {
|
|
1334
1543
|
"selection": {
|
|
1335
|
-
"app_key":
|
|
1544
|
+
"app_key": resolved_app_key,
|
|
1336
1545
|
"record_id": record_id_text,
|
|
1337
|
-
"workflow_node_id":
|
|
1546
|
+
"workflow_node_id": resolved_workflow_node_id,
|
|
1338
1547
|
},
|
|
1339
1548
|
"visibility": {
|
|
1340
1549
|
"audit_record_visible": visibility.get("audit_record_visible"),
|
|
@@ -1343,6 +1552,11 @@ class TaskContextTools(ToolBase):
|
|
|
1343
1552
|
"items": items,
|
|
1344
1553
|
},
|
|
1345
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
|
|
1346
1560
|
|
|
1347
1561
|
return self._run(profile, runner)
|
|
1348
1562
|
|
|
@@ -1732,6 +1946,15 @@ class TaskContextTools(ToolBase):
|
|
|
1732
1946
|
"summary_fields": self._normalize_task_summary_fields(raw.get("dataSnapshot")),
|
|
1733
1947
|
}
|
|
1734
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
|
+
|
|
1735
1958
|
def _normalize_task_summary_fields(self, raw: Any) -> list[dict[str, Any]]:
|
|
1736
1959
|
"""执行内部辅助逻辑。"""
|
|
1737
1960
|
if not isinstance(raw, list):
|