@josephyan/qingflow-app-user-mcp 0.2.0-beta.2 → 0.2.0-beta.21

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.
Files changed (32) hide show
  1. package/README.md +12 -2
  2. package/npm/lib/runtime.mjs +37 -0
  3. package/npm/scripts/postinstall.mjs +5 -1
  4. package/package.json +3 -2
  5. package/pyproject.toml +1 -1
  6. package/skills/qingflow-app-user/SKILL.md +230 -0
  7. package/skills/qingflow-app-user/agents/openai.yaml +4 -0
  8. package/skills/qingflow-app-user/references/data-gotchas.md +49 -0
  9. package/skills/qingflow-app-user/references/environments.md +63 -0
  10. package/skills/qingflow-app-user/references/record-patterns.md +110 -0
  11. package/skills/qingflow-app-user/references/workflow-usage.md +26 -0
  12. package/skills/qingflow-record-analysis/SKILL.md +253 -0
  13. package/skills/qingflow-record-analysis/agents/openai.yaml +4 -0
  14. package/skills/qingflow-record-analysis/references/analysis-gotchas.md +141 -0
  15. package/skills/qingflow-record-analysis/references/analysis-patterns.md +113 -0
  16. package/skills/qingflow-record-analysis/references/confidence-reporting.md +92 -0
  17. package/src/qingflow_mcp/__init__.py +1 -1
  18. package/src/qingflow_mcp/builder_facade/models.py +294 -1
  19. package/src/qingflow_mcp/builder_facade/service.py +2727 -235
  20. package/src/qingflow_mcp/server.py +7 -5
  21. package/src/qingflow_mcp/server_app_builder.py +80 -4
  22. package/src/qingflow_mcp/server_app_user.py +8 -182
  23. package/src/qingflow_mcp/solution/compiler/form_compiler.py +1 -1
  24. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +21 -2
  25. package/src/qingflow_mcp/solution/executor.py +34 -7
  26. package/src/qingflow_mcp/tools/ai_builder_tools.py +1038 -30
  27. package/src/qingflow_mcp/tools/app_tools.py +1 -2
  28. package/src/qingflow_mcp/tools/approval_tools.py +357 -75
  29. package/src/qingflow_mcp/tools/directory_tools.py +158 -28
  30. package/src/qingflow_mcp/tools/record_tools.py +1954 -973
  31. package/src/qingflow_mcp/tools/task_tools.py +376 -225
  32. package/src/qingflow_mcp/tools/workflow_tools.py +78 -4
@@ -9,6 +9,26 @@ from ..errors import QingflowApiError, raise_tool_error
9
9
  from ..list_type_labels import get_record_list_type_label, get_task_type_label
10
10
  from .base import ToolBase
11
11
 
12
+ TASK_BOX_TO_TYPE = {
13
+ "todo": 1,
14
+ "initiated": 2,
15
+ "cc": 3,
16
+ "done": 5,
17
+ }
18
+
19
+ FLOW_STATUS_TO_PROCESS_STATUS = {
20
+ "all": 1,
21
+ "in_progress": 2,
22
+ "approved": 3,
23
+ "rejected": 4,
24
+ "pending_fix": 5,
25
+ "urged": 6,
26
+ "overdue": 7,
27
+ "due_soon": 8,
28
+ "unread": 9,
29
+ "ended": 10,
30
+ }
31
+
12
32
 
13
33
  class TaskTools(ToolBase):
