@josephyan/qingflow-app-user-mcp 0.2.0-beta.993 → 0.2.0-beta.995

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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.993
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.993 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.993",
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.0b993"
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.0b993"
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
  )
@@ -173,20 +173,24 @@ def _format_record_list(result: dict[str, Any]) -> str:
173
173
  def _format_task_list(result: dict[str, Any]) -> str:
174
174
  data = result.get("data") if isinstance(result.get("data"), dict) else {}
175
175
  items = data.get("items") if isinstance(data.get("items"), list) else []
176
- rows = []
176
+ lines = ["Tasks"]
177
177
  for item in items:
178
178
  if not isinstance(item, dict):
179
179
  continue
180
- rows.append(
181
- [
182
- str(item.get("app_key") or ""),
183
- str(item.get("record_id") or ""),
184
- str(item.get("workflow_node_id") or ""),
185
- str(item.get("workflow_node_name") or ""),
186
- ]
187
- )
188
- output = _render_titled_table("Tasks", ["app_key", "record_id", "node_id", "node_name"], rows)
189
- lines = output.rstrip("\n").split("\n")
180
+ header_parts = [
181
+ str(item.get("task_id") or "-"),
182
+ str(item.get("app_name") or item.get("app_key") or "-"),
183
+ str(item.get("workflow_node_name") or "-"),
184
+ ]
185
+ apply_time = item.get("apply_time")
186
+ if apply_time not in (None, ""):
187
+ header_parts.append(str(apply_time))
188
+ lines.append("- " + " / ".join(header_parts))
189
+ summary_fields = item.get("summary_fields") if isinstance(item.get("summary_fields"), list) else []
190
+ for summary in summary_fields:
191
+ if not isinstance(summary, dict):
192
+ continue
193
+ lines.append(f" {summary.get('title') or '-'}: {summary.get('answer') or '-'}")
190
194
  _append_warnings(lines, result.get("warnings"))
191
195
  return "\n".join(lines) + "\n"
192
196
 
@@ -200,15 +204,20 @@ def _format_task_get(result: dict[str, Any]) -> str:
200
204
  extras = data.get("extras") if isinstance(data.get("extras"), dict) else {}
201
205
  initiator = task.get("initiator") if isinstance(task.get("initiator"), dict) else {}
202
206
  initiator_label = initiator.get("displayName") or initiator.get("email") or "-"
203
- lines = [
204
- f"Task: {task.get('app_key') or '-'} / {task.get('record_id') or '-'} / {task.get('workflow_node_id') or '-'}",
205
- f"Node: {task.get('workflow_node_name') or '-'}",
206
- f"App: {task.get('app_name') or '-'}",
207
- f"Initiator: {initiator_label}",
208
- f"Apply Status: {record_summary.get('apply_status')}",
209
- f"Available Actions: {', '.join(str(item) for item in available_actions) or '-'}",
210
- f"Editable Fields: {len(editable_fields)}",
211
- ]
207
+ lines = []
208
+ if task.get("task_id") not in (None, ""):
209
+ lines.append(f"Task ID: {task.get('task_id')}")
210
+ lines.extend(
211
+ [
212
+ f"Locator: {task.get('app_key') or '-'} / {task.get('record_id') or '-'} / {task.get('workflow_node_id') or '-'}",
213
+ f"Node: {task.get('workflow_node_name') or '-'}",
214
+ f"App: {task.get('app_name') or '-'}",
215
+ f"Initiator: {initiator_label}",
216
+ f"Apply Status: {record_summary.get('apply_status')}",
217
+ f"Available Actions: {', '.join(str(item) for item in available_actions) or '-'}",
218
+ f"Editable Fields: {len(editable_fields)}",
219
+ ]
220
+ )
212
221
  core_fields = record_summary.get("core_fields") if isinstance(record_summary.get("core_fields"), dict) else {}
213
222
  if core_fields:
214
223
  lines.append("Core Fields:")
@@ -240,6 +249,48 @@ def _format_task_get(result: dict[str, Any]) -> str:
240
249
  return "\n".join(lines) + "\n"
241
250
 
242
251
 
