@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 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.994
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.994 qingflow-app-user-mcp
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-app-user-mcp",
3
- "version": "0.2.0-beta.994",
3
+ "version": "0.2.0-beta.995",
4
4
  "description": "Operational end-user MCP for Qingflow records, tasks, comments, and directory workflows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qingflow-mcp"
7
- version = "0.2.0b994"
7
+ version = "0.2.0b995"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -5,7 +5,7 @@ from pathlib import Path
5
5
 
6
6
  __all__ = ["__version__"]
7
7
 
8
- _FALLBACK_VERSION = "0.2.0b994"
8
+ _FALLBACK_VERSION = "0.2.0b995"
9
9
 
10
10
 
11
11
  def _resolve_local_pyproject_version() -> str | None:
@@ -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("--app-key", required=True)
28
- get.add_argument("--record-id", required=True)
29
- get.add_argument("--workflow-node-id", required=True, type=int)
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("--app-key", required=True)
36
- action.add_argument("--record-id", required=True)
37
- action.add_argument("--workflow-node-id", required=True, type=int)
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
- log = task_subparsers.add_parser("log", help="读取流程日志")
44
- log.add_argument("--app-key", required=True)
45
- log.add_argument("--record-id", required=True)
46
- log.add_argument("--workflow-node-id", required=True, type=int)
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
- app_key=args.app_key,
67
- record_id=args.record_id,
68
- workflow_node_id=args.workflow_node_id,
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
- app_key=args.app_key,
78
- record_id=args.record_id,
79
- workflow_node_id=args.workflow_node_id,
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
- app_key=args.app_key,
90
- record_id=args.record_id,
91
- workflow_node_id=args.workflow_node_id,
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
- lines.append(
181
- "- "
182
- + " / ".join(
183
- [
184
- str(item.get("app_key") or "-"),
185
- str(item.get("record_id") or "-"),
186
- str(item.get("workflow_node_id") or "-"),
187
- str(item.get("workflow_node_name") or "-"),
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
- f"Task: {task.get('app_key') or '-'} / {task.get('record_id') or '-'} / {task.get('workflow_node_id') or '-'}",
211
- f"Node: {task.get('workflow_node_name') or '-'}",
212
- f"App: {task.get('app_name') or '-'}",
213
- f"Initiator: {initiator_label}",
214
- f"Apply Status: {record_summary.get('apply_status')}",
215
- f"Available Actions: {', '.join(str(item) for item in available_actions) or '-'}",
216
- f"Editable Fields: {len(editable_fields)}",
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(USER_DOMAIN, "task_associated_report_detail_get", ("task_associated_report_detail_get",), cli_public=False, cli_show_effective_context=True),
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
- normalized_type = self._task_tools._task_box_to_type(task_box)
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
- type=normalized_type,
178
- process_status=normalized_status,
183
+ task_box=task_box,
184
+ flow_status=flow_status,
179
185
  app_key=app_key,
180
- node_id=workflow_node_id,
181
- search_key=query,
182
- page_num=page,
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 = [self._normalize_task_item(item) for item in _task_page_items(task_page) if isinstance(item, dict)]
189
- returned_items = len(items)
190
- page_amount = _task_page_amount(task_page)
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": items,
229
+ "items": public_items,
226
230
  "pagination": {
227
231
  "page": page,
228
232
  "page_size": page_size,
229
- "returned_items": 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
- app_key: str,
247
- record_id: Any,
248
- workflow_node_id: int,
249
- include_candidates: bool,
250
- include_associated_reports: bool,
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
- record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
254
- self._require_app_record_and_node(app_key, record_id_int, workflow_node_id)
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
- data = self._build_task_context(
262
+ locator = self._resolve_task_locator_input(
258
263
  profile=profile,
259
- context=context,
264
+ task_id=task_id,
260
265
  app_key=app_key,
261
- record_id=record_id_int,
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
- app_key: str,
310
- record_id: Any,
311
- workflow_node_id: int,
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
- record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
318
- record_id_text = stringify_backend_id(record_id_int)
319
- self._require_app_record_and_node(app_key, record_id_int, workflow_node_id)
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=app_key,
342
- record_id=record_id_int,
343
- workflow_node_id=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=app_key,
355
- record_id=record_id_int,
356
- workflow_node_id=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='{app_key}' record_id={record_id_text} workflow_node_id={workflow_node_id}"
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=app_key,
410
- record_id=record_id_int,
411
- workflow_node_id=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=app_key,
422
- record_id=record_id_int,
423
- workflow_node_id=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=app_key,
429
- record_id=record_id_int,
430
- workflow_node_id=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=app_key,
442
- record_id=record_id_int,
443
- workflow_node_id=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=app_key,
454
- record_id=record_id_int,
455
- workflow_node_id=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=app_key,
468
- record_id=record_id_int,
469
- workflow_node_id=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
- return {
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": app_key,
527
+ "app_key": resolved_app_key,
491
528
  "record_id": record_id_text,
492
- "workflow_node_id": 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
- return {
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
- return {
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.task_list(
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
- data = response.get("data") if isinstance(response, dict) else None
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
- raw = self._task_tools.task_list(
1117
+ response = self._list_normalized_task_items(
1035
1118
  profile=profile,
1036
- type=normalized_type,
1037
- process_status=normalized_status,
1119
+ task_box=task_box,
1120
+ flow_status=flow_status,
1038
1121
  app_key=app_key,
1039
- node_id=workflow_node_id,
1040
- search_key=None,
1041
- page_num=scan_page,
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
- task_page = raw.get("page", {})
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(_task_page_amount(task_page))
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 raw_items or len(raw_items) < scan_page_size:
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
- app_key: str,
1092
- record_id: int,
1093
- workflow_node_id: int,
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
- record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
1100
- record_id_text = stringify_backend_id(record_id_int)
1101
- self._require_app_record_and_node(app_key, record_id_int, workflow_node_id)
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
- task_context = self._build_task_context(
1289
+ locator = self._resolve_task_locator_input(
1109
1290
  profile=profile,
1110
- context=context,
1291
+ task_id=task_id,
1111
1292
  app_key=app_key,
1112
- record_id=record_id_int,
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='{app_key}' record_id={record_id_text} workflow_node_id={workflow_node_id}"
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": app_key,
1324
+ "app_key": resolved_app_key,
1131
1325
  "record_id": record_id_text,
1132
- "workflow_node_id": 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
- app_key: str,
1286
- record_id: Any,
1287
- workflow_node_id: int,
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
- record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
1291
- record_id_text = stringify_backend_id(record_id_int)
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
- task_context = self._build_task_context(
1491
+ locator = self._resolve_task_locator_input(
1296
1492
  profile=profile,
1297
- context=context,
1493
+ task_id=task_id,
1298
1494
  app_key=app_key,
1299
- record_id=record_id_int,
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='{app_key}' record_id={record_id_text} workflow_node_id={workflow_node_id}"
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": app_key,
1318
- "rowRecordId": record_id_int,
1319
- "nodeId": workflow_node_id,
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
- return {
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": app_key,
1544
+ "app_key": resolved_app_key,
1336
1545
  "record_id": record_id_text,
1337
- "workflow_node_id": 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):