14
34
  """任务中心(待办/已办)相关工具
@@ -40,276 +60,278 @@ class TaskTools(ToolBase):
40
60
 
41
61
  def register(self, mcp: FastMCP) -> None:
42
62
  @mcp.tool()
43
- def task_list(
63
+ def task_summary(
44
64
  profile: str = DEFAULT_PROFILE,
45
- type: int = 1,
46
- process_status: int = 1,
47
65
  app_key: str | None = None,
48
- node_id: int | None = None,
49
- search_key: str | None = None,
50
- page_num: int = 1,
51
- page_size: int = 20,
52
- create_time_asc: bool | None = None,
53
66
  ) -> dict[str, Any]:
54
- """查询任务列表(待办/已办/我发起的/抄送)
55
-
56
- Args:
57
- profile: 配置文件名
58
- type: 消息类型 (1=待办, 2=我发起的, 3=抄送, 5=已办)
59
- process_status: 流程状态 (1=全部, 2=流程中, 3=已通过, 4=已拒绝, 5=待完善, 6=催办, 7=超时, 8=即将超时, 9=未读)
60
- app_key: 应用key(可选,用于筛选特定应用)
61
- node_id: 节点ID(可选,用于筛选特定节点)
62
- search_key: 搜索关键词
63
- page_num: 页码,从1开始
64
- page_size: 每页数量
65
- create_time_asc: 是否按创建时间升序(None表示默认排序)
66
- """
67
- return self.task_list(
67
+ return self.task_summary(
68
68
  profile=profile,
69
- type=type,
70
- process_status=process_status,
71
69
  app_key=app_key,
72
- node_id=node_id,
73
- search_key=search_key,
74
- page_num=page_num,
75
- page_size=page_size,
76
- create_time_asc=create_time_asc,
77
70
  )
78
71
 
79
72
  @mcp.tool()
80
- def task_list_grouped(
73
+ def task_list(
81
74
  profile: str = DEFAULT_PROFILE,
82
- type: int = 1,
83
- process_status: int = 1,
75
+ task_box: str = "todo",
76
+ flow_status: str = "all",
84
77
  app_key: str | None = None,
85
- node_id: int | None = None,
86
- search_key: str | None = None,
87
- page_num: int = 1,
78
+ workflow_node_id: int | None = None,
79
+ query: str | None = None,
80
+ page: int = 1,
88
81
  page_size: int = 20,
82
+ sort_by: str | None = None,
83
+ sort_direction: str = "desc",
89
84
  ) -> dict[str, Any]:
90
- """查询任务列表(带分组信息)
91
-
92
- 返回按表单分组的任务列表,适用于需要分组展示的场景。
93
-
94
- Args:
95
- profile: 配置文件名
96
- type: 消息类型 (1=待办, 2=我发起的, 3=抄送, 5=已办)
97
- process_status: 流程状态 (1=全部, 2=流程中, 3=已通过, 4=已拒绝, 5=待完善, 6=催办, 7=超时, 8=即将超时, 9=未读)
98
- app_key: 应用key(可选,用于筛选特定应用)
99
- node_id: 节点ID(可选,用于筛选特定节点)
100
- search_key: 搜索关键词
101
- page_num: 页码,从1开始
102
- page_size: 每页数量
103
- """
104
- return self.task_list_grouped(
85
+ return self.task_list_public(
105
86
  profile=profile,
106
- type=type,
107
- process_status=process_status,
87
+ task_box=task_box,
88
+ flow_status=flow_status,
108
89
  app_key=app_key,
109
- node_id=node_id,
110
- search_key=search_key,
111
- page_num=page_num,
90
+ workflow_node_id=workflow_node_id,
91
+ query=query,
92
+ page=page,
112
93
  page_size=page_size,
94
+ sort_by=sort_by,
95
+ sort_direction=sort_direction,
113
96
  )
114
97
 
115
98
  @mcp.tool()
116
- def task_statistics(
99
+ def task_facets(
117
100
  profile: str = DEFAULT_PROFILE,
101
+ task_box: str = "todo",
102
+ flow_status: str = "all",
103
+ dimension: str = "worksheet",
118
104
  app_key: str | None = None,
105
+ query: str | None = None,
106
+ limit: int = 50,
119
107
  ) -> dict[str, Any]:
120
- """查询任务中心统计信息
121
-
122
- 获取当前用户的任务统计数量,包括:
123
- - 待办数量
124
- - 超时数量
125
- - 即将超时数量
126
- - 催办数量
127
- - 抄送未读数量
128
- - 我发起的流程中数量
129
-
130
- Args:
131
- profile: 配置文件名
132
- app_key: 应用key(可选,用于统计特定应用)
133
- """
134
- return self.task_statistics(profile=profile, app_key=app_key)
135
-
136
- @mcp.tool()
137
- def task_workflow_nodes(
138
- profile: str = DEFAULT_PROFILE,
139
- type: int = 1,
140
- status: str | None = None,
141
- app_key_list: list[str] | None = None,
142
- search_key: str | None = None,
143
- page_num: int = 1,
144
- page_size: int = 20,
145
- ) -> dict[str, Any]:
146
- """查询流程节点列表
147
-
148
- 获取工作流节点信息,可用于了解当前有哪些流程节点。
149
-
150
- Args:
151
- profile: 配置文件名
152
- type: 消息类型 (1=待办, 2=我发起的, 3=抄送, 5=已办)
153
- status: 流程状态
154
- app_key_list: 应用key列表(用于筛选特定应用)
155
- search_key: 节点名称搜索关键词
156
- page_num: 页码
157
- page_size: 每页数量
158
- """
159
- return self.task_workflow_nodes(
160
- profile=profile,
161
- type=type,
162
- status=status,
163
- app_key_list=app_key_list,
164
- search_key=search_key,
165
- page_num=page_num,
166
- page_size=page_size,
167
- )
168
-
169
- @mcp.tool()
170
- def task_node_statistics(
171
- profile: str = DEFAULT_PROFILE,
172
- app_key: str = "",
173
- type: int = 1,
174
- search_key: str | None = None,
175
- ) -> dict[str, Any]:
176
- """查询表单下节点的分组统计信息
177
-
178
- 获取指定应用下各节点的任务数量统计。
179
-
180
- Args:
181
- profile: 配置文件名
182
- app_key: 应用key
183
- type: 消息类型 (1=待办, 2=我发起的, 3=抄送, 5=已办)
184
- search_key: 节点名称搜索关键词
185
- """
186
- return self.task_node_statistics(
108
+ return self.task_facets(
187
109
  profile=profile,
110
+ task_box=task_box,
111
+ flow_status=flow_status,
112
+ dimension=dimension,
188
113
  app_key=app_key,
189
- type=type,
190
- search_key=search_key,
191
- )
192
-
193
- @mcp.tool()
194
- def task_worksheet_statistics(
195
- profile: str = DEFAULT_PROFILE,
196
- type: int = 1,
197
- worksheet_name: str | None = None,
198
- page_num: int = 1,
199
- page_size: int = 20,
200
- ) -> dict[str, Any]:
201
- """查询表单分组统计信息
202
-
203
- 获取各表单的任务数量统计。
204
-
205
- Args:
206
- profile: 配置文件名
207
- type: 消息类型 (1=待办, 2=我发起的, 3=抄送, 5=已办)
208
- worksheet_name: 表单名称搜索关键词
209
- page_num: 页码
210
- page_size: 每页数量
211
- """
212
- return self.task_worksheet_statistics(
213
- profile=profile,
214
- type=type,
215
- worksheet_name=worksheet_name,
216
- page_num=page_num,
217
- page_size=page_size,
114
+ query=query,
115
+ limit=limit,
218
116
  )
219
117
 
220
118
  @mcp.tool()
221
119
  def task_mark_read(
222
120
  profile: str = DEFAULT_PROFILE,
223
121
  app_key: str = "",
224
- id: int = 0,
225
- type: int = 1,
122
+ task_id: int = 0,
123
+ task_box: str = "todo",
226
124
  ) -> dict[str, Any]:
227
- """标记任务为已读
228
-
229
- Args:
230
- profile: 配置文件名
231
- app_key: 应用key
232
- id: 任务ID
233
- type: 消息类型
234
- """
235
- return self.task_mark_read(profile=profile, app_key=app_key, id=id, type=type)
125
+ return self.task_mark_read_public(profile=profile, app_key=app_key, task_id=task_id, task_box=task_box)
236
126
 
237
127
  @mcp.tool()
238
128
  def task_mark_all_cc_read(
239
129
  profile: str = DEFAULT_PROFILE,
240
- type: int = 3,
241
- process_status: int = 1,
130
+ flow_status: str = "all",
242
131
  ) -> dict[str, Any]:
243
- """标记所有抄送为已读
244
-
245
- Args:
246
- profile: 配置文件名
247
- type: 消息类型(默认为3=抄送)
248
- process_status: 流程状态
249
- """
250
- return self.task_mark_all_cc_read(
251
- profile=profile,
252
- type=type,
253
- process_status=process_status,
254
- )
132
+ return self.task_mark_all_cc_read_public(profile=profile, flow_status=flow_status)
255
133
 
256
134
  @mcp.tool()
257
135
  def task_urge(
258
136
  profile: str = DEFAULT_PROFILE,
259
137
  app_key: str = "",
260
- row_record_id: int = 0,
261
- ) -> dict[str, Any]:
262
- """催办任务
263
-
264
- 对指定记录发起催办,提醒处理人尽快处理。
265
-
266
- Args:
267
- profile: 配置文件名
268
- app_key: 应用key
269
- row_record_id: 记录ID(原applyId)
270
- """
271
- return self.task_urge(profile=profile, app_key=app_key, row_record_id=row_record_id)
272
-
273
- @mcp.tool()
274
- def task_group_detail(
275
- profile: str = DEFAULT_PROFILE,
276
- app_key: str = "",
277
- group_id: int = 0,
278
- ) -> dict[str, Any]:
279
- """查询分组详情
280
-
281
- 获取指定分组的详细信息。
282
-
283
- Args:
284
- profile: 配置文件名
285
- app_key: 应用key
286
- group_id: 分组ID
287
- """
288
- return self.task_group_detail(profile=profile, app_key=app_key, group_id=group_id)
289
-
290
- @mcp.tool()
291
- def task_batch_processing_amount(
292
- profile: str = DEFAULT_PROFILE,
293
- app_key: str = "",
294
- list_type: int = 0,
295
- task_center_filter: dict[str, Any] | None = None,
138
+ record_id: int = 0,
296
139
  ) -> dict[str, Any]:
297
- """查询批量处理数量
140
+ return self.task_urge_public(profile=profile, app_key=app_key, record_id=record_id)
298
141
 
