@josephyan/qingflow-cli 0.2.0-beta.55

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 (79) hide show
  1. package/README.md +30 -0
  2. package/docs/local-agent-install.md +235 -0
  3. package/entry_point.py +13 -0
  4. package/npm/bin/qingflow.mjs +5 -0
  5. package/npm/lib/runtime.mjs +204 -0
  6. package/npm/scripts/postinstall.mjs +16 -0
  7. package/package.json +34 -0
  8. package/pyproject.toml +67 -0
  9. package/qingflow +15 -0
  10. package/src/qingflow_mcp/__init__.py +5 -0
  11. package/src/qingflow_mcp/__main__.py +5 -0
  12. package/src/qingflow_mcp/backend_client.py +547 -0
  13. package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
  14. package/src/qingflow_mcp/builder_facade/models.py +985 -0
  15. package/src/qingflow_mcp/builder_facade/service.py +8243 -0
  16. package/src/qingflow_mcp/cli/__init__.py +1 -0
  17. package/src/qingflow_mcp/cli/commands/__init__.py +15 -0
  18. package/src/qingflow_mcp/cli/commands/app.py +40 -0
  19. package/src/qingflow_mcp/cli/commands/auth.py +78 -0
  20. package/src/qingflow_mcp/cli/commands/builder.py +184 -0
  21. package/src/qingflow_mcp/cli/commands/common.py +47 -0
  22. package/src/qingflow_mcp/cli/commands/imports.py +86 -0
  23. package/src/qingflow_mcp/cli/commands/record.py +202 -0
  24. package/src/qingflow_mcp/cli/commands/task.py +87 -0
  25. package/src/qingflow_mcp/cli/commands/workspace.py +33 -0
  26. package/src/qingflow_mcp/cli/context.py +48 -0
  27. package/src/qingflow_mcp/cli/formatters.py +269 -0
  28. package/src/qingflow_mcp/cli/json_io.py +50 -0
  29. package/src/qingflow_mcp/cli/main.py +147 -0
  30. package/src/qingflow_mcp/config.py +221 -0
  31. package/src/qingflow_mcp/errors.py +66 -0
  32. package/src/qingflow_mcp/import_store.py +121 -0
  33. package/src/qingflow_mcp/json_types.py +18 -0
  34. package/src/qingflow_mcp/list_type_labels.py +76 -0
  35. package/src/qingflow_mcp/server.py +211 -0
  36. package/src/qingflow_mcp/server_app_builder.py +387 -0
  37. package/src/qingflow_mcp/server_app_user.py +317 -0
  38. package/src/qingflow_mcp/session_store.py +289 -0
  39. package/src/qingflow_mcp/solution/__init__.py +6 -0
  40. package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
  41. package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
  42. package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
  43. package/src/qingflow_mcp/solution/compiler/form_compiler.py +466 -0
  44. package/src/qingflow_mcp/solution/compiler/icon_utils.py +113 -0
  45. package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
  46. package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
  47. package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
  48. package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
  49. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
  50. package/src/qingflow_mcp/solution/design_session.py +222 -0
  51. package/src/qingflow_mcp/solution/design_store.py +100 -0
  52. package/src/qingflow_mcp/solution/executor.py +2339 -0
  53. package/src/qingflow_mcp/solution/normalizer.py +23 -0
  54. package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
  55. package/src/qingflow_mcp/solution/run_store.py +244 -0
  56. package/src/qingflow_mcp/solution/spec_models.py +853 -0
  57. package/src/qingflow_mcp/tools/__init__.py +1 -0
  58. package/src/qingflow_mcp/tools/ai_builder_tools.py +2063 -0
  59. package/src/qingflow_mcp/tools/app_tools.py +850 -0
  60. package/src/qingflow_mcp/tools/approval_tools.py +833 -0
  61. package/src/qingflow_mcp/tools/auth_tools.py +697 -0
  62. package/src/qingflow_mcp/tools/base.py +81 -0
  63. package/src/qingflow_mcp/tools/code_block_tools.py +679 -0
  64. package/src/qingflow_mcp/tools/directory_tools.py +648 -0
  65. package/src/qingflow_mcp/tools/feedback_tools.py +230 -0
  66. package/src/qingflow_mcp/tools/file_tools.py +385 -0
  67. package/src/qingflow_mcp/tools/import_tools.py +1971 -0
  68. package/src/qingflow_mcp/tools/navigation_tools.py +177 -0
  69. package/src/qingflow_mcp/tools/package_tools.py +240 -0
  70. package/src/qingflow_mcp/tools/portal_tools.py +131 -0
  71. package/src/qingflow_mcp/tools/qingbi_report_tools.py +269 -0
  72. package/src/qingflow_mcp/tools/record_tools.py +12739 -0
  73. package/src/qingflow_mcp/tools/role_tools.py +94 -0
  74. package/src/qingflow_mcp/tools/solution_tools.py +3887 -0
  75. package/src/qingflow_mcp/tools/task_context_tools.py +1423 -0
  76. package/src/qingflow_mcp/tools/task_tools.py +843 -0
  77. package/src/qingflow_mcp/tools/view_tools.py +280 -0
  78. package/src/qingflow_mcp/tools/workflow_tools.py +312 -0
  79. package/src/qingflow_mcp/tools/workspace_tools.py +219 -0
