@josephyan/qingflow-cli 0.2.0-beta.1000

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 (92) hide show
  1. package/README.md +31 -0
  2. package/docs/local-agent-install.md +309 -0
  3. package/entry_point.py +13 -0
  4. package/npm/bin/qingflow.mjs +5 -0
  5. package/npm/lib/runtime.mjs +346 -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 +37 -0
  11. package/src/qingflow_mcp/__main__.py +5 -0
  12. package/src/qingflow_mcp/backend_client.py +649 -0
  13. package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
  14. package/src/qingflow_mcp/builder_facade/models.py +1846 -0
  15. package/src/qingflow_mcp/builder_facade/service.py +16502 -0
  16. package/src/qingflow_mcp/cli/__init__.py +1 -0
  17. package/src/qingflow_mcp/cli/commands/__init__.py +18 -0
  18. package/src/qingflow_mcp/cli/commands/app.py +40 -0
  19. package/src/qingflow_mcp/cli/commands/auth.py +112 -0
  20. package/src/qingflow_mcp/cli/commands/builder.py +539 -0
  21. package/src/qingflow_mcp/cli/commands/chart.py +18 -0
  22. package/src/qingflow_mcp/cli/commands/common.py +62 -0
  23. package/src/qingflow_mcp/cli/commands/imports.py +96 -0
  24. package/src/qingflow_mcp/cli/commands/portal.py +25 -0
  25. package/src/qingflow_mcp/cli/commands/record.py +331 -0
  26. package/src/qingflow_mcp/cli/commands/repo.py +80 -0
  27. package/src/qingflow_mcp/cli/commands/task.py +141 -0
  28. package/src/qingflow_mcp/cli/commands/view.py +18 -0
  29. package/src/qingflow_mcp/cli/commands/workspace.py +110 -0
  30. package/src/qingflow_mcp/cli/context.py +60 -0
  31. package/src/qingflow_mcp/cli/formatters.py +573 -0
  32. package/src/qingflow_mcp/cli/json_io.py +50 -0
  33. package/src/qingflow_mcp/cli/main.py +186 -0
  34. package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
  35. package/src/qingflow_mcp/cli/terminal_ui.py +173 -0
  36. package/src/qingflow_mcp/config.py +407 -0
  37. package/src/qingflow_mcp/errors.py +66 -0
  38. package/src/qingflow_mcp/id_utils.py +49 -0
  39. package/src/qingflow_mcp/import_store.py +121 -0
  40. package/src/qingflow_mcp/json_types.py +18 -0
  41. package/src/qingflow_mcp/list_type_labels.py +76 -0
  42. package/src/qingflow_mcp/public_surface.py +243 -0
  43. package/src/qingflow_mcp/repository_store.py +71 -0
  44. package/src/qingflow_mcp/response_trim.py +841 -0
  45. package/src/qingflow_mcp/server.py +216 -0
  46. package/src/qingflow_mcp/server_app_builder.py +543 -0
  47. package/src/qingflow_mcp/server_app_user.py +386 -0
  48. package/src/qingflow_mcp/session_store.py +369 -0
  49. package/src/qingflow_mcp/solution/__init__.py +6 -0
  50. package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
  51. package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
  52. package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
  53. package/src/qingflow_mcp/solution/compiler/form_compiler.py +495 -0
  54. package/src/qingflow_mcp/solution/compiler/icon_utils.py +187 -0
  55. package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
  56. package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
  57. package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
  58. package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
  59. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
  60. package/src/qingflow_mcp/solution/design_session.py +222 -0
  61. package/src/qingflow_mcp/solution/design_store.py +100 -0
  62. package/src/qingflow_mcp/solution/executor.py +2398 -0
  63. package/src/qingflow_mcp/solution/normalizer.py +23 -0
  64. package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
  65. package/src/qingflow_mcp/solution/run_store.py +244 -0
  66. package/src/qingflow_mcp/solution/spec_models.py +855 -0
  67. package/src/qingflow_mcp/tools/__init__.py +1 -0
  68. package/src/qingflow_mcp/tools/ai_builder_tools.py +3449 -0
  69. package/src/qingflow_mcp/tools/app_tools.py +926 -0
  70. package/src/qingflow_mcp/tools/approval_tools.py +1062 -0
  71. package/src/qingflow_mcp/tools/auth_tools.py +1133 -0
  72. package/src/qingflow_mcp/tools/base.py +281 -0
  73. package/src/qingflow_mcp/tools/code_block_tools.py +777 -0
  74. package/src/qingflow_mcp/tools/custom_button_tools.py +202 -0
  75. package/src/qingflow_mcp/tools/directory_tools.py +675 -0
  76. package/src/qingflow_mcp/tools/feedback_tools.py +238 -0
  77. package/src/qingflow_mcp/tools/file_tools.py +409 -0
  78. package/src/qingflow_mcp/tools/import_tools.py +2223 -0
  79. package/src/qingflow_mcp/tools/navigation_tools.py +210 -0
  80. package/src/qingflow_mcp/tools/package_tools.py +326 -0
  81. package/src/qingflow_mcp/tools/portal_tools.py +158 -0
  82. package/src/qingflow_mcp/tools/qingbi_report_tools.py +374 -0
  83. package/src/qingflow_mcp/tools/record_tools.py +14291 -0
  84. package/src/qingflow_mcp/tools/repository_dev_tools.py +552 -0
  85. package/src/qingflow_mcp/tools/resource_read_tools.py +503 -0
  86. package/src/qingflow_mcp/tools/role_tools.py +112 -0
  87. package/src/qingflow_mcp/tools/solution_tools.py +4054 -0
  88. package/src/qingflow_mcp/tools/task_context_tools.py +2986 -0
  89. package/src/qingflow_mcp/tools/task_tools.py +889 -0
  90. package/src/qingflow_mcp/tools/view_tools.py +335 -0
  91. package/src/qingflow_mcp/tools/workflow_tools.py +376 -0
  92. package/src/qingflow_mcp/tools/workspace_tools.py +266 -0