299
- 获取符合筛选条件的任务数量,用于批量操作前的数量确认。
300
-
301
- Args:
302
- profile: 配置文件名
303
- app_key: 应用key
304
- list_type: 操作类型
305
- task_center_filter: 筛选条件
306
- """
307
- return self.task_batch_processing_amount(
142
+ def task_summary(
143
+ self,
144
+ *,
145
+ profile: str,
146
+ app_key: str | None,
147
+ ) -> dict[str, Any]:
148
+ raw = self.task_statistics(profile=profile, app_key=app_key)
149
+ statistics = raw.get("statistics", {})
150
+ summary = self._normalize_task_summary_payload(statistics)
151
+ return {
152
+ "profile": profile,
153
+ "ws_id": raw.get("ws_id"),
154
+ "ok": True,
155
+ "request_route": raw.get("request_route"),
156
+ "warnings": [],
157
+ "output_profile": "normal",
158
+ "data": {
159
+ "summary": summary,
160
+ },
161
+ }
162
+
163
+ def task_list_public(
164
+ self,
165
+ *,
166
+ profile: str,
167
+ task_box: str,
168
+ flow_status: str,
169
+ app_key: str | None,
170
+ workflow_node_id: int | None,
171
+ query: str | None,
172
+ page: int,
173
+ page_size: int,
174
+ sort_by: str | None,
175
+ sort_direction: str,
176
+ ) -> dict[str, Any]:
177
+ normalized_type = self._task_box_to_type(task_box)
178
+ normalized_status = self._flow_status_to_process_status(flow_status)
179
+ create_time_asc = self._task_sort_to_create_time_asc(sort_by, sort_direction)
180
+ raw = self.task_list(
181
+ profile=profile,
182
+ type=normalized_type,
183
+ process_status=normalized_status,
184
+ app_key=app_key,
185
+ node_id=workflow_node_id,
186
+ search_key=query,
187
+ page_num=page,
188
+ page_size=page_size,
189
+ create_time_asc=create_time_asc,
190
+ )
191
+ task_page = raw.get("page", {})
192
+ return {
193
+ "profile": profile,
194
+ "ws_id": raw.get("ws_id"),
195
+ "ok": True,
196
+ "request_route": raw.get("request_route"),
197
+ "warnings": [],
198
+ "output_profile": "normal",
199
+ "data": {
200
+ "items": _task_page_items(task_page),
201
+ "pagination": {
202
+ "page": page,
203
+ "page_size": page_size,
204
+ "returned_items": len(_task_page_items(task_page)),
205
+ "page_amount": _task_page_amount(task_page),
206
+ "reported_total": _task_page_total(task_page),
207
+ },
208
+ "selection": {
209
+ "task_box": task_box,
210
+ "flow_status": flow_status,
211
+ "app_key": app_key,
212
+ "workflow_node_id": workflow_node_id,
213
+ "query": query,
214
+ "applied_sort": self._task_applied_sort(sort_by, sort_direction),
215
+ },
216
+ },
217
+ }
218
+
219
+ def task_facets(
220
+ self,
221
+ *,
222
+ profile: str,
223
+ task_box: str,
224
+ flow_status: str,
225
+ dimension: str,
226
+ app_key: str | None,
227
+ query: str | None,
228
+ limit: int,
229
+ ) -> dict[str, Any]:
230
+ normalized_type = self._task_box_to_type(task_box)
231
+ normalized_status = self._flow_status_to_process_status(flow_status)
232
+ if dimension not in {"worksheet", "workflow_node"}:
233
+ raise_tool_error(QingflowApiError.config_error("dimension must be worksheet or workflow_node"))
234
+ if limit <= 0:
235
+ raise_tool_error(QingflowApiError.config_error("limit must be positive"))
236
+
237
+ if dimension == "worksheet":
238
+ raw = self.task_worksheet_statistics(
239
+ profile=profile,
240
+ type=normalized_type,
241
+ worksheet_name=query,
242
+ page_num=1,
243
+ page_size=max(limit, 20),
244
+ )
245
+ source = raw.get("page", {})
246
+ elif app_key:
247
+ raw = self.task_node_statistics(
308
248
  profile=profile,
309
249
  app_key=app_key,
310
- list_type=list_type,
311
- task_center_filter=task_center_filter,
250
+ type=normalized_type,
251
+ search_key=query,
312
252
  )
253
+ source = raw.get("nodes", {})
254
+ else:
255
+ raw = self.task_workflow_nodes(
256
+ profile=profile,
257
+ type=normalized_type,
258
+ status=str(normalized_status),
259
+ app_key_list=None,
260
+ search_key=query,
261
+ page_num=1,
262
+ page_size=max(limit, 20),
263
+ )
264
+ source = raw.get("page", {})
265
+
266
+ groups = self._normalize_task_facets(source)
267
+ rows_truncated = len(groups) > limit
268
+ returned_groups = groups[:limit]
269
+ return {
270
+ "profile": profile,
271
+ "ws_id": raw.get("ws_id"),
272
+ "ok": True,
273
+ "request_route": raw.get("request_route"),
274
+ "warnings": [],
275
+ "output_profile": "normal",
276
+ "data": {
277
+ "groups": returned_groups,
278
+ "rows_truncated": rows_truncated,
279
+ "statement_scope": "returned_groups_only" if rows_truncated else "full_population",
280
+ "selection": {
281
+ "task_box": task_box,
282
+ "flow_status": flow_status,
283
+ "dimension": dimension,
284
+ "app_key": app_key,
285
+ "query": query,
286
+ "limit": limit,
287
+ },
288
+ },
289
+ }
290
+
291
+ def task_mark_read_public(
292
+ self,
293
+ *,
294
+ profile: str,
295
+ app_key: str,
296
+ task_id: int,
297
+ task_box: str,
298
+ ) -> dict[str, Any]:
299
+ raw = self.task_mark_read(profile=profile, app_key=app_key, id=task_id, type=self._task_box_to_type(task_box))
300
+ return self._public_task_action_response(
301
+ raw,
302
+ action="mark_read",
303
+ resource={"app_key": app_key, "task_id": task_id},
304
+ selection={"task_box": task_box},
305
+ )
306
+
307
+ def task_mark_all_cc_read_public(
308
+ self,
309
+ *,
310
+ profile: str,
311
+ flow_status: str,
312
+ ) -> dict[str, Any]:
313
+ raw = self.task_mark_all_cc_read(profile=profile, type=TASK_BOX_TO_TYPE["cc"], process_status=self._flow_status_to_process_status(flow_status))
314
+ return self._public_task_action_response(
315
+ raw,
316
+ action="mark_all_cc_read",
317
+ resource={},
318
+ selection={"task_box": "cc", "flow_status": flow_status},
319
+ )
320
+
321
+ def task_urge_public(
322
+ self,
323
+ *,
324
+ profile: str,
325
+ app_key: str,
326
+ record_id: int,
327
+ ) -> dict[str, Any]:
328
+ raw = self.task_urge(profile=profile, app_key=app_key, row_record_id=record_id)
329
+ return self._public_task_action_response(
330
+ raw,
331
+ action="urge",
332
+ resource={"app_key": app_key, "record_id": record_id},
333
+ selection={},
334
+ )
313
335
 
314
336
  def task_list(
315
337
  self,
@@ -347,6 +369,7 @@ class TaskTools(ToolBase):
347
369
  return {
348
370
  "profile": profile,
349
371
  "ws_id": session_profile.selected_ws_id,
372
+ "request_route": self._request_route_payload(context),
350
373
  "type": type,
351
374
  "type_label": get_task_type_label(type),
352
375
  "list_type_label": get_task_type_label(type),
@@ -389,6 +412,7 @@ class TaskTools(ToolBase):
389
412
  return {
390
413
  "profile": profile,
391
414
  "ws_id": session_profile.selected_ws_id,
415
+ "request_route": self._request_route_payload(context),
392
416
  "type": type,
393
417
  "type_label": get_task_type_label(type),
394
418
  "list_type_label": get_task_type_label(type),
@@ -413,6 +437,7 @@ class TaskTools(ToolBase):
413
437
  return {
414
438
  "profile": profile,
415
439
  "ws_id": session_profile.selected_ws_id,
440
+ "request_route": self._request_route_payload(context),
416
441
  "statistics": result,
417
442
  }
418
443
 
@@ -448,6 +473,7 @@ class TaskTools(ToolBase):
448
473
  return {
449
474
  "profile": profile,
450
475
  "ws_id": session_profile.selected_ws_id,
476
+ "request_route": self._request_route_payload(context),
451
477
  "type": type,
452
478
  "type_label": get_task_type_label(type),
453
479
  "list_type_label": get_task_type_label(type),
@@ -480,6 +506,7 @@ class TaskTools(ToolBase):
480
506
  return {
481
507
  "profile": profile,
482
508
  "ws_id": session_profile.selected_ws_id,
509
+ "request_route": self._request_route_payload(context),
483
510
  "app_key": app_key,
484
511
  "type": type,
485
512
  "type_label": get_task_type_label(type),
@@ -513,6 +540,7 @@ class TaskTools(ToolBase):
513
540
  return {
514
541
  "profile": profile,
515
542
  "ws_id": session_profile.selected_ws_id,
543
+ "request_route": self._request_route_payload(context),
516
544
  "type": type,
517
545
  "type_label": get_task_type_label(type),
518
546
  "list_type_label": get_task_type_label(type),
@@ -544,6 +572,7 @@ class TaskTools(ToolBase):
544
572
  return {
545
573
  "profile": profile,
546
574
  "ws_id": session_profile.selected_ws_id,
575
+ "request_route": self._request_route_payload(context),
547
576
  "app_key": app_key,
548
577
  "id": id,
549
578
  "type": type,
@@ -573,6 +602,7 @@ class TaskTools(ToolBase):
573
602
  return {
574
603
  "profile": profile,
575
604
  "ws_id": session_profile.selected_ws_id,
605
+ "request_route": self._request_route_payload(context),
576
606
  "type": type,
577
607
  "type_label": get_task_type_label(type),
578
608
  "list_type_label": get_task_type_label(type),
@@ -603,6 +633,7 @@ class TaskTools(ToolBase):
603
633
  return {
604
634
  "profile": profile,
605
635
  "ws_id": session_profile.selected_ws_id,
636
+ "request_route": self._request_route_payload(context),
606
637
  "app_key": app_key,
607
638
  "row_record_id": row_record_id,
608
639
  "result": result,
@@ -631,6 +662,7 @@ class TaskTools(ToolBase):
631
662
  return {
632
663
  "profile": profile,
633
664
  "ws_id": session_profile.selected_ws_id,
665
+ "request_route": self._request_route_payload(context),
634
666
  "app_key": app_key,
635
667
  "group_id": group_id,
636
668
  "detail": result,
@@ -665,6 +697,7 @@ class TaskTools(ToolBase):
665
697
  return {
666
698
  "profile": profile,
667
699
  "ws_id": session_profile.selected_ws_id,
700
+ "request_route": self._request_route_payload(context),
668
701
  "app_key": app_key,
669
702
  "list_type": list_type,
670
703
  "list_type_label": get_record_list_type_label(list_type),
@@ -690,3 +723,121 @@ class TaskTools(ToolBase):
690
723
  f"Invalid process_status: {process_status}. Must be one of {valid_statuses}"
691
724
  )
692
725
  )
726
+
727
+ def _task_box_to_type(self, task_box: str) -> int:
728
+ normalized = (task_box or "").strip().lower()
729
+ if normalized not in TASK_BOX_TO_TYPE:
730
+ raise_tool_error(QingflowApiError.config_error("task_box must be todo, initiated, cc, or done"))
731
+ return TASK_BOX_TO_TYPE[normalized]
732
+
733
+ def _flow_status_to_process_status(self, flow_status: str) -> int:
734
+ normalized = (flow_status or "").strip().lower()
735
+ if normalized not in FLOW_STATUS_TO_PROCESS_STATUS:
736
+ raise_tool_error(
737
+ QingflowApiError.config_error(
738
+ "flow_status must be all, in_progress, approved, rejected, pending_fix, urged, overdue, due_soon, unread, or ended"
739
+ )
740
+ )
741
+ return FLOW_STATUS_TO_PROCESS_STATUS[normalized]
742
+
743
+ def _task_sort_to_create_time_asc(self, sort_by: str | None, sort_direction: str) -> bool | None:
744
+ normalized_sort_by = (sort_by or "").strip().lower() if sort_by is not None else ""
745
+ normalized_direction = (sort_direction or "desc").strip().lower()
746
+ if normalized_direction not in {"asc", "desc"}:
747
+ raise_tool_error(QingflowApiError.config_error("sort_direction must be asc or desc"))
748
+ if not normalized_sort_by:
749
+ return None
750
+ if normalized_sort_by not in {"created_at", "create_time"}:
751
+ raise_tool_error(QingflowApiError.config_error("task_list only supports sort_by=created_at"))
752
+ return normalized_direction == "asc"
753
+
754
+ def _task_applied_sort(self, sort_by: str | None, sort_direction: str) -> list[dict[str, Any]]:
755
+ if not sort_by:
756
+ return []
757
+ return [{"by": "created_at", "order": (sort_direction or "desc").strip().lower()}]
758
+
759
+ def _normalize_task_summary_payload(self, payload: Any) -> dict[str, Any]:
760
+ if not isinstance(payload, dict):
761
+ return {}
762
+ return {
763
+ "todo_count": payload.get("todoCount", payload.get("todo_count")),
764
+ "overdue_count": payload.get("timeoutCount", payload.get("timeout_count")),
765
+ "due_soon_count": payload.get("preTimeoutCount", payload.get("pre_timeout_count")),
766
+ "urged_count": payload.get("urgedCount", payload.get("urged_count")),
767
+ "cc_unread_count": payload.get("ccUnreadCount", payload.get("cc_unread_count")),
768
+ "in_progress_initiated_count": payload.get("processingCount", payload.get("processing_count")),
769
+ }
770
+
771
+ def _normalize_task_facets(self, payload: Any) -> list[dict[str, Any]]:
772
+ items = _task_page_items(payload)
773
+ groups: list[dict[str, Any]] = []
774
+ for item in items:
775
+ if not isinstance(item, dict):
776
+ continue
777
+ key = item.get("worksheetId", item.get("groupId", item.get("nodeId", item.get("id", item.get("key")))))
778
+ label = item.get("worksheetName", item.get("groupName", item.get("nodeName", item.get("name", item.get("label", item.get("title"))))))
779
+ count = item.get("count", item.get("taskCount", item.get("amount", item.get("todoCount", item.get("num")))))
780
+ groups.append({"key": key if key is not None else label, "label": label, "count": count})
781
+ return groups
782
+
783
+ def _public_task_action_response(
784
+ self,
785
+ raw: dict[str, Any],
786
+ *,
787
+ action: str,
788
+ resource: dict[str, Any],
789
+ selection: dict[str, Any],
790
+ ) -> dict[str, Any]:
791
+ response = dict(raw)
792
+ response["ok"] = bool(raw.get("ok", True))
793
+ response["warnings"] = []
794
+ response["output_profile"] = "normal"
795
+ response["data"] = {
796
+ "action": action,
797
+ "resource": resource,
798
+ "selection": selection,
799
+ "result": raw.get("result"),
800
+ }
801
+ return response
802
+
803
+ def _request_route_payload(self, context) -> dict[str, Any]: # type: ignore[no-untyped-def]
804
+ describe_route = getattr(self.backend, "describe_route", None)
805
+ if callable(describe_route):
806
+ payload = describe_route(context)
807
+ if isinstance(payload, dict):
808
+ return payload
809
+ return {
810
+ "base_url": getattr(context, "base_url", None),
811
+ "qf_version": getattr(context, "qf_version", None),
812
+ "qf_version_source": getattr(context, "qf_version_source", None) or ("context" if getattr(context, "qf_version", None) else "unknown"),
813
+ }
814
+
815
+
816
+ def _task_page_items(payload: Any) -> list[Any]:
817
+ if isinstance(payload, list):
818
+ return payload
819
+ if not isinstance(payload, dict):
820
+ return []
821
+ for key in ("list", "items", "rows", "result"):
822
+ value = payload.get(key)
823
+ if isinstance(value, list):
824
+ return value
825
+ for container_key in ("page", "data"):
826
+ nested = payload.get(container_key)
827
+ if isinstance(nested, dict):
828
+ nested_items = _task_page_items(nested)
829
+ if nested_items:
830
+ return nested_items
831
+ return []
832
+
833
+
834
+ def _task_page_amount(payload: Any) -> Any:
835
+ if isinstance(payload, dict):
836
+ return payload.get("pageAmount", payload.get("page_amount"))
837
+ return None
838
+
839
+
840
+ def _task_page_total(payload: Any) -> Any:
841
+ if isinstance(payload, dict):
842
+ return payload.get("total", payload.get("count"))
843
+ return None