@@ -0,0 +1,843 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from mcp.server.fastmcp import FastMCP
6
+
7
+ from ..config import DEFAULT_PROFILE
8
+ from ..errors import QingflowApiError, raise_tool_error
9
+ from ..list_type_labels import get_record_list_type_label, get_task_type_label
10
+ from .base import ToolBase
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
+
32
+
33
+ class TaskTools(ToolBase):
34
+ """任务中心(待办/已办)相关工具
35
+
36
+ 提供对工作流任务的管理功能,包括:
37
+ - 查询待办/已办列表
38
+ - 查看任务统计
39
+ - 标记已读/催办
40
+ - 查询节点和表单分组信息
41
+
42
+ 消息类型 (type):
43
+ - 1: 待办 (TODO) - 需要当前用户处理的任务
44
+ - 2: 我发起的 (INITIATED) - 当前用户发起的流程
45
+ - 3: 抄送 (CC) - 抄送给当前用户的任务
46
+ - 5: 已办 (DONE) - 当前用户已处理的任务
47
+
48
+ 流程状态 (process_status):
49
+ - 1: 全部
50
+ - 2: 流程中
51
+ - 3: 已通过
52
+ - 4: 已拒绝
53
+ - 5: 待完善
54
+ - 6: 催办
55
+ - 7: 超时
56
+ - 8: 即将超时
57
+ - 9: 未读
58
+ - 10: 流程结束
59
+ """
60
+
61
+ def register(self, mcp: FastMCP) -> None:
62
+ @mcp.tool()
63
+ def task_summary(
64
+ profile: str = DEFAULT_PROFILE,
65
+ app_key: str | None = None,
66
+ ) -> dict[str, Any]:
67
+ return self.task_summary(
68
+ profile=profile,
69
+ app_key=app_key,
70
+ )
71
+
72
+ @mcp.tool()
73
+ def task_list(
74
+ profile: str = DEFAULT_PROFILE,
75
+ task_box: str = "todo",
76
+ flow_status: str = "all",
77
+ app_key: str | None = None,
78
+ workflow_node_id: int | None = None,
79
+ query: str | None = None,
80
+ page: int = 1,
81
+ page_size: int = 20,
82
+ sort_by: str | None = None,
83
+ sort_direction: str = "desc",
84
+ ) -> dict[str, Any]:
85
+ return self.task_list_public(
86
+ profile=profile,
87
+ task_box=task_box,
88
+ flow_status=flow_status,
89
+ app_key=app_key,
90
+ workflow_node_id=workflow_node_id,
91
+ query=query,
92
+ page=page,
93
+ page_size=page_size,
94
+ sort_by=sort_by,
95
+ sort_direction=sort_direction,
96
+ )
97
+
98
+ @mcp.tool()
99
+ def task_facets(
100
+ profile: str = DEFAULT_PROFILE,
101
+ task_box: str = "todo",
102
+ flow_status: str = "all",
103
+ dimension: str = "worksheet",
104
+ app_key: str | None = None,
105
+ query: str | None = None,
106
+ limit: int = 50,
107
+ ) -> dict[str, Any]:
108
+ return self.task_facets(
109
+ profile=profile,
110
+ task_box=task_box,
111
+ flow_status=flow_status,
112
+ dimension=dimension,
113
+ app_key=app_key,
114
+ query=query,
115
+ limit=limit,
116
+ )
117
+
118
+ @mcp.tool()
119
+ def task_mark_read(
120
+ profile: str = DEFAULT_PROFILE,
121
+ app_key: str = "",
122
+ task_id: int = 0,
123
+ task_box: str = "todo",
124
+ ) -> dict[str, Any]:
125
+ return self.task_mark_read_public(profile=profile, app_key=app_key, task_id=task_id, task_box=task_box)
126
+
127
+ @mcp.tool()
128
+ def task_mark_all_cc_read(
129
+ profile: str = DEFAULT_PROFILE,
130
+ flow_status: str = "all",
131
+ ) -> dict[str, Any]:
132
+ return self.task_mark_all_cc_read_public(profile=profile, flow_status=flow_status)
133
+
134
+ @mcp.tool()
135
+ def task_urge(
136
+ profile: str = DEFAULT_PROFILE,
137
+ app_key: str = "",
138
+ record_id: int = 0,
139
+ ) -> dict[str, Any]:
140
+ return self.task_urge_public(profile=profile, app_key=app_key, record_id=record_id)
141
+
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(
248
+ profile=profile,
249
+ app_key=app_key,
250
+ type=normalized_type,
251
+ search_key=query,
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
+ )
335
+
336
+ def task_list(
337
+ self,
338
+ *,
339
+ profile: str,
340
+ type: int,
341
+ process_status: int,
342
+ app_key: str | None,
343
+ node_id: int | None,
344
+ search_key: str | None,
345
+ page_num: int,
346
+ page_size: int,
347
+ create_time_asc: bool | None,
348
+ ) -> dict[str, Any]:
349
+ self._validate_type(type)
350
+ self._validate_process_status(process_status)
351
+
352
+ def runner(session_profile, context):
353
+ payload: dict[str, Any] = {
354
+ "type": type,
355
+ "processStatus": process_status,
356
+ "pageNum": page_num,
357
+ "pageSize": page_size,
358
+ }
359
+ if app_key is not None:
360
+ payload["appKey"] = app_key
361
+ if node_id is not None:
362
+ payload["nodeId"] = node_id
363
+ if search_key:
364
+ payload["searchKey"] = search_key
365
+ if create_time_asc is not None:
366
+ payload["createTimeAsc"] = create_time_asc
367
+
368
+ result = self.backend.request("POST", context, "/task/dynamic/page", json_body=payload)
369
+ return {
370
+ "profile": profile,
371
+ "ws_id": session_profile.selected_ws_id,
372
+ "request_route": self._request_route_payload(context),
373
+ "type": type,
374
+ "type_label": get_task_type_label(type),
375
+ "list_type_label": get_task_type_label(type),
376
+ "process_status": process_status,
377
+ "page": result,
378
+ }
379
+
380
+ return self._run(profile, runner)
381
+
382
+ def task_list_grouped(
383
+ self,
384
+ *,
385
+ profile: str,
386
+ type: int,
387
+ process_status: int,
388
+ app_key: str | None,
389
+ node_id: int | None,
390
+ search_key: str | None,
391
+ page_num: int,
392
+ page_size: int,
393
+ ) -> dict[str, Any]:
394
+ self._validate_type(type)
395
+ self._validate_process_status(process_status)
396
+
397
+ def runner(session_profile, context):
398
+ payload: dict[str, Any] = {
399
+ "type": type,
400
+ "processStatus": process_status,
401
+ "pageNum": page_num,
402
+ "pageSize": page_size,
403
+ }
404
+ if app_key is not None:
405
+ payload["appKey"] = app_key
406
+ if node_id is not None:
407
+ payload["nodeId"] = node_id
408
+ if search_key:
409
+ payload["searchKey"] = search_key
410
+
411
+ result = self.backend.request("POST", context, "/task/dynamic/page/group", json_body=payload)
412
+ return {
413
+ "profile": profile,
414
+ "ws_id": session_profile.selected_ws_id,
415
+ "request_route": self._request_route_payload(context),
416
+ "type": type,
417
+ "type_label": get_task_type_label(type),
418
+ "list_type_label": get_task_type_label(type),
419
+ "process_status": process_status,
420
+ "page": result,
421
+ }
422
+
423
+ return self._run(profile, runner)
424
+
425
+ def task_statistics(
426
+ self,
427
+ *,
428
+ profile: str,
429
+ app_key: str | None,
430
+ ) -> dict[str, Any]:
431
+ def runner(session_profile, context):
432
+ params: dict[str, Any] = {}
433
+ if app_key:
434
+ params["appKey"] = app_key
435
+
436
+ result = self.backend.request("GET", context, "/task/dynamic/statics", params=params)
437
+ return {
438
+ "profile": profile,
439
+ "ws_id": session_profile.selected_ws_id,
440
+ "request_route": self._request_route_payload(context),
441
+ "statistics": result,
442
+ }
443
+
444
+ return self._run(profile, runner)
445
+
446
+ def task_workflow_nodes(
447
+ self,
448
+ *,
449
+ profile: str,
450
+ type: int,
451
+ status: str | None,
452
+ app_key_list: list[str] | None,
453
+ search_key: str | None,
454
+ page_num: int,
455
+ page_size: int,
456
+ ) -> dict[str, Any]:
457
+ self._validate_type(type)
458
+
459
+ def runner(session_profile, context):
460
+ params: dict[str, Any] = {
461
+ "type": type,
462
+ "pageNum": page_num,
463
+ "pageSize": page_size,
464
+ }
465
+ if status is not None:
466
+ params["status"] = status
467
+ if app_key_list:
468
+ params["appKeyList"] = app_key_list
469
+ if search_key:
470
+ params["searchKey"] = search_key
471
+
472
+ result = self.backend.request("GET", context, "/task/dynamic/workflow/nodes", params=params)
473
+ return {
474
+ "profile": profile,
475
+ "ws_id": session_profile.selected_ws_id,
476
+ "request_route": self._request_route_payload(context),
477
+ "type": type,
478
+ "type_label": get_task_type_label(type),
479
+ "list_type_label": get_task_type_label(type),
480
+ "page": result,
481
+ }
482
+
483
+ return self._run(profile, runner)
484
+
485
+ def task_node_statistics(
486
+ self,
487
+ *,
488
+ profile: str,
489
+ app_key: str,
490
+ type: int,
491
+ search_key: str | None,
492
+ ) -> dict[str, Any]:
493
+ self._validate_type(type)
494
+ if not app_key:
495
+ raise_tool_error(QingflowApiError.config_error("app_key is required"))
496
+
497
+ def runner(session_profile, context):
498
+ params: dict[str, Any] = {
499
+ "appKey": app_key,
500
+ "type": type,
501
+ }
502
+ if search_key:
503
+ params["searchKey"] = search_key
504
+
505
+ result = self.backend.request("GET", context, "/task/dynamic/statics/node", params=params)
506
+ return {
507
+ "profile": profile,
508
+ "ws_id": session_profile.selected_ws_id,
509
+ "request_route": self._request_route_payload(context),
510
+ "app_key": app_key,
511
+ "type": type,
512
+ "type_label": get_task_type_label(type),
513
+ "list_type_label": get_task_type_label(type),
514
+ "nodes": result,
515
+ }
516
+
517
+ return self._run(profile, runner)
518
+
519
+ def task_worksheet_statistics(
520
+ self,
521
+ *,
522
+ profile: str,
523
+ type: int,
524
+ worksheet_name: str | None,
525
+ page_num: int,
526
+ page_size: int,
527
+ ) -> dict[str, Any]:
528
+ self._validate_type(type)
529
+
530
+ def runner(session_profile, context):
531
+ params: dict[str, Any] = {
532
+ "type": type,
533
+ "pageNum": page_num,
534
+ "pageSize": page_size,
535
+ }
536
+ if worksheet_name:
537
+ params["worksheetName"] = worksheet_name
538
+
539
+ result = self.backend.request("GET", context, "/task/dynamic/statics/worksheet", params=params)
540
+ return {
541
+ "profile": profile,
542
+ "ws_id": session_profile.selected_ws_id,
543
+ "request_route": self._request_route_payload(context),
544
+ "type": type,
545
+ "type_label": get_task_type_label(type),
546
+ "list_type_label": get_task_type_label(type),
547
+ "page": result,
548
+ }
549
+
550
+ return self._run(profile, runner)
551
+
552
+ def task_mark_read(
553
+ self,
554
+ *,
555
+ profile: str,
556
+ app_key: str,
557
+ id: int,
558
+ type: int,
559
+ ) -> dict[str, Any]:
560
+ if not app_key:
561
+ raise_tool_error(QingflowApiError.config_error("app_key is required"))
562
+ if id <= 0:
563
+ raise_tool_error(QingflowApiError.config_error("id must be positive"))
564
+ self._validate_type(type)
565
+
566
+ def runner(session_profile, context):
567
+ result = self.backend.request(
568
+ "POST",
569
+ context,
570
+ f"/task/dynamic/{app_key}/{id}/read/{type}",
571
+ )
572
+ return {
573
+ "profile": profile,
574
+ "ws_id": session_profile.selected_ws_id,
575
+ "request_route": self._request_route_payload(context),
576
+ "app_key": app_key,
577
+ "id": id,
578
+ "type": type,
579
+ "type_label": get_task_type_label(type),
580
+ "list_type_label": get_task_type_label(type),
581
+ "result": result,
582
+ }
583
+
584
+ return self._run(profile, runner)
585
+
586
+ def task_mark_all_cc_read(
587
+ self,
588
+ *,
589
+ profile: str,
590
+ type: int,
591
+ process_status: int,
592
+ ) -> dict[str, Any]:
593
+ self._validate_type(type)
594
+ self._validate_process_status(process_status)
595
+
596
+ def runner(session_profile, context):
597
+ payload: dict[str, Any] = {
598
+ "type": type,
599
+ "processStatus": process_status,
600
+ }
601
+ result = self.backend.request("POST", context, "/task/dynamic/cc/readAll", json_body=payload)
602
+ return {
603
+ "profile": profile,
604
+ "ws_id": session_profile.selected_ws_id,
605
+ "request_route": self._request_route_payload(context),
606
+ "type": type,
607
+ "type_label": get_task_type_label(type),
608
+ "list_type_label": get_task_type_label(type),
609
+ "process_status": process_status,
610
+ "result": result,
611
+ }
612
+
613
+ return self._run(profile, runner)
614
+
615
+ def task_urge(
616
+ self,
617
+ *,
618
+ profile: str,
619
+ app_key: str,
620
+ row_record_id: int,
621
+ ) -> dict[str, Any]:
622
+ if not app_key:
623
+ raise_tool_error(QingflowApiError.config_error("app_key is required"))
624
+ if row_record_id <= 0:
625
+ raise_tool_error(QingflowApiError.config_error("row_record_id must be positive"))
626
+
627
+ def runner(session_profile, context):
628
+ result = self.backend.request(
629
+ "POST",
630
+ context,
631
+ f"/task/dynamic/{app_key}/{row_record_id}/urge",
632
+ )
633
+ return {
634
+ "profile": profile,
635
+ "ws_id": session_profile.selected_ws_id,
636
+ "request_route": self._request_route_payload(context),
637
+ "app_key": app_key,
638
+ "row_record_id": row_record_id,
639
+ "result": result,
640
+ }
641
+
642
+ return self._run(profile, runner)
643
+
644
+ def task_group_detail(
645
+ self,
646
+ *,
647
+ profile: str,
648
+ app_key: str,
649
+ group_id: int,
650
+ ) -> dict[str, Any]:
651
+ if not app_key:
652
+ raise_tool_error(QingflowApiError.config_error("app_key is required"))
653
+ if group_id <= 0:
654
+ raise_tool_error(QingflowApiError.config_error("group_id must be positive"))
655
+
656
+ def runner(session_profile, context):
657
+ result = self.backend.request(
658
+ "GET",
659
+ context,
660
+ f"/task/dynamic/app/{app_key}/group/{group_id}/detail",
661
+ )
662
+ return {
663
+ "profile": profile,
664
+ "ws_id": session_profile.selected_ws_id,
665
+ "request_route": self._request_route_payload(context),
666
+ "app_key": app_key,
667
+ "group_id": group_id,
668
+ "detail": result,
669
+ }
670
+
671
+ return self._run(profile, runner)
672
+
673
+ def task_batch_processing_amount(
674
+ self,
675
+ *,
676
+ profile: str,
677
+ app_key: str,
678
+ list_type: int,
679
+ task_center_filter: dict[str, Any] | None,
680
+ ) -> dict[str, Any]:
681
+ if not app_key:
682
+ raise_tool_error(QingflowApiError.config_error("app_key is required"))
683
+
684
+ def runner(session_profile, context):
685
+ payload: dict[str, Any] = {
686
+ "listType": list_type,
687
+ }
688
+ if task_center_filter is not None:
689
+ payload["taskCenterFilter"] = task_center_filter
690
+
691
+ result = self.backend.request(
692
+ "POST",
693
+ context,
694
+ f"/task/app/{app_key}/batchProcessingAmount",
695
+ json_body=payload,
696
+ )
697
+ return {
698
+ "profile": profile,
699
+ "ws_id": session_profile.selected_ws_id,
700
+ "request_route": self._request_route_payload(context),
701
+ "app_key": app_key,
702
+ "list_type": list_type,
703
+ "list_type_label": get_record_list_type_label(list_type),
704
+ "amount": result,
705
+ }
706
+
707
+ return self._run(profile, runner)
708
+
709
+ def _validate_type(self, type: int) -> None:
710
+ valid_types = [1, 2, 3, 5] # TODO, INITIATED, CC, DONE
711
+ if type not in valid_types:
712
+ raise_tool_error(
713
+ QingflowApiError.config_error(
714
+ f"Invalid type: {type}. Must be one of {valid_types} (1=待办, 2=我发起的, 3=抄送, 5=已办)"
715
+ )
716
+ )
717
+
718
+ def _validate_process_status(self, process_status: int) -> None:
719
+ valid_statuses = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
720
+ if process_status not in valid_statuses:
721
+ raise_tool_error(
722
+ QingflowApiError.config_error(
723
+ f"Invalid process_status: {process_status}. Must be one of {valid_statuses}"
724
+ )
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