@@ -0,0 +1,889 @@
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, tool_cn_name
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
+ - 查询节点和表单分组信息
43
+
44
+ 消息类型 (type):
45
+ - 1: 待办 (TODO) - 需要当前用户处理的任务
46
+ - 2: 我发起的 (INITIATED) - 当前用户发起的流程
47
+ - 3: 抄送 (CC) - 抄送给当前用户的任务
48
+ - 5: 已办 (DONE) - 当前用户已处理的任务
49
+
50
+ 流程状态 (process_status):
51
+ - 1: 全部
52
+ - 2: 流程中
53
+ - 3: 已通过
54
+ - 4: 已拒绝
55
+ - 5: 待完善
56
+ - 6: 催办
57
+ - 7: 超时
58
+ - 8: 即将超时
59
+ - 9: 未读
60
+ - 10: 流程结束
61
+ """
62
+
63
+ def register(self, mcp: FastMCP) -> None:
64
+ """注册当前工具到 MCP 服务。"""
65
+ @mcp.tool()
66
+ def task_summary(
67
+ profile: str = DEFAULT_PROFILE,
68
+ app_key: str | None = None,
69
+ ) -> dict[str, Any]:
70
+ return self.task_summary(
71
+ profile=profile,
72
+ app_key=app_key,
73
+ )
74
+
75
+ @mcp.tool()
76
+ def task_list(
77
+ profile: str = DEFAULT_PROFILE,
78
+ task_box: str = "todo",
79
+ flow_status: str = "all",
80
+ app_key: str | None = None,
81
+ workflow_node_id: int | None = None,
82
+ query: str | None = None,
83
+ page: int = 1,
84
+ page_size: int = 20,
85
+ sort_by: str | None = None,
86
+ sort_direction: str = "desc",
87
+ ) -> dict[str, Any]:
88
+ return self.task_list_public(
89
+ profile=profile,
90
+ task_box=task_box,
91
+ flow_status=flow_status,
92
+ app_key=app_key,
93
+ workflow_node_id=workflow_node_id,
94
+ query=query,
95
+ page=page,
96
+ page_size=page_size,
97
+ sort_by=sort_by,
98
+ sort_direction=sort_direction,
99
+ )
100
+
101
+ @mcp.tool()
102
+ def task_facets(
103
+ profile: str = DEFAULT_PROFILE,
104
+ task_box: str = "todo",
105
+ flow_status: str = "all",
106
+ dimension: str = "worksheet",
107
+ app_key: str | None = None,
108
+ query: str | None = None,
109
+ limit: int = 50,
110
+ ) -> dict[str, Any]:
111
+ return self.task_facets(
112
+ profile=profile,
113
+ task_box=task_box,
114
+ flow_status=flow_status,
115
+ dimension=dimension,
116
+ app_key=app_key,
117
+ query=query,
118
+ limit=limit,
119
+ )
120
+
121
+ @mcp.tool()
122
+ def task_mark_read(
123
+ profile: str = DEFAULT_PROFILE,
124
+ app_key: str = "",
125
+ task_id: int = 0,
126
+ task_box: str = "todo",
127
+ ) -> dict[str, Any]:
128
+ return self.task_mark_read_public(profile=profile, app_key=app_key, task_id=task_id, task_box=task_box)
129
+
130
+ @mcp.tool()
131
+ def task_mark_all_cc_read(
132
+ profile: str = DEFAULT_PROFILE,
133
+ flow_status: str = "all",
134
+ ) -> dict[str, Any]:
135
+ return self.task_mark_all_cc_read_public(profile=profile, flow_status=flow_status)
136
+
137
+ @mcp.tool()
138
+ def task_urge(
139
+ profile: str = DEFAULT_PROFILE,
140
+ app_key: str = "",
141
+ record_id: int = 0,
142
+ ) -> dict[str, Any]:
143
+ return self.task_urge_public(profile=profile, app_key=app_key, record_id=record_id)
144
+
145
+ @tool_cn_name("任务摘要")
146
+ def task_summary(
147
+ self,
148
+ *,
149
+ profile: str,
150
+ app_key: str | None,
151
+ ) -> dict[str, Any]:
152
+ """执行任务相关逻辑。"""
153
+ raw = self.task_statistics(profile=profile, app_key=app_key)
154
+ statistics = raw.get("statistics", {})
155
+ summary = self._normalize_task_summary_payload(statistics)
156
+ return {
157
+ "profile": profile,
158
+ "ws_id": raw.get("ws_id"),
159
+ "ok": True,
160
+ "request_route": raw.get("request_route"),
161
+ "warnings": [],
162
+ "output_profile": "normal",
163
+ "data": {
164
+ "summary": summary,
165
+ },
166
+ }
167
+
168
+ @tool_cn_name("任务列表")
169
+ def task_list_public(
170
+ self,
171
+ *,
172
+ profile: str,
173
+ task_box: str,
174
+ flow_status: str,
175
+ app_key: str | None,
176
+ workflow_node_id: int | None,
177
+ query: str | None,
178
+ page: int,
179
+ page_size: int,
180
+ sort_by: str | None,
181
+ sort_direction: str,
182
+ ) -> dict[str, Any]:
183
+ """执行任务相关逻辑。"""
184
+ normalized_type = self._task_box_to_type(task_box)
185
+ normalized_status = self._flow_status_to_process_status(flow_status)
186
+ create_time_asc = self._task_sort_to_create_time_asc(sort_by, sort_direction)
187
+ raw = self.task_list(
188
+ profile=profile,
189
+ type=normalized_type,
190
+ process_status=normalized_status,
191
+ app_key=app_key,
192
+ node_id=workflow_node_id,
193
+ search_key=query,
194
+ page_num=page,
195
+ page_size=page_size,
196
+ create_time_asc=create_time_asc,
197
+ )
198
+ task_page = raw.get("page", {})
199
+ return {
200
+ "profile": profile,
201
+ "ws_id": raw.get("ws_id"),
202
+ "ok": True,
203
+ "request_route": raw.get("request_route"),
204
+ "warnings": [],
205
+ "output_profile": "normal",
206
+ "data": {
207
+ "items": _task_page_items(task_page),
208
+ "pagination": {
209
+ "page": page,
210
+ "page_size": page_size,
211
+ "returned_items": len(_task_page_items(task_page)),
212
+ "page_amount": _task_page_amount(task_page),
213
+ "reported_total": _task_page_total(task_page),
214
+ },
215
+ "selection": {
216
+ "task_box": task_box,
217
+ "flow_status": flow_status,
218
+ "app_key": app_key,
219
+ "workflow_node_id": workflow_node_id,
220
+ "query": query,
221
+ "applied_sort": self._task_applied_sort(sort_by, sort_direction),
222
+ },
223
+ },
224
+ }
225
+
226
+ @tool_cn_name("任务分面统计")
227
+ def task_facets(
228
+ self,
229
+ *,
230
+ profile: str,
231
+ task_box: str,
232
+ flow_status: str,
233
+ dimension: str,
234
+ app_key: str | None,
235
+ query: str | None,
236
+ limit: int,
237
+ ) -> dict[str, Any]:
238
+ """执行任务相关逻辑。"""
239
+ normalized_type = self._task_box_to_type(task_box)
240
+ normalized_status = self._flow_status_to_process_status(flow_status)
241
+ if dimension not in {"worksheet", "workflow_node"}:
242
+ raise_tool_error(QingflowApiError.config_error("dimension must be worksheet or workflow_node"))
243
+ if limit <= 0:
244
+ raise_tool_error(QingflowApiError.config_error("limit must be positive"))
245
+
246
+ if dimension == "worksheet":
247
+ raw = self.task_worksheet_statistics(
248
+ profile=profile,
249
+ type=normalized_type,
250
+ worksheet_name=query,
251
+ page_num=1,
252
+ page_size=max(limit, 20),
253
+ )
254
+ source = raw.get("page", {})
255
+ elif app_key:
256
+ raw = self.task_node_statistics(
257
+ profile=profile,
258
+ app_key=app_key,
259
+ type=normalized_type,
260
+ search_key=query,
261
+ )
262
+ source = raw.get("nodes", {})
263
+ else:
264
+ raw = self.task_workflow_nodes(
265
+ profile=profile,
266
+ type=normalized_type,
267
+ status=str(normalized_status),
268
+ app_key_list=None,
269
+ search_key=query,
270
+ page_num=1,
271
+ page_size=max(limit, 20),
272
+ )
273
+ source = raw.get("page", {})
274
+
275
+ groups = self._normalize_task_facets(source)
276
+ rows_truncated = len(groups) > limit
277
+ returned_groups = groups[:limit]
278
+ return {
279
+ "profile": profile,
280
+ "ws_id": raw.get("ws_id"),
281
+ "ok": True,
282
+ "request_route": raw.get("request_route"),
283
+ "warnings": [],
284
+ "output_profile": "normal",
285
+ "data": {
286
+ "groups": returned_groups,
287
+ "rows_truncated": rows_truncated,
288
+ "statement_scope": "returned_groups_only" if rows_truncated else "full_population",
289
+ "selection": {
290
+ "task_box": task_box,
291
+ "flow_status": flow_status,
292
+ "dimension": dimension,
293
+ "app_key": app_key,
294
+ "query": query,
295
+ "limit": limit,
296
+ },
297
+ },
298
+ }
299
+
300
+ @tool_cn_name("任务标记已读")
301
+ def task_mark_read_public(
302
+ self,
303
+ *,
304
+ profile: str,
305
+ app_key: str,
306
+ task_id: int,
307
+ task_box: str,
308
+ ) -> dict[str, Any]:
309
+ """执行任务相关逻辑。"""
310
+ raw = self.task_mark_read(profile=profile, app_key=app_key, id=task_id, type=self._task_box_to_type(task_box))
311
+ return self._public_task_action_response(
312
+ raw,
313
+ action="mark_read",
314
+ resource={"app_key": app_key, "task_id": task_id},
315
+ selection={"task_box": task_box},
316
+ )
317
+
318
+ @tool_cn_name("抄送全部已读")
319
+ def task_mark_all_cc_read_public(
320
+ self,
321
+ *,
322
+ profile: str,
323
+ flow_status: str,
324
+ ) -> dict[str, Any]:
325
+ """执行任务相关逻辑。"""
326
+ 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))
327
+ return self._public_task_action_response(
328
+ raw,
329
+ action="mark_all_cc_read",
330
+ resource={},
331
+ selection={"task_box": "cc", "flow_status": flow_status},
332
+ )
333
+
334
+ @tool_cn_name("任务催办")
335
+ def task_urge_public(
336
+ self,
337
+ *,
338
+ profile: str,
339
+ app_key: str,
340
+ record_id: int,
341
+ ) -> dict[str, Any]:
342
+ """执行任务相关逻辑。"""
343
+ raw = self.task_urge(profile=profile, app_key=app_key, row_record_id=record_id)
344
+ return self._public_task_action_response(
345
+ raw,
346
+ action="urge",
347
+ resource={"app_key": app_key, "record_id": record_id},
348
+ selection={},
349
+ )
350
+
351
+ @tool_cn_name("任务列表(兼容)")
352
+ def task_list(
353
+ self,
354
+ *,
355
+ profile: str,
356
+ type: int,
357
+ process_status: int,
358
+ app_key: str | None,
359
+ node_id: int | None,
360
+ search_key: str | None,
361
+ page_num: int,
362
+ page_size: int,
363
+ create_time_asc: bool | None,
364
+ ) -> dict[str, Any]:
365
+ """执行任务相关逻辑。"""
366
+ self._validate_type(type)
367
+ self._validate_process_status(process_status)
368
+ def runner(session_profile, context):
369
+ payload: dict[str, Any] = {
370
+ "type": type,
371
+ "processStatus": process_status,
372
+ "pageNum": page_num,
373
+ "pageSize": page_size,
374
+ }
375
+ if app_key is not None:
376
+ payload["appKey"] = app_key
377
+ if node_id is not None:
378
+ payload["nodeId"] = node_id
379
+ if search_key:
380
+ payload["searchKey"] = search_key
381
+ if create_time_asc is not None:
382
+ payload["createTimeAsc"] = create_time_asc
383
+
384
+ result = self.backend.request("POST", context, "/task/dynamic/page", json_body=payload)
385
+ return {
386
+ "profile": profile,
387
+ "ws_id": session_profile.selected_ws_id,
388
+ "request_route": self._request_route_payload(context),
389
+ "type": type,
390
+ "type_label": get_task_type_label(type),
391
+ "list_type_label": get_task_type_label(type),
392
+ "process_status": process_status,
393
+ "page": result,
394
+ }
395
+
396
+ return self._run(profile, runner)
397
+
398
+ @tool_cn_name("任务分组列表")
399
+ def task_list_grouped(
400
+ self,
401
+ *,
402
+ profile: str,
403
+ type: int,
404
+ process_status: int,
405
+ app_key: str | None,
406
+ node_id: int | None,
407
+ search_key: str | None,
408
+ page_num: int,
409
+ page_size: int,
410
+ ) -> dict[str, Any]:
411
+ """执行任务相关逻辑。"""
412
+ self._validate_type(type)
413
+ self._validate_process_status(process_status)
414
+
415
+ def runner(session_profile, context):
416
+ payload: dict[str, Any] = {
417
+ "type": type,
418
+ "processStatus": process_status,
419
+ "pageNum": page_num,
420
+ "pageSize": page_size,
421
+ }
422
+ if app_key is not None:
423
+ payload["appKey"] = app_key
424
+ if node_id is not None:
425
+ payload["nodeId"] = node_id
426
+ if search_key:
427
+ payload["searchKey"] = search_key
428
+
429
+ result = self.backend.request("POST", context, "/task/dynamic/page/group", json_body=payload)
430
+ return {
431
+ "profile": profile,
432
+ "ws_id": session_profile.selected_ws_id,
433
+ "request_route": self._request_route_payload(context),
434
+ "type": type,
435
+ "type_label": get_task_type_label(type),
436
+ "list_type_label": get_task_type_label(type),
437
+ "process_status": process_status,
438
+ "page": result,
439
+ }
440
+
441
+ return self._run(profile, runner)
442
+
443
+ @tool_cn_name("任务统计")
444
+ def task_statistics(
445
+ self,
446
+ *,
447
+ profile: str,
448
+ app_key: str | None,
449
+ ) -> dict[str, Any]:
450
+ """执行任务相关逻辑。"""
451
+ def runner(session_profile, context):
452
+ params: dict[str, Any] = {}
453
+ if app_key:
454
+ params["appKey"] = app_key
455
+
456
+ result = self.backend.request("GET", context, "/task/dynamic/statics", params=params)
457
+ return {
458
+ "profile": profile,
459
+ "ws_id": session_profile.selected_ws_id,
460
+ "request_route": self._request_route_payload(context),
461
+ "statistics": result,
462
+ }
463
+
464
+ return self._run(profile, runner)
465
+
466
+ @tool_cn_name("任务流程节点")
467
+ def task_workflow_nodes(
468
+ self,
469
+ *,
470
+ profile: str,
471
+ type: int,
472
+ status: str | None,
473
+ app_key_list: list[str] | None,
474
+ search_key: str | None,
475
+ page_num: int,
476
+ page_size: int,
477
+ ) -> dict[str, Any]:
478
+ """执行任务相关逻辑。"""
479
+ self._validate_type(type)
480
+
481
+ def runner(session_profile, context):
482
+ params: dict[str, Any] = {
483
+ "type": type,
484
+ "pageNum": page_num,
485
+ "pageSize": page_size,
486
+ }
487
+ if status is not None:
488
+ params["status"] = status
489
+ if app_key_list:
490
+ params["appKeyList"] = app_key_list
491
+ if search_key:
492
+ params["searchKey"] = search_key
493
+
494
+ result = self.backend.request("GET", context, "/task/dynamic/workflow/nodes", params=params)
495
+ return {
496
+ "profile": profile,
497
+ "ws_id": session_profile.selected_ws_id,
498
+ "request_route": self._request_route_payload(context),
499
+ "type": type,
500
+ "type_label": get_task_type_label(type),
501
+ "list_type_label": get_task_type_label(type),
502
+ "page": result,
503
+ }
504
+
505
+ return self._run(profile, runner)
506
+
507
+ @tool_cn_name("任务节点统计")
508
+ def task_node_statistics(
509
+ self,
510
+ *,
511
+ profile: str,
512
+ app_key: str,
513
+ type: int,
514
+ search_key: str | None,
515
+ ) -> dict[str, Any]:
516
+ """执行任务相关逻辑。"""
517
+ self._validate_type(type)
518
+ if not app_key:
519
+ raise_tool_error(QingflowApiError.config_error("app_key is required"))
520
+
521
+ def runner(session_profile, context):
522
+ params: dict[str, Any] = {
523
+ "appKey": app_key,
524
+ "type": type,
525
+ }
526
+ if search_key:
527
+ params["searchKey"] = search_key
528
+
529
+ result = self.backend.request("GET", context, "/task/dynamic/statics/node", params=params)
530
+ return {
531
+ "profile": profile,
532
+ "ws_id": session_profile.selected_ws_id,
533
+ "request_route": self._request_route_payload(context),
534
+ "app_key": app_key,
535
+ "type": type,
536
+ "type_label": get_task_type_label(type),
537
+ "list_type_label": get_task_type_label(type),
538
+ "nodes": result,
539
+ }
540
+
541
+ return self._run(profile, runner)
542
+
543
+ @tool_cn_name("任务应用统计")
544
+ def task_worksheet_statistics(
545
+ self,
546
+ *,
547
+ profile: str,
548
+ type: int,
549
+ worksheet_name: str | None,
550
+ page_num: int,
551
+ page_size: int,
552
+ ) -> dict[str, Any]:
553
+ """执行任务相关逻辑。"""
554
+ self._validate_type(type)
555
+
556
+ def runner(session_profile, context):
557
+ params: dict[str, Any] = {
558
+ "type": type,
559
+ "pageNum": page_num,
560
+ "pageSize": page_size,
561
+ }
562
+ if worksheet_name:
563
+ params["worksheetName"] = worksheet_name
564
+
565
+ result = self.backend.request("GET", context, "/task/dynamic/statics/worksheet", params=params)
566
+ return {
567
+ "profile": profile,
568
+ "ws_id": session_profile.selected_ws_id,
569
+ "request_route": self._request_route_payload(context),
570
+ "type": type,
571
+ "type_label": get_task_type_label(type),
572
+ "list_type_label": get_task_type_label(type),
573
+ "page": result,
574
+ }
575
+
576
+ return self._run(profile, runner)
577
+
578
+ @tool_cn_name("任务已读(兼容)")
579
+ def task_mark_read(
580
+ self,
581
+ *,
582
+ profile: str,
583
+ app_key: str,
584
+ id: int,
585
+ type: int,
586
+ ) -> dict[str, Any]:
587
+ """执行任务相关逻辑。"""
588
+ if not app_key:
589
+ raise_tool_error(QingflowApiError.config_error("app_key is required"))
590
+ if id <= 0:
591
+ raise_tool_error(QingflowApiError.config_error("id must be positive"))
592
+ self._validate_type(type)
593
+
594
+ def runner(session_profile, context):
595
+ result = self.backend.request(
596
+ "POST",
597
+ context,
598
+ f"/task/dynamic/{app_key}/{id}/read/{type}",
599
+ )
600
+ return {
601
+ "profile": profile,
602
+ "ws_id": session_profile.selected_ws_id,
603
+ "request_route": self._request_route_payload(context),
604
+ "app_key": app_key,
605
+ "id": id,
606
+ "type": type,
607
+ "type_label": get_task_type_label(type),
608
+ "list_type_label": get_task_type_label(type),
609
+ "result": result,
610
+ }
611
+
612
+ return self._run(profile, runner)
613
+
614
+ @tool_cn_name("抄送已读(兼容)")
615
+ def task_mark_all_cc_read(
616
+ self,
617
+ *,
618
+ profile: str,
619
+ type: int,
620
+ process_status: int,
621
+ ) -> dict[str, Any]:
622
+ """执行任务相关逻辑。"""
623
+ self._validate_type(type)
624
+ self._validate_process_status(process_status)
625
+
626
+ def runner(session_profile, context):
627
+ payload: dict[str, Any] = {
628
+ "type": type,
629
+ "processStatus": process_status,
630
+ }
631
+ result = self.backend.request("POST", context, "/task/dynamic/cc/readAll", json_body=payload)
632
+ return {
633
+ "profile": profile,
634
+ "ws_id": session_profile.selected_ws_id,
635
+ "request_route": self._request_route_payload(context),
636
+ "type": type,
637
+ "type_label": get_task_type_label(type),
638
+ "list_type_label": get_task_type_label(type),
639
+ "process_status": process_status,
640
+ "result": result,
641
+ }
642
+
643
+ return self._run(profile, runner)
644
+
645
+ @tool_cn_name("任务催办(兼容)")
646
+ def task_urge(
647
+ self,
648
+ *,
649
+ profile: str,
650
+ app_key: str,
651
+ row_record_id: int,
652
+ ) -> dict[str, Any]:
653
+ """执行任务相关逻辑。"""
654
+ if not app_key:
655
+ raise_tool_error(QingflowApiError.config_error("app_key is required"))
656
+ if row_record_id <= 0:
657
+ raise_tool_error(QingflowApiError.config_error("row_record_id must be positive"))
658
+
659
+ def runner(session_profile, context):
660
+ result = self.backend.request(
661
+ "POST",
662
+ context,
663
+ f"/task/dynamic/{app_key}/{row_record_id}/urge",
664
+ )
665
+ return {
666
+ "profile": profile,
667
+ "ws_id": session_profile.selected_ws_id,
668
+ "request_route": self._request_route_payload(context),
669
+ "app_key": app_key,
670
+ "row_record_id": row_record_id,
671
+ "result": result,
672
+ }
673
+
674
+ return self._run(profile, runner)
675
+
676
+ @tool_cn_name("任务分组详情")
677
+ def task_group_detail(
678
+ self,
679
+ *,
680
+ profile: str,
681
+ app_key: str,
682
+ group_id: int,
683
+ ) -> dict[str, Any]:
684
+ """执行任务相关逻辑。"""
685
+ if not app_key:
686
+ raise_tool_error(QingflowApiError.config_error("app_key is required"))
687
+ if group_id <= 0:
688
+ raise_tool_error(QingflowApiError.config_error("group_id must be positive"))
689
+
690
+ def runner(session_profile, context):
691
+ result = self.backend.request(
692
+ "GET",
693
+ context,
694
+ f"/task/dynamic/app/{app_key}/group/{group_id}/detail",
695
+ )
696
+ return {
697
+ "profile": profile,
698
+ "ws_id": session_profile.selected_ws_id,
699
+ "request_route": self._request_route_payload(context),
700
+ "app_key": app_key,
701
+ "group_id": group_id,
702
+ "detail": result,
703
+ }
704
+
705
+ return self._run(profile, runner)
706
+
707
+ @tool_cn_name("批处理任务数量")
708
+ def task_batch_processing_amount(
709
+ self,
710
+ *,
711
+ profile: str,
712
+ app_key: str,
713
+ list_type: int,
714
+ task_center_filter: dict[str, Any] | None,
715
+ ) -> dict[str, Any]:
716
+ """执行任务相关逻辑。"""
717
+ if not app_key:
718
+ raise_tool_error(QingflowApiError.config_error("app_key is required"))
719
+
720
+ def runner(session_profile, context):
721
+ payload: dict[str, Any] = {
722
+ "listType": list_type,
723
+ }
724
+ if task_center_filter is not None:
725
+ payload["taskCenterFilter"] = task_center_filter
726
+
727
+ result = self.backend.request(
728
+ "POST",
729
+ context,
730
+ f"/task/app/{app_key}/batchProcessingAmount",
731
+ json_body=payload,
732
+ )
733
+ return {
734
+ "profile": profile,
735
+ "ws_id": session_profile.selected_ws_id,
736
+ "request_route": self._request_route_payload(context),
737
+ "app_key": app_key,
738
+ "list_type": list_type,
739
+ "list_type_label": get_record_list_type_label(list_type),
740
+ "amount": result,
741
+ }
742
+
743
+ return self._run(profile, runner)
744
+
745
+ def _validate_type(self, type: int) -> None:
746
+ """执行内部辅助逻辑。"""
747
+ valid_types = [1, 2, 3, 5] # TODO, INITIATED, CC, DONE
748
+ if type not in valid_types:
749
+ raise_tool_error(
750
+ QingflowApiError.config_error(
751
+ f"Invalid type: {type}. Must be one of {valid_types} (1=待办, 2=我发起的, 3=抄送, 5=已办)"
752
+ )
753
+ )
754
+
755
+ def _validate_process_status(self, process_status: int) -> None:
756
+ """执行内部辅助逻辑。"""
757
+ valid_statuses = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
758
+ if process_status not in valid_statuses:
759
+ raise_tool_error(
760
+ QingflowApiError.config_error(
761
+ f"Invalid process_status: {process_status}. Must be one of {valid_statuses}"
762
+ )
763
+ )
764
+
765
+ def _task_box_to_type(self, task_box: str) -> int:
766
+ """执行内部辅助逻辑。"""
767
+ normalized = (task_box or "").strip().lower()
768
+ if normalized not in TASK_BOX_TO_TYPE:
769
+ raise_tool_error(QingflowApiError.config_error("task_box must be todo, initiated, cc, or done"))
770
+ return TASK_BOX_TO_TYPE[normalized]
771
+
772
+ def _flow_status_to_process_status(self, flow_status: str) -> int:
773
+ """执行内部辅助逻辑。"""
774
+ normalized = (flow_status or "").strip().lower()
775
+ if normalized not in FLOW_STATUS_TO_PROCESS_STATUS:
776
+ raise_tool_error(
777
+ QingflowApiError.config_error(
778
+ "flow_status must be all, in_progress, approved, rejected, pending_fix, urged, overdue, due_soon, unread, or ended"
779
+ )
780
+ )
781
+ return FLOW_STATUS_TO_PROCESS_STATUS[normalized]
782
+
783
+ def _task_sort_to_create_time_asc(self, sort_by: str | None, sort_direction: str) -> bool | None:
784
+ """执行内部辅助逻辑。"""
785
+ normalized_sort_by = (sort_by or "").strip().lower() if sort_by is not None else ""
786
+ normalized_direction = (sort_direction or "desc").strip().lower()
787
+ if normalized_direction not in {"asc", "desc"}:
788
+ raise_tool_error(QingflowApiError.config_error("sort_direction must be asc or desc"))
789
+ if not normalized_sort_by:
790
+ return None
791
+ if normalized_sort_by not in {"created_at", "create_time"}:
792
+ raise_tool_error(QingflowApiError.config_error("task_list only supports sort_by=created_at"))
793
+ return normalized_direction == "asc"
794
+
795
+ def _task_applied_sort(self, sort_by: str | None, sort_direction: str) -> list[dict[str, Any]]:
796
+ """执行内部辅助逻辑。"""
797
+ if not sort_by:
798
+ return []
799
+ return [{"by": "created_at", "order": (sort_direction or "desc").strip().lower()}]
800
+
801
+ def _normalize_task_summary_payload(self, payload: Any) -> dict[str, Any]:
802
+ """执行内部辅助逻辑。"""
803
+ if not isinstance(payload, dict):
804
+ return {}
805
+ return {
806
+ "todo_count": payload.get("todoCount", payload.get("todo_count")),
807
+ "overdue_count": payload.get("timeoutCount", payload.get("timeout_count")),
808
+ "due_soon_count": payload.get("preTimeoutCount", payload.get("pre_timeout_count")),
809
+ "urged_count": payload.get("urgedCount", payload.get("urged_count")),
810
+ "cc_unread_count": payload.get("ccUnreadCount", payload.get("cc_unread_count")),
811
+ "in_progress_initiated_count": payload.get("processingCount", payload.get("processing_count")),
812
+ }
813
+
814
+ def _normalize_task_facets(self, payload: Any) -> list[dict[str, Any]]:
815
+ """执行内部辅助逻辑。"""
816
+ items = _task_page_items(payload)
817
+ groups: list[dict[str, Any]] = []
818
+ for item in items:
819
+ if not isinstance(item, dict):
820
+ continue
821
+ key = item.get("worksheetId", item.get("groupId", item.get("nodeId", item.get("id", item.get("key")))))
822
+ label = item.get("worksheetName", item.get("groupName", item.get("nodeName", item.get("name", item.get("label", item.get("title"))))))
823
+ count = item.get("count", item.get("taskCount", item.get("amount", item.get("todoCount", item.get("num")))))
824
+ groups.append({"key": key if key is not None else label, "label": label, "count": count})
825
+ return groups
826
+
827
+ def _public_task_action_response(
828
+ self,
829
+ raw: dict[str, Any],
830
+ *,
831
+ action: str,
832
+ resource: dict[str, Any],
833
+ selection: dict[str, Any],
834
+ ) -> dict[str, Any]:
835
+ """执行内部辅助逻辑。"""
836
+ response = dict(raw)
837
+ response["ok"] = bool(raw.get("ok", True))
838
+ response["warnings"] = []
839
+ response["output_profile"] = "normal"
840
+ response["data"] = {
841
+ "action": action,
842
+ "resource": resource,
843
+ "selection": selection,
844
+ "result": raw.get("result"),
845
+ }
846
+ return response
847
+
848
+ def _request_route_payload(self, context) -> dict[str, Any]: # type: ignore[no-untyped-def]
849
+ """执行内部辅助逻辑。"""
850
+ describe_route = getattr(self.backend, "describe_route", None)
851
+ if callable(describe_route):
852
+ payload = describe_route(context)
853
+ if isinstance(payload, dict):
854
+ return payload
855
+ return {
856
+ "base_url": getattr(context, "base_url", None),
857
+ "qf_version": getattr(context, "qf_version", None),
858
+ "qf_version_source": getattr(context, "qf_version_source", None) or ("context" if getattr(context, "qf_version", None) else "unknown"),
859
+ }
860
+
861
+
862
+ def _task_page_items(payload: Any) -> list[Any]:
863
+ if isinstance(payload, list):
864
+ return payload
865
+ if not isinstance(payload, dict):
866
+ return []
867
+ for key in ("list", "items", "rows", "result"):
868
+ value = payload.get(key)
869
+ if isinstance(value, list):
870
+ return value
871
+ for container_key in ("page", "data"):
872
+ nested = payload.get(container_key)
873
+ if isinstance(nested, dict):
874
+ nested_items = _task_page_items(nested)
875
+ if nested_items:
876
+ return nested_items
877
+ return []
878
+
879
+
880
+ def _task_page_amount(payload: Any) -> Any:
881
+ if isinstance(payload, dict):
882
+ return payload.get("pageAmount", payload.get("page_amount"))
883
+ return None
884
+
885
+
886
+ def _task_page_total(payload: Any) -> Any:
887
+ if isinstance(payload, dict):
888
+ return payload.get("total", payload.get("count"))
889
+ return None