252
+ def _format_task_associated_report_detail(result: dict[str, Any]) -> str:
253
+ data = result.get("data") if isinstance(result.get("data"), dict) else {}
254
+ selection = data.get("selection") if isinstance(data.get("selection"), dict) else {}
255
+ result_type = str(data.get("result_type") or "-")
256
+ context = data.get("context") if isinstance(data.get("context"), dict) else {}
257
+ lines = []
258
+ if selection.get("task_id") not in (None, ""):
259
+ lines.append(f"Task ID: {selection.get('task_id')}")
260
+ lines.extend(
261
+ [
262
+ f"Report: {selection.get('chart_name') or '-'} ({selection.get('report_id') or '-'})",
263
+ f"Type: {result_type}",
264
+ ]
265
+ )
266
+ if result_type == "view_list":
267
+ result_payload = data.get("result") if isinstance(data.get("result"), dict) else {}
268
+ items = result_payload.get("items") if isinstance(result_payload.get("items"), list) else []
269
+ lines.append(f"Returned Records: {len(items)}")
270
+ for item in items[:10]:
271
+ if isinstance(item, dict):
272
+ lines.append(json.dumps(item, ensure_ascii=False))
273
+ if len(items) > 10:
274
+ lines.append(f"... {len(items) - 10} more")
275
+ elif result_type == "chart_data":
276
+ result_payload = data.get("result") if isinstance(data.get("result"), dict) else {}
277
+ summary = result_payload.get("summary") if isinstance(result_payload.get("summary"), dict) else {}
278
+ rows = result_payload.get("rows") if isinstance(result_payload.get("rows"), list) else []
279
+ if summary:
280
+ lines.append(f"Summary: {json.dumps(summary, ensure_ascii=False)}")
281
+ lines.append(f"Rows: {len(rows)}")
282
+ for row in rows[:10]:
283
+ if isinstance(row, dict):
284
+ lines.append(json.dumps(row, ensure_ascii=False))
285
+ if len(rows) > 10:
286
+ lines.append(f"... {len(rows) - 10} more")
287
+ resolved_filters = context.get("resolved_filters") if isinstance(context.get("resolved_filters"), list) else []
288
+ if resolved_filters:
289
+ lines.append(f"Resolved Filters: {len(resolved_filters)}")
290
+ _append_warnings(lines, result.get("warnings"))
291
+ return "\n".join(lines) + "\n"
292
+
293
+
243
294
  def _format_import_verify(result: dict[str, Any]) -> str:
244
295
  lines = [
245
296
  f"App Key: {result.get('app_key') or '-'}",
@@ -375,6 +426,7 @@ _FORMATTERS = {
375
426
  "record_list": _format_record_list,
376
427
  "task_list": _format_task_list,
377
428
  "task_get": _format_task_get,
429
+ "task_associated_report_detail_get": _format_task_associated_report_detail,
378
430
  "import_verify": _format_import_verify,
379
431
  "import_status": _format_import_status,
380
432
  "builder_summary": _format_builder_summary,
@@ -94,7 +94,13 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
94
94
  PublicToolSpec(USER_DOMAIN, "task_list", ("task_list",), ("task", "list"), cli_show_effective_context=True),
95
95
  PublicToolSpec(USER_DOMAIN, "task_get", ("task_get",), ("task", "get"), cli_show_effective_context=True),
96
96
  PublicToolSpec(USER_DOMAIN, "task_action_execute", ("task_action_execute",), ("task", "action"), cli_show_effective_context=True, cli_context_write=True),
97
- PublicToolSpec(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,29 +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 = [
189
- self._normalize_task_item(item, task_box=task_box, flow_status=flow_status)
190
- for item in _task_page_items(task_page)
191
- if isinstance(item, dict)
192
- ]
193
- returned_items = len(items)
194
- page_amount = _task_page_amount(task_page)
195
- reported_total = _task_page_total(task_page)
192
+ items = response["items"] if isinstance(response.get("items"), list) else []
193
+ page_amount = response.get("page_amount")
194
+ reported_total = response.get("reported_total")
196
195
  if query and not items:
197
196
  fallback = self._task_list_local_query_fallback(
198
197
  profile=profile,
@@ -218,25 +217,24 @@ class TaskContextTools(ToolBase):
218
217
  ),
219
218
  }
220
219
  )
220
+ public_items = [self._public_task_item(item) for item in items]
221
221
  return {
222
222
  "profile": profile,
223
- "ws_id": raw.get("ws_id"),
223
+ "ws_id": response.get("raw", {}).get("ws_id") if isinstance(response.get("raw"), dict) else None,
224
224
  "ok": True,
225
- "request_route": raw.get("request_route"),
225
+ "request_route": response.get("raw", {}).get("request_route") if isinstance(response.get("raw"), dict) else None,
226
226
  "warnings": warnings,
227
227
  "output_profile": "normal",
228
228
  "data": {
229
- "items": items,
229
+ "items": public_items,
230
230
  "pagination": {
231
231
  "page": page,
232
232
  "page_size": page_size,
233
- "returned_items": returned_items,
233
+ "returned_items": len(public_items),
234
234
  "page_amount": page_amount,
235
235
  "reported_total": reported_total,
236
236
  },
237
237
  "selection": {
238
- "task_box": task_box,
239
- "flow_status": flow_status,
240
238
  "app_key": app_key,
241
239
  "workflow_node_id": workflow_node_id,
242
240
  "query": query,
@@ -249,28 +247,44 @@ class TaskContextTools(ToolBase):
249
247
  self,
250
248
  *,
251
249
  profile: str,
252
- app_key: str,
253
- record_id: Any,
254
- workflow_node_id: int,
255
- include_candidates: bool,
256
- 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,
257
256
  ) -> dict[str, Any]:
258
257
  """执行任务相关逻辑。"""
259
- record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
260
- 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")
261
260
 
262
261
  def runner(session_profile, context):
263
- data = self._build_task_context(
262
+ locator = self._resolve_task_locator_input(
264
263
  profile=profile,
265
- context=context,
264
+ task_id=task_id,
266
265
  app_key=app_key,
267
- record_id=record_id_int,
266
+ record_id=record_id,
268
267
  workflow_node_id=workflow_node_id,
268
+ )
269
+ task_id_text = locator["task_id"]
270
+ resolved_app_key = str(locator["app_key"])
271
+ resolved_record_id = int(locator["record_id"])
272
+ resolved_workflow_node_id = int(locator["workflow_node_id"])
273
+ self._require_app_record_and_node(resolved_app_key, resolved_record_id, resolved_workflow_node_id)
274
+ data = self._build_task_context(
275
+ profile=profile,
276
+ context=context,
277
+ app_key=resolved_app_key,
278
+ record_id=resolved_record_id,
279
+ workflow_node_id=resolved_workflow_node_id,
269
280
  include_candidates=include_candidates,
270
281
  include_associated_reports=include_associated_reports,
271
282
  current_uid=session_profile.uid,
272
283
  )
273
284
  data = self._compact_task_get_context(data)
285
+ task_payload = data.get("task")
286
+ if isinstance(task_payload, dict) and task_id_text is not None:
287
+ task_payload["task_id"] = task_id_text
274
288
  return {
275
289
  "profile": profile,
276
290
  "ws_id": session_profile.selected_ws_id,
@@ -312,17 +326,18 @@ class TaskContextTools(ToolBase):
312
326
  self,
313
327
  *,
314
328
  profile: str,
315
- app_key: str,
316
- record_id: Any,
317
- workflow_node_id: int,
329
+ task_id: Any = None,
330
+ app_key: str = "",
331
+ record_id: Any = "",
332
+ workflow_node_id: int = 0,
318
333
  action: str,
319
334
  payload: dict[str, Any],
320
335
  fields: dict[str, Any] | None = None,
321
336
  ) -> dict[str, Any]:
322
337
  """执行任务相关逻辑。"""
323
- record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
324
- record_id_text = stringify_backend_id(record_id_int)
325
- 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
+
326
341
  normalized_action = (action or "").strip().lower()
327
342
  if normalized_action not in {"approve", "reject", "rollback", "transfer", "urge", "save_only"}:
328
343
  raise_tool_error(
@@ -340,13 +355,27 @@ class TaskContextTools(ToolBase):
340
355
  )
341
356
 
342
357
  def runner(session_profile, context):
358
+ locator = self._resolve_task_locator_input(
359
+ profile=profile,
360
+ task_id=task_id,
361
+ app_key=app_key,
362
+ record_id=record_id,
363
+ workflow_node_id=workflow_node_id,
364
+ )
365
+ task_id_text = locator["task_id"]
366
+ resolved_app_key = str(locator["app_key"])
367
+ resolved_record_id = int(locator["record_id"])
368
+ resolved_record_id_text = str(locator["record_id_text"] or "")
369
+ resolved_workflow_node_id = int(locator["workflow_node_id"])
370
+ record_id_text = resolved_record_id_text
371
+ self._require_app_record_and_node(resolved_app_key, resolved_record_id, resolved_workflow_node_id)
343
372
  try:
344
373
  task_context = self._build_task_context(
345
374
  profile=profile,
346
375
  context=context,
347
- app_key=app_key,
348
- record_id=record_id_int,
349
- 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,
350
379
  include_candidates=False,
351
380
  include_associated_reports=False,
352
381
  current_uid=session_profile.uid,
@@ -357,12 +386,13 @@ class TaskContextTools(ToolBase):
357
386
  profile=profile,
358
387
  session_profile=session_profile,
359
388
  context=context,
360
- app_key=app_key,
361
- record_id=record_id_int,
362
- 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,
363
392
  action=normalized_action,
364
393
  source_error=error,
365
394
  before_apply_status=None,
395
+ task_id=task_id_text,
366
396
  )
367
397
  raise
368
398
  if normalized_action == "save_only" and not field_updates:
@@ -391,7 +421,7 @@ class TaskContextTools(ToolBase):
391
421
  raise_tool_error(QingflowApiError.config_error(message))
392
422
  raise_tool_error(
393
423
  QingflowApiError.config_error(
394
- f"task action '{normalized_action}' is not currently available for app_key='{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}"
395
425
  )
396
426
  )
397
427
  feedback_required_for = capabilities.get("action_constraints", {}).get("feedback_required_for") or []
@@ -412,9 +442,9 @@ class TaskContextTools(ToolBase):
412
442
  prepared_fields = self._prepare_task_field_update(
413
443
  profile=profile,
414
444
  context=context,
415
- app_key=app_key,
416
- record_id=record_id_int,
417
- 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,
418
448
  task_context=task_context,
419
449
  fields=field_updates,
420
450
  )
@@ -424,16 +454,16 @@ class TaskContextTools(ToolBase):
424
454
  runtime_baseline = self._capture_task_runtime_baseline(
425
455
  profile=profile,
426
456
  context=context,
427
- app_key=app_key,
428
- record_id=record_id_int,
429
- 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,
430
460
  )
431
461
  try:
432
462
  raw = self._execute_task_action(
433
463
  profile=profile,
434
- app_key=app_key,
435
- record_id=record_id_int,
436
- 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,
437
467
  normalized_action=normalized_action,
438
468
  payload=body,
439
469
  prepared_fields=prepared_fields,
@@ -444,21 +474,22 @@ class TaskContextTools(ToolBase):
444
474
  profile=profile,
445
475
  session_profile=session_profile,
446
476
  context=context,
447
- app_key=app_key,
448
- record_id=record_id_int,
449
- 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,
450
480
  action=normalized_action,
451
481
  source_error=error,
452
482
  before_apply_status=before_apply_status,
483
+ task_id=task_id_text,
453
484
  )
454
485
  raise
455
486
 
456
487
  if normalized_action == "save_only":
457
488
  verification, warnings = self._verify_task_save_only(
458
489
  context=context,
459
- app_key=app_key,
460
- record_id=record_id_int,
461
- 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,
462
493
  before_apply_status=before_apply_status,
463
494
  expected_answers=((prepared_fields or {}).get("normalized_answers") or []),
464
495
  task_context=task_context,
@@ -470,9 +501,9 @@ class TaskContextTools(ToolBase):
470
501
  verification, warnings = self._verify_task_action_runtime(
471
502
  profile=profile,
472
503
  context=context,
473
- app_key=app_key,
474
- record_id=record_id_int,
475
- 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,
476
507
  action=normalized_action,
477
508
  before_apply_status=before_apply_status,
478
509
  runtime_baseline=runtime_baseline,
@@ -480,7 +511,7 @@ class TaskContextTools(ToolBase):
480
511
  runtime_verified = bool(verification.get("runtime_continuation_verified"))
481
512
  status = "success" if runtime_verified else "partial_success"
482
513
  error_code = None if runtime_verified else "WORKFLOW_CONTINUATION_UNVERIFIED"
483
- return {
514
+ result = {
484
515
  "profile": raw.get("profile", profile),
485
516
  "ws_id": raw.get("ws_id", session_profile.selected_ws_id),
486
517
  "ok": bool(raw.get("ok", True)) and status != "failed",
@@ -493,9 +524,9 @@ class TaskContextTools(ToolBase):
493
524
  "data": {
494
525
  "action": normalized_action,
495
526
  "resource": {
496
- "app_key": app_key,
527
+ "app_key": resolved_app_key,
497
528
  "record_id": record_id_text,
498
- "workflow_node_id": workflow_node_id,
529
+ "workflow_node_id": resolved_workflow_node_id,
499
530
  },
500
531
  "selection": {"action": normalized_action},
501
532
  "result": raw.get("result"),
@@ -503,6 +534,11 @@ class TaskContextTools(ToolBase):
503
534
  "field_update_applied": bool(field_updates),
504
535
  },
505
536
  }
537
+ if task_id_text is not None:
538
+ resource = result["data"].get("resource")
539
+ if isinstance(resource, dict):
540
+ resource["task_id"] = task_id_text
541
+ return result
506
542
 
507
543
  return self._run(profile, runner)
508
544
 
@@ -920,6 +956,7 @@ class TaskContextTools(ToolBase):
920
956
  action: str,
921
957
  source_error: QingflowApiError,
922
958
  before_apply_status: Any,
959
+ task_id: str | None = None,
923
960
  ) -> dict[str, Any]:
924
961
  """执行内部辅助逻辑。"""
925
962
  record_id_text = stringify_backend_id(record_id)
@@ -941,7 +978,7 @@ class TaskContextTools(ToolBase):
941
978
  "message": "the task is no longer actionable in the current context; MCP found downstream workflow evidence and treats it as already processed by another actor.",
942
979
  }
943
980
  )
944
- return {
981
+ result = {
945
982
  "profile": profile,
946
983
  "ws_id": session_profile.selected_ws_id,
947
984
  "ok": True,
@@ -963,13 +1000,18 @@ class TaskContextTools(ToolBase):
963
1000
  "human_review": True,
964
1001
  },
965
1002
  }
1003
+ if task_id is not None:
1004
+ resource = result["data"].get("resource")
1005
+ if isinstance(resource, dict):
1006
+ resource["task_id"] = task_id
1007
+ return result
966
1008
  warnings.append(
967
1009
  {
968
1010
  "code": "TASK_CONTEXT_VISIBILITY_UNVERIFIED",
969
1011
  "message": "the task is no longer actionable, and MCP could not verify from state or workflow logs whether it was already processed.",
970
1012
  }
971
1013
  )
972
- return {
1014
+ result = {
973
1015
  "profile": profile,
974
1016
  "ws_id": session_profile.selected_ws_id,
975
1017
  "ok": False,
@@ -996,11 +1038,16 @@ class TaskContextTools(ToolBase):
996
1038
  },
997
1039
  },
998
1040
  }
1041
+ if task_id is not None:
1042
+ resource = result["data"].get("resource")
1043
+ if isinstance(resource, dict):
1044
+ resource["task_id"] = task_id
1045
+ return result
999
1046
 
1000
1047
  def _safe_task_list_items(self, *, profile: str, task_box: str, app_key: str) -> list[dict[str, Any]]:
1001
1048
  """执行内部辅助逻辑。"""
1002
1049
  try:
1003
- response = self.task_list(
1050
+ response = self._list_normalized_task_items(
1004
1051
  profile=profile,
1005
1052
  task_box=task_box,
1006
1053
  flow_status="all",
@@ -1012,12 +1059,44 @@ class TaskContextTools(ToolBase):
1012
1059
  )
1013
1060
  except QingflowApiError:
1014
1061
  return []
1015
- data = response.get("data") if isinstance(response, dict) else None
1016
- items = data.get("items") if isinstance(data, dict) else None
1062
+ items = response.get("items") if isinstance(response, dict) else None
1017
1063
  if not isinstance(items, list):
1018
1064
  return []
1019
1065
  return [item for item in items if isinstance(item, dict)]
1020
1066
 
1067
+ def _list_normalized_task_items(
1068
+ self,
1069
+ *,
1070
+ profile: str,
1071
+ task_box: str,
1072
+ flow_status: str,
1073
+ app_key: str | None,
1074
+ workflow_node_id: int | None,
1075
+ query: str | None,
1076
+ page: int,
1077
+ page_size: int,
1078
+ ) -> dict[str, Any]:
1079
+ normalized_type = self._task_tools._task_box_to_type(task_box)
1080
+ normalized_status = self._task_tools._flow_status_to_process_status(flow_status)
1081
+ raw = self._task_tools.task_list(
1082
+ profile=profile,
1083
+ type=normalized_type,
1084
+ process_status=normalized_status,
1085
+ app_key=app_key,
1086
+ node_id=workflow_node_id,
1087
+ search_key=query,
1088
+ page_num=page,
1089
+ page_size=page_size,
1090
+ create_time_asc=None,
1091
+ )
1092
+ task_page = raw.get("page", {})
1093
+ return {
1094
+ "raw": raw,
1095
+ "items": [self._normalize_task_item(item) for item in _task_page_items(task_page) if isinstance(item, dict)],
1096
+ "page_amount": _task_page_amount(task_page),
1097
+ "reported_total": _task_page_total(task_page),
1098
+ }
1099
+
1021
1100
  def _task_list_local_query_fallback(
1022
1101
  self,
1023
1102
  *,
@@ -1030,39 +1109,30 @@ class TaskContextTools(ToolBase):
1030
1109
  page: int,
1031
1110
  page_size: int,
1032
1111
  ) -> dict[str, Any] | None:
1033
- normalized_type = self._task_tools._task_box_to_type(task_box)
1034
- normalized_status = self._task_tools._flow_status_to_process_status(flow_status)
1035
1112
  scan_page_size = max(page_size, 100)
1036
1113
  scan_page = 1
1037
1114
  page_amount: int | None = None
1038
1115
  matched_items: list[dict[str, Any]] = []
1039
1116
  while True:
1040
- raw = self._task_tools.task_list(
1117
+ response = self._list_normalized_task_items(
1041
1118
  profile=profile,
1042
- type=normalized_type,
1043
- process_status=normalized_status,
1119
+ task_box=task_box,
1120
+ flow_status=flow_status,
1044
1121
  app_key=app_key,
1045
- node_id=workflow_node_id,
1046
- search_key=None,
1047
- page_num=scan_page,
1122
+ workflow_node_id=workflow_node_id,
1123
+ query=None,
1124
+ page=scan_page,
1048
1125
  page_size=scan_page_size,
1049
- create_time_asc=None,
1050
1126
  )
1051
- task_page = raw.get("page", {})
1052
- raw_items = _task_page_items(task_page)
1053
- normalized_items = [
1054
- self._normalize_task_item(item, task_box=task_box, flow_status=flow_status)
1055
- for item in raw_items
1056
- if isinstance(item, dict)
1057
- ]
1127
+ normalized_items = response.get("items") if isinstance(response.get("items"), list) else []
1058
1128
  matched_items.extend(item for item in normalized_items if self._task_item_matches_query(item, query))
1059
1129
  if page_amount is None:
1060
- coerced_page_amount = _coerce_count(_task_page_amount(task_page))
1130
+ coerced_page_amount = _coerce_count(response.get("page_amount"))
1061
1131
  if coerced_page_amount is not None and coerced_page_amount > 0:
1062
1132
  page_amount = coerced_page_amount
1063
1133
  if page_amount is not None and scan_page >= page_amount:
1064
1134
  break
1065
- if not raw_items or len(raw_items) < scan_page_size:
1135
+ if not normalized_items or len(normalized_items) < scan_page_size:
1066
1136
  break
1067
1137
  scan_page += 1
1068
1138
  if not matched_items:
@@ -1077,6 +1147,106 @@ class TaskContextTools(ToolBase):
1077
1147
  "reported_total": matched_total,
1078
1148
  }
1079
1149
 
1150
+ def _resolve_todo_task_locator(self, *, profile: str, task_id: Any) -> dict[str, Any]:
1151
+ task_id_text = normalize_positive_id_text(task_id, field_name="task_id")
1152
+ page = 1
1153
+ page_size = 100
1154
+ page_amount: int | None = None
1155
+ while True:
1156
+ response = self._list_normalized_task_items(
1157
+ profile=profile,
1158
+ task_box="todo",
1159
+ flow_status="all",
1160
+ app_key=None,
1161
+ workflow_node_id=None,
1162
+ query=None,
1163
+ page=page,
1164
+ page_size=page_size,
1165
+ )
1166
+ items = response.get("items") if isinstance(response.get("items"), list) else []
1167
+ for item in items:
1168
+ if not isinstance(item, dict) or not ids_equal(item.get("task_id"), task_id_text):
1169
+ continue
1170
+ app_key = str(item.get("app_key") or "").strip()
1171
+ record_id = stringify_backend_id(item.get("record_id"))
1172
+ workflow_node_id = int(item.get("workflow_node_id") or 0)
1173
+ if not app_key or record_id is None or workflow_node_id <= 0:
1174
+ raise_tool_error(
1175
+ QingflowApiError.config_error(
1176
+ f"task_id={task_id_text} resolved to an incomplete task locator; please refresh the todo list and retry"
1177
+ )
1178
+ )
1179
+ return {
1180
+ "task_id": task_id_text,
1181
+ "app_key": app_key,
1182
+ "record_id": record_id,
1183
+ "workflow_node_id": workflow_node_id,
1184
+ }
1185
+ if page_amount is None:
1186
+ coerced_page_amount = _coerce_count(response.get("page_amount"))
1187
+ if coerced_page_amount is not None and coerced_page_amount > 0:
1188
+ page_amount = coerced_page_amount
1189
+ if page_amount is not None and page >= page_amount:
1190
+ break
1191
+ if not items or len(items) < page_size:
1192
+ break
1193
+ page += 1
1194
+ raise_tool_error(
1195
+ QingflowApiError.config_error(
1196
+ f"task_id={task_id_text} was not found in the current todo list; task context tools currently resolve actionable todo tasks by task_id"
1197
+ )
1198
+ )
1199
+
1200
+ def _resolve_task_locator_input(
1201
+ self,
1202
+ *,
1203
+ profile: str,
1204
+ task_id: Any = None,
1205
+ app_key: str = "",
1206
+ record_id: Any = "",
1207
+ workflow_node_id: int = 0,
1208
+ ) -> dict[str, Any]:
1209
+ task_id_text = normalize_positive_id_text(task_id, field_name="task_id") if task_id not in (None, "") else None
1210
+ resolved_app_key = (app_key or "").strip()
1211
+ resolved_record_id: int
1212
+ resolved_workflow_node_id: int
1213
+ if task_id_text is not None:
1214
+ locator = self._resolve_todo_task_locator(profile=profile, task_id=task_id_text)
1215
+ resolved_app_key = str(locator["app_key"])
1216
+ resolved_record_id = normalize_positive_id_int(locator["record_id"], field_name="record_id")
1217
+ resolved_workflow_node_id = int(locator["workflow_node_id"])
1218
+ explicit_app_key = (app_key or "").strip()
1219
+ if explicit_app_key and explicit_app_key != resolved_app_key:
1220
+ raise_tool_error(
1221
+ QingflowApiError.config_error(
1222
+ f"task_id={task_id_text} resolved to app_key='{resolved_app_key}', which does not match app_key='{explicit_app_key}'"
1223
+ )
1224
+ )
1225
+ if record_id not in (None, ""):
1226
+ explicit_record_id = normalize_positive_id_text(record_id, field_name="record_id")
1227
+ if explicit_record_id != stringify_backend_id(resolved_record_id):
1228
+ raise_tool_error(
1229
+ QingflowApiError.config_error(
1230
+ f"task_id={task_id_text} resolved to record_id={resolved_record_id}, which does not match record_id={explicit_record_id}"
1231
+ )
1232
+ )
1233
+ if workflow_node_id not in (None, 0) and int(workflow_node_id) != resolved_workflow_node_id:
1234
+ raise_tool_error(
1235
+ QingflowApiError.config_error(
1236
+ f"task_id={task_id_text} resolved to workflow_node_id={resolved_workflow_node_id}, which does not match workflow_node_id={workflow_node_id}"
1237
+ )
1238
+ )
1239
+ else:
1240
+ resolved_record_id = normalize_positive_id_int(record_id, field_name="record_id")
1241
+ resolved_workflow_node_id = int(workflow_node_id)
1242
+ return {
1243
+ "task_id": task_id_text,
1244
+ "app_key": resolved_app_key,
1245
+ "record_id": resolved_record_id,
1246
+ "record_id_text": stringify_backend_id(resolved_record_id),
1247
+ "workflow_node_id": resolved_workflow_node_id,
1248
+ }
1249
+
1080
1250
  def _task_item_matches_query(self, item: dict[str, Any], query: str) -> bool:
1081
1251
  needle = str(query or "").strip().casefold()
1082
1252
  if not needle:
@@ -1098,29 +1268,43 @@ class TaskContextTools(ToolBase):
1098
1268
  self,
1099
1269
  *,
1100
1270
  profile: str,
1101
- app_key: str,
1102
- record_id: int,
1103
- workflow_node_id: int,
1271
+ task_id: Any = None,
1272
+ app_key: str = "",
1273
+ record_id: Any = "",
1274
+ workflow_node_id: int = 0,
1104
1275
  report_id: int,
1105
1276
  page: int,
1106
1277
  page_size: int,
1107
1278
  ) -> dict[str, Any]:
1108
1279
  """执行任务相关逻辑。"""
1109
- record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
1110
- record_id_text = stringify_backend_id(record_id_int)
1111
- 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
+
1112
1283
  if report_id <= 0:
1113
1284
  raise_tool_error(QingflowApiError.config_error("report_id must be positive"))
1114
1285
  if page <= 0 or page_size <= 0:
1115
1286
  raise_tool_error(QingflowApiError.config_error("page and page_size must be positive"))
1116
1287
 
1117
1288
  def runner(session_profile, context):
1118
- task_context = self._build_task_context(
1289
+ locator = self._resolve_task_locator_input(
1119
1290
  profile=profile,
1120
- context=context,
1291
+ task_id=task_id,
1121
1292
  app_key=app_key,
1122
- record_id=record_id_int,
1293
+ record_id=record_id,
1123
1294
  workflow_node_id=workflow_node_id,
1295
+ )
1296
+ task_id_text = locator["task_id"]
1297
+ resolved_app_key = str(locator["app_key"])
1298
+ resolved_record_id = int(locator["record_id"])
1299
+ record_id_text = str(locator["record_id_text"] or "")
1300
+ resolved_workflow_node_id = int(locator["workflow_node_id"])
1301
+ self._require_app_record_and_node(resolved_app_key, resolved_record_id, resolved_workflow_node_id)
1302
+ task_context = self._build_task_context(
1303
+ profile=profile,
1304
+ context=context,
1305
+ app_key=resolved_app_key,
1306
+ record_id=resolved_record_id,
1307
+ workflow_node_id=resolved_workflow_node_id,
1124
1308
  include_candidates=False,
1125
1309
  include_associated_reports=True,
1126
1310
  current_uid=session_profile.uid,
@@ -1129,7 +1313,7 @@ class TaskContextTools(ToolBase):
1129
1313
  if report_item is None:
1130
1314
  raise_tool_error(
1131
1315
  QingflowApiError.config_error(
1132
- f"report_id={report_id} is not visible for app_key='{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}"
1133
1317
  )
1134
1318
  )
1135
1319
  association_query = self._build_association_query(
@@ -1137,15 +1321,17 @@ class TaskContextTools(ToolBase):
1137
1321
  task_context.get("record", {}).get("answers") or [],
1138
1322
  )
1139
1323
  selection = {
1140
- "app_key": app_key,
1324
+ "app_key": resolved_app_key,
1141
1325
  "record_id": record_id_text,
1142
- "workflow_node_id": workflow_node_id,
1326
+ "workflow_node_id": resolved_workflow_node_id,
1143
1327
  "report_id": report_id,
1144
1328
  "target_app_key": report_item.get("target_app_key"),
1145
1329
  "target_app_name": report_item.get("target_app_name"),
1146
1330
  "chart_key": report_item.get("chart_key"),
1147
1331
  "chart_name": report_item.get("chart_name"),
1148
1332
  }
1333
+ if task_id_text is not None:
1334
+ selection["task_id"] = task_id_text
1149
1335
  context_payload = {
1150
1336
  "match_rules": report_item.get("match_rules") or [],
1151
1337
  "resolved_filters": association_query.get("keyQueValues") or [],
@@ -1292,22 +1478,35 @@ class TaskContextTools(ToolBase):
1292
1478
  self,
1293
1479
  *,
1294
1480
  profile: str,
1295
- app_key: str,
1296
- record_id: Any,
1297
- workflow_node_id: int,
1481
+ task_id: Any = None,
1482
+ app_key: str = "",
1483
+ record_id: Any = "",
1484
+ workflow_node_id: int = 0,
1298
1485
  ) -> dict[str, Any]:
1299
1486
  """执行任务相关逻辑。"""
1300
- record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
1301
- record_id_text = stringify_backend_id(record_id_int)
1302
- self._require_app_record_and_node(app_key, record_id_int, workflow_node_id)
1487
+ if task_id in (None, ""):
1488
+ normalize_positive_id_int(record_id, field_name="record_id")
1303
1489
 
1304
1490
  def runner(session_profile, context):
1305
- task_context = self._build_task_context(
1491
+ locator = self._resolve_task_locator_input(
1306
1492
  profile=profile,
1307
- context=context,
1493
+ task_id=task_id,
1308
1494
  app_key=app_key,
1309
- record_id=record_id_int,
1495
+ record_id=record_id,
1310
1496
  workflow_node_id=workflow_node_id,
1497
+ )
1498
+ task_id_text = locator["task_id"]
1499
+ resolved_app_key = str(locator["app_key"])
1500
+ resolved_record_id = int(locator["record_id"])
1501
+ record_id_text = str(locator["record_id_text"] or "")
1502
+ resolved_workflow_node_id = int(locator["workflow_node_id"])
1503
+ self._require_app_record_and_node(resolved_app_key, resolved_record_id, resolved_workflow_node_id)
1504
+ task_context = self._build_task_context(
1505
+ profile=profile,
1506
+ context=context,
1507
+ app_key=resolved_app_key,
1508
+ record_id=resolved_record_id,
1509
+ workflow_node_id=resolved_workflow_node_id,
1311
1510
  include_candidates=False,
1312
1511
  include_associated_reports=False,
1313
1512
  current_uid=session_profile.uid,
@@ -1316,7 +1515,7 @@ class TaskContextTools(ToolBase):
1316
1515
  if not visibility.get("audit_record_visible"):
1317
1516
  raise_tool_error(
1318
1517
  QingflowApiError.config_error(
1319
- f"workflow logs are not visible for app_key='{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}"
1320
1519
  )
1321
1520
  )
1322
1521
  page = self.backend.request(
@@ -1324,16 +1523,16 @@ class TaskContextTools(ToolBase):
1324
1523
  context,
1325
1524
  "/application/workflow/node/record",
1326
1525
  json_body={
1327
- "key": app_key,
1328
- "rowRecordId": record_id_int,
1329
- "nodeId": workflow_node_id,
1526
+ "key": resolved_app_key,
1527
+ "rowRecordId": resolved_record_id,
1528
+ "nodeId": resolved_workflow_node_id,
1330
1529
  "role": 3,
1331
1530
  "pageNum": 1,
1332
1531
  "pageSize": 200,
1333
1532
  },
1334
1533
  )
1335
1534
  items = self._normalize_workflow_logs(page)
1336
- return {
1535
+ result = {
1337
1536
  "profile": profile,
1338
1537
  "ws_id": session_profile.selected_ws_id,
1339
1538
  "ok": True,
@@ -1342,9 +1541,9 @@ class TaskContextTools(ToolBase):
1342
1541
  "output_profile": "normal",
1343
1542
  "data": {
1344
1543
  "selection": {
1345
- "app_key": app_key,
1544
+ "app_key": resolved_app_key,
1346
1545
  "record_id": record_id_text,
1347
- "workflow_node_id": workflow_node_id,
1546
+ "workflow_node_id": resolved_workflow_node_id,
1348
1547
  },
1349
1548
  "visibility": {
1350
1549
  "audit_record_visible": visibility.get("audit_record_visible"),
@@ -1353,6 +1552,11 @@ class TaskContextTools(ToolBase):
1353
1552
  "items": items,
1354
1553
  },
1355
1554
  }
1555
+ if task_id_text is not None:
1556
+ selection = result["data"].get("selection")
1557
+ if isinstance(selection, dict):
1558
+ selection["task_id"] = task_id_text
1559
+ return result
1356
1560
 
1357
1561
  return self._run(profile, runner)
1358
1562
 
@@ -1726,7 +1930,7 @@ class TaskContextTools(ToolBase):
1726
1930
  if value not in (None, "", [])
1727
1931
  }
1728
1932
 
1729
- def _normalize_task_item(self, raw: dict[str, Any], *, task_box: str, flow_status: str) -> dict[str, Any]:
1933
+ def _normalize_task_item(self, raw: dict[str, Any]) -> dict[str, Any]:
1730
1934
  """执行内部辅助逻辑。"""
1731
1935
  app_key = raw.get("appKey") or raw.get("app_key")
1732
1936
  record_id = raw.get("rowRecordId") or raw.get("recordId") or raw.get("applyId")
@@ -1739,11 +1943,39 @@ class TaskContextTools(ToolBase):
1739
1943
  "workflow_node_id": workflow_node_id,
1740
1944
  "workflow_node_name": raw.get("nodeName") or raw.get("auditNodeName"),
1741
1945
  "apply_time": raw.get("applyTime") or raw.get("receiveTime"),
1742
- "task_box": task_box,
1743
- "flow_status": flow_status,
1744
- "actionable": task_box == "todo" and bool(record_id) and bool(workflow_node_id),
1946
+ "summary_fields": self._normalize_task_summary_fields(raw.get("dataSnapshot")),
1745
1947
  }
1746
1948
 
1949
+ def _public_task_item(self, item: dict[str, Any]) -> dict[str, Any]:
1950
+ return {
1951
+ "task_id": item.get("task_id"),
1952
+ "app_name": item.get("app_name"),
1953
+ "workflow_node_name": item.get("workflow_node_name"),
1954
+ "apply_time": item.get("apply_time"),
1955
+ "summary_fields": item.get("summary_fields") if isinstance(item.get("summary_fields"), list) else [],
1956
+ }
1957
+
1958
+ def _normalize_task_summary_fields(self, raw: Any) -> list[dict[str, Any]]:
1959
+ """执行内部辅助逻辑。"""
1960
+ if not isinstance(raw, list):
1961
+ return []
1962
+ summary_fields: list[dict[str, Any]] = []
1963
+ for item in raw:
1964
+ if not isinstance(item, dict):
1965
+ continue
1966
+ summary_field: dict[str, Any] = {
1967
+ "field_id": item.get("fieldId"),
1968
+ "title": item.get("fieldTitle"),
1969
+ "type": item.get("fieldType"),
1970
+ "answer": item.get("fieldAnswer"),
1971
+ "desensitized": self._coerce_bool(item.get("beingDesensitized")),
1972
+ }
1973
+ associated_field_type = item.get("associatedQueType")
1974
+ if associated_field_type is not None:
1975
+ summary_field["associated_field_type"] = associated_field_type
1976
+ summary_fields.append(summary_field)
1977
+ return summary_fields
1978
+
1747
1979
  def _select_task_node(self, infos: Any, workflow_node_id: int, *, app_key: str, record_id: int) -> dict[str, Any]:
1748
1980
  """执行内部辅助逻辑。"""
1749
1981
  if not isinstance(infos, list) or not infos: