@josephyan/qingflow-app-user-mcp 0.2.0-beta.31 → 0.2.0-beta.33

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.31
6
+ npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.33
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.31 qingflow-app-user-mcp
12
+ npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.33 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.31",
3
+ "version": "0.2.0-beta.33",
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.0b31"
7
+ version = "0.2.0b33"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -19,16 +19,20 @@ Route to exactly one of these specialized paths:
19
19
  1. Record CRUD
20
20
  Switch to [$qingflow-record-crud](/Users/yanqidong/Documents/qingflow-next/.codex/skills/qingflow-record-crud/SKILL.md)
21
21
 
22
- 2. Analysis
22
+ 2. Task workflow operations
23
+ Switch to [$qingflow-task-ops](/Users/yanqidong/Documents/qingflow-next/.codex/skills/qingflow-task-ops/SKILL.md)
24
+
25
+ 3. Analysis
23
26
  Switch to [$qingflow-record-analysis](/Users/yanqidong/Documents/qingflow-next/.codex/skills/qingflow-record-analysis/SKILL.md)
24
27
 
25
- 3. MCP connection / auth / workspace selection
28
+ 4. MCP connection / auth / workspace selection
26
29
  Switch to [$qingflow-mcp-setup](/Users/yanqidong/.codex/skills/qingflow-mcp-setup/SKILL.md)
27
30
 
28
31
  ## Routing Rules
29
32
 
30
33
  - If the user does not know the target `app_key`, discover apps first with `app_list` or `app_search`, then route to the specialized skill
31
34
  - If the task is about browsing, reading, creating, updating, deleting, attachments, relations, subtable writes, or member/department-field candidate lookup, switch to `$qingflow-record-crud`
35
+ - If the task is about todo discovery, task context, approval actions, rollback or transfer, associated report review, or workflow log review, switch to `$qingflow-task-ops`
32
36
  - If the task is about grouped distributions, ratios, rankings, trends, insights, or any final statistical conclusion, switch to `$qingflow-record-analysis`
33
37
  - If the MCP is not connected, authenticated, or bound to the right workspace, switch to `$qingflow-mcp-setup`
34
38
 
@@ -37,6 +41,7 @@ Route to exactly one of these specialized paths:
37
41
  - prefer canonical app ids, record ids, task ids, and workflow node ids over guessed names
38
42
  - if a field or target is still ambiguous after schema/task lookup, ask the user to confirm from a short candidate list instead of guessing
39
43
  - if the task can stay read-only, do not write or act
44
+ - if the current MCP capability is unsupported, the workflow is awkward, or the user's need still cannot be satisfied after reasonable use, summarize the gap, ask whether to submit feedback, and call `feedback_submit` only after explicit user confirmation
40
45
 
41
46
  ## Resources
42
47
 
@@ -1,82 +1,62 @@
1
1
  ---
2
2
  name: qingflow-task-ops
3
- description: Use Qingflow task center, workflow usage actions, comments, and directory lookup after the MCP is already connected and authenticated. Do not use this skill for record CRUD or final statistical analysis.
3
+ description: Use Qingflow todo discovery, workflow task context, associated approval context, workflow logs, and unified task actions after the MCP is already connected and authenticated. Do not use this skill for record CRUD or final statistical analysis.
4
4
  metadata:
5
- short-description: Qingflow task center and workflow operations
5
+ short-description: Qingflow task workflow context and actions
6
6
  ---
7
7
 
8
8
  # Qingflow Task Ops
9
9
 
10
10
  ## Overview
11
11
 
12
- This skill is for task-center and workflow usage operations only.
12
+ This skill is for task workflow operations only.
13
13
  Assumes MCP is connected, authenticated, and on the correct workspace.
14
14
 
15
15
  ## Default Paths
16
16
 
17
17
  Use exactly one of these default paths:
18
18
 
19
- 1. Task headline counts
20
- `task_summary`
21
-
22
- 2. Flat task browsing
19
+ 1. Find target todos
23
20
  `task_list`
24
21
 
25
- 3. Grouped workload buckets
26
- `task_facets`
22
+ 2. Read one task context
23
+ `task_list -> exact target -> task_get`
27
24
 
28
- 4. Task or workflow action
29
- `task_list / task_facets -> exact target -> task_* action`
25
+ 3. Read associated approval context
26
+ `task_get -> task_associated_report_detail_get` or `task_workflow_log_get`
30
27
 
31
- 5. Comments and directory support
32
- `record_get -> record_comment_*` or `directory_*`
28
+ 4. Execute workflow action
29
+ `task_list -> exact target -> task_get -> task_action_execute`
33
30
 
34
31
  ## Core Tools
35
32
 
36
- - `task_summary`
37
33
  - `task_list`
38
- - `task_facets`
39
- - `task_mark_read`
40
- - `task_mark_all_cc_read`
41
- - `task_urge`
42
- - `task_approve`
43
- - `task_reject`
44
- - `task_rollback_candidates`
45
- - `task_rollback`
46
- - `task_transfer_candidates`
47
- - `task_transfer`
48
- - `record_comment_write`
49
- - `record_comment_list`
50
- - `record_comment_mentions`
51
- - `record_comment_mark_read`
34
+ - `task_get`
35
+ - `task_action_execute`
36
+ - `task_associated_report_detail_get`
37
+ - `task_workflow_log_get`
52
38
 
53
39
  ## Supporting Tools
54
40
 
55
- - `directory_search`
56
- - `directory_list_internal_users`
57
- - `directory_list_all_internal_users`
58
- - `directory_list_internal_departments`
59
- - `directory_list_all_departments`
60
- - `directory_list_sub_departments`
61
- - `directory_list_external_members`
62
- - `record_get`
41
+ - `app_list`
42
+ - `app_search`
63
43
 
64
44
  ## Standard Operating Order
65
45
 
66
46
  1. Ensure auth exists
67
47
  2. Ensure workspace is selected
68
- 3. Confirm target app and whether the task is task browse / grouped workload / comment / workflow action
69
- 4. Use `task_summary`, `task_list`, or `task_facets` to locate the exact target first
70
- 5. If a workflow action is required, identify the exact `task_id`, `record_id`, and `workflow_node_id` whenever practical
71
- 6. Use directory tools only when member/department lookup is needed to support the action
72
- 7. For production actions, read current task or record state first whenever practical
73
- 8. After actions, report the affected `task_id`, `record_id`, or returned item count
48
+ 3. Discover the exact target with `task_list`
49
+ 4. Read node context with `task_get`
50
+ 5. Before giving any approval recommendation, read `task_workflow_log_get`
51
+ 6. If `task_get` returns any `associated_reports`, read every visible report through `task_associated_report_detail_get`
52
+ 7. Give an approval recommendation only after reviewing the node context, workflow log, and associated report details
53
+ 8. Wait for explicit user confirmation before `task_action_execute`
54
+ 9. Execute through `task_action_execute`
55
+ 10. After actions, report the exact `app_key`, `record_id`, `workflow_node_id`, and executed action
74
56
 
75
57
  ## Task-Center Rules
76
58
 
77
- - Use `task_summary` for headline counts
78
59
  - Use `task_list` for flat browsing
79
- - Use `task_facets` for grouped worksheet or workflow-node buckets
80
60
  - `task_box` must be one of:
81
61
  - `todo`
82
62
  - `initiated`
@@ -93,30 +73,39 @@ Use exactly one of these default paths:
93
73
  - `due_soon`
94
74
  - `unread`
95
75
  - `ended`
96
- - Task counts are task-center counts, not record counts
97
- - If the user asks for workload by worksheet or node, use `task_facets`
98
- - If a result set is truncated, describe it as `已返回分组中` or `主要分组`
76
+ - `task_list` is the only public task discovery path in this MCP surface
77
+ - Treat `task_id` as a locator only; the action primary key is `app_key + record_id + workflow_node_id`
78
+ - Default box usage:
79
+ - `todo`: `task_list -> task_get -> task_workflow_log_get / task_associated_report_detail_get -> recommendation -> explicit user confirmation -> task_action_execute`
80
+ - `initiated`: `task_list -> record_get`
81
+ - `done`: `task_list -> record_get`
82
+ - `cc`: `task_list -> record_get`
83
+ - Treat `initiated`, `done`, and `cc` primarily as list-plus-record-detail flows, not task action flows
99
84
 
100
85
  ## Workflow Usage Actions
101
86
 
102
- - Find the exact target first
103
- - For approve or reject, identify the exact `workflow_node_id` first; prefer task-center results or current audit info
104
- - Avoid workflow actions on ambiguous tasks or records
105
- - For rollback or transfer, fetch candidates first
106
- - Summarize the final action and target task ids or record ids
107
-
108
- ## Comments and Directory
109
-
110
- - Use `record_comment_write` only after the exact `record_id` is known
111
- - Use `record_comment_mentions` to resolve mention candidates before building complex comment payloads
112
- - Use `directory_search` for fuzzy member/department lookup
113
- - Use `directory_list_all_internal_users` and `directory_list_all_departments` only when the user explicitly wants a complete export
87
+ - `task_get.capabilities.available_actions` is the source of truth for v1 executable actions
88
+ - Current public actions are:
89
+ - `approve`
90
+ - `reject`
91
+ - `rollback`
92
+ - `transfer`
93
+ - `urge`
94
+ - Before any approve/reject/rollback/transfer recommendation, always review `task_workflow_log_get` when `task_get.visibility.audit_record_visible=true`
95
+ - If `task_get` returns visible `associated_reports`, review each one with `task_associated_report_detail_get`; do not rely on report summary alone
96
+ - Do not give an approval recommendation based only on `task_get`
97
+ - Do not execute `task_action_execute` until the user explicitly confirms the chosen action
98
+ - Avoid actions on ambiguous tasks or records
99
+ - Summarize the final action and the exact `app_key / record_id / workflow_node_id`
114
100
 
115
101
  ## Response Interpretation
116
102
 
117
- - `task_summary` gives headline counts only
118
- - `task_list` returns flat task rows, not grouped workload conclusions
119
- - `task_facets` is the only default grouped workload path
103
+ - `task_list` returns normalized todo rows and is the only default discovery path
104
+ - `task_get` returns node context summary, not full historical report data
105
+ - `task_associated_report_detail_get` may return either:
106
+ - `result_type=view_list`
107
+ - `result_type=chart_data`
108
+ - `task_workflow_log_get` returns workflow log detail only when the node grants log visibility
120
109
  - Treat `request_route` as the source of truth for live route debugging
121
110
  - If only part of the requested work is completed, explicitly disclose which parts are done and which are not
122
111
 
@@ -2,4 +2,4 @@ from __future__ import annotations
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.2.0b31"
5
+ __version__ = "0.2.0b33"
@@ -12,6 +12,7 @@ DEFAULT_USER_AGENT = "qingflow-mcp/1.0"
12
12
  DEFAULT_RECORD_LIST_TYPE = 8
13
13
  ATTACHMENT_QUESTION_TYPE = 13
14
14
  DEFAULT_BASE_URL = "https://qingflow.com/api"
15
+ DEFAULT_FEEDBACK_APP_KEY = "e0d017kju002"
15
16
 
16
17
 
17
18
  def get_mcp_home() -> Path:
@@ -136,6 +137,43 @@ def get_default_qf_version() -> str | None:
136
137
  return normalized or None
137
138
 
138
139
 
140
+ def get_feedback_qsource_token() -> str | None:
141
+ """获取反馈 q-source 被动入口 token"""
142
+ value = get_config_value(
143
+ "feedback.qsource_token",
144
+ env_var="QINGFLOW_MCP_FEEDBACK_QSOURCE_TOKEN",
145
+ default=None,
146
+ )
147
+ if value is None:
148
+ return None
149
+ normalized = str(value).strip()
150
+ return normalized or None
151
+
152
+
153
+ def get_feedback_base_url() -> str | None:
154
+ """获取反馈 q-source 使用的 base URL"""
155
+ value = get_config_value(
156
+ "feedback.base_url",
157
+ env_var="QINGFLOW_MCP_FEEDBACK_BASE_URL",
158
+ default=None,
159
+ )
160
+ if value is None:
161
+ return get_default_base_url()
162
+ normalized = normalize_base_url(value)
163
+ return normalized or get_default_base_url()
164
+
165
+
166
+ def get_feedback_app_key() -> str:
167
+ """获取内部反馈表 app_key"""
168
+ value = get_config_value(
169
+ "feedback.app_key",
170
+ env_var="QINGFLOW_MCP_FEEDBACK_APP_KEY",
171
+ default=DEFAULT_FEEDBACK_APP_KEY,
172
+ )
173
+ normalized = str(value or "").strip()
174
+ return normalized or DEFAULT_FEEDBACK_APP_KEY
175
+
176
+
139
177
  def get_timeout_seconds() -> float:
140
178
  """获取 HTTP 超时秒数"""
141
179
  value = get_config_value(
@@ -8,6 +8,7 @@ from .backend_client import BackendClient
8
8
  from .session_store import SessionStore
9
9
  from .tools.app_tools import AppTools
10
10
  from .tools.auth_tools import AuthTools
11
+ from .tools.feedback_tools import FeedbackTools
11
12
  from .tools.file_tools import FileTools
12
13
  from .tools.package_tools import PackageTools
13
14
  from .tools.navigation_tools import NavigationTools
@@ -17,6 +18,7 @@ from .tools.qingbi_report_tools import QingbiReportTools
17
18
  from .tools.record_tools import RecordTools
18
19
  from .tools.role_tools import RoleTools
19
20
  from .tools.solution_tools import SolutionTools
21
+ from .tools.task_context_tools import TaskContextTools
20
22
  from .tools.view_tools import ViewTools
21
23
  from .tools.workflow_tools import WorkflowTools
22
24
  from .tools.workspace_tools import WorkspaceTools
@@ -93,6 +95,14 @@ Analysis answers must include concrete numbers. When applicable, include percent
93
95
  - If a member or department field id is known but candidate ids are not, use `record_member_candidates` or `record_department_candidates` before `record_write`.
94
96
  - For default-all member or department fields, prefer those field candidate tools instead of starting with `directory_*`.
95
97
 
98
+ ## Task Workflow Path
99
+
100
+ `task_list -> task_get -> task_action_execute`
101
+
102
+ - Use `task_associated_report_detail_get` for associated view or report details.
103
+ - Use `task_workflow_log_get` for full workflow log history.
104
+ - Task actions operate on `app_key + record_id + workflow_node_id`, not `task_id`.
105
+
96
106
  ## Time Handling
97
107
 
98
108
  Normalize relative dates before building DSL.
@@ -107,14 +117,24 @@ Default to `prod` unless the user explicitly specifies `test`.
107
117
 
108
118
  ## Constraints
109
119
 
110
- Avoid builder-side app or schema changes here.""",
120
+ Avoid builder-side app or schema changes here.
121
+
122
+ ## Feedback Path
123
+
124
+ If the current MCP capability is unsupported, the workflow is awkward, or the user's need still cannot be satisfied after reasonable use, offer to submit product feedback.
125
+
126
+ - First summarize what is still not working
127
+ - Ask the user whether to submit feedback
128
+ - Call `feedback_submit` only after explicit user confirmation""",
111
129
  )
112
130
  sessions = SessionStore()
113
131
  backend = BackendClient()
114
132
  AuthTools(sessions, backend).register(server)
133
+ FeedbackTools(backend, mcp_side="通用").register(server)
115
134
  WorkspaceTools(sessions, backend).register(server)
116
135
  FileTools(sessions, backend).register(server)
117
136
  RecordTools(sessions, backend).register(server)
137
+ TaskContextTools(sessions, backend).register(server)
118
138
  RoleTools(sessions, backend).register(server)
119
139
  AppTools(sessions, backend).register(server)
120
140
  QingbiReportTools(sessions, backend).register(server)
@@ -7,6 +7,7 @@ from .config import DEFAULT_PROFILE
7
7
  from .session_store import SessionStore
8
8
  from .tools.ai_builder_tools import AiBuilderTools
9
9
  from .tools.auth_tools import AuthTools
10
+ from .tools.feedback_tools import FeedbackTools
10
11
  from .tools.file_tools import FileTools
11
12
  from .tools.workspace_tools import WorkspaceTools
12
13
 
@@ -26,7 +27,8 @@ def build_builder_server() -> FastMCP:
26
27
  "Use package_attach_app to attach apps to packages, and app_publish_verify for explicit final publish verification. "
27
28
  "For workflow edits, prefer preset plus explicit patching over generating a full custom graph from scratch, and declare node assignees and editable fields explicitly. "
28
29
  "If builder writes are blocked by the current user's own edit lock, use app_release_edit_lock_if_mine with the lock owner details from the failed result. "
29
- "Do not handcraft internal solution payloads or rely on build_id/stage/repair."
30
+ "Do not handcraft internal solution payloads or rely on build_id/stage/repair. "
31
+ "If the current MCP capability is unsupported, the workflow is awkward, or the user's need still cannot be satisfied after reasonable use, first summarize the gap, ask whether to submit feedback, and call feedback_submit only after explicit user confirmation."
30
32
  ),
31
33
  )
32
34
  sessions = SessionStore()
@@ -35,6 +37,7 @@ def build_builder_server() -> FastMCP:
35
37
  workspace = WorkspaceTools(sessions, backend)
36
38
  files = FileTools(sessions, backend)
37
39
  ai_builder = AiBuilderTools(sessions, backend)
40
+ feedback = FeedbackTools(backend, mcp_side="App Builder MCP")
38
41
 
39
42
  @server.tool()
40
43
  def auth_login(
@@ -120,6 +123,8 @@ def build_builder_server() -> FastMCP:
120
123
  file_related_url=file_related_url,
121
124
  )
122
125
 
126
+ feedback.register(server)
127
+
123
128
  @server.tool()
124
129
  def package_list(profile: str = DEFAULT_PROFILE, trial_status: str = "all") -> dict:
125
130
  return ai_builder.package_list(profile=profile, trial_status=trial_status)
@@ -10,8 +10,10 @@ from .session_store import SessionStore
10
10
  from .tools.app_tools import AppTools
11
11
  from .tools.auth_tools import AuthTools
12
12
  from .tools.directory_tools import DirectoryTools
13
+ from .tools.feedback_tools import FeedbackTools
13
14
  from .tools.file_tools import FileTools
14
15
  from .tools.record_tools import RecordTools
16
+ from .tools.task_context_tools import TaskContextTools
15
17
  from .tools.workspace_tools import WorkspaceTools
16
18
 
17
19
 
@@ -81,6 +83,14 @@ Analysis answers must include concrete numbers. When applicable, include percent
81
83
  - If a member or department field id is known but candidate ids are not, use `record_member_candidates` or `record_department_candidates` before `record_write`.
82
84
  - For default-all member or department fields, prefer those field candidate tools instead of starting with `directory_*`.
83
85
 
86
+ ## Task Workflow Path
87
+
88
+ `task_list -> task_get -> task_action_execute`
89
+
90
+ - Use `task_associated_report_detail_get` for associated view or report details.
91
+ - Use `task_workflow_log_get` for full workflow log history.
92
+ - Task actions operate on `app_key + record_id + workflow_node_id`, not `task_id`.
93
+
84
94
  ## Time Handling
85
95
 
86
96
  Normalize relative dates before building DSL.
@@ -95,7 +105,15 @@ Default to `prod` unless the user explicitly specifies `test`.
95
105
 
96
106
  ## Constraints
97
107
 
98
- Avoid builder-side app or schema changes here.""",
108
+ Avoid builder-side app or schema changes here.
109
+
110
+ ## Feedback Path
111
+
112
+ If the current MCP capability is unsupported, the workflow is awkward, or the user's need still cannot be satisfied after reasonable use, offer to submit product feedback.
113
+
114
+ - First summarize what is still not working
115
+ - Ask the user whether to submit feedback
116
+ - Call `feedback_submit` only after explicit user confirmation""",
99
117
  )
100
118
  sessions = SessionStore()
101
119
  backend = BackendClient()
@@ -103,6 +121,7 @@ Avoid builder-side app or schema changes here.""",
103
121
  apps = AppTools(sessions, backend)
104
122
  workspace = WorkspaceTools(sessions, backend)
105
123
  files = FileTools(sessions, backend)
124
+ feedback = FeedbackTools(backend, mcp_side="App User MCP")
106
125
 
107
126
  @server.tool()
108
127
  def auth_login(
@@ -218,9 +237,11 @@ Avoid builder-side app or schema changes here.""",
218
237
  bucket_type=bucket_type,
219
238
  path_id=path_id,
220
239
  file_related_url=file_related_url,
221
- )
240
+ )
222
241
 
242
+ feedback.register(server)
223
243
  RecordTools(sessions, backend).register(server)
244
+ TaskContextTools(sessions, backend).register(server)
224
245
  DirectoryTools(sessions, backend).register(server)
225
246
 
226
247
  return server
@@ -611,9 +611,11 @@ class ApprovalTools(ToolBase):
611
611
  self._normalize_alias(body, "formId", "form_id")
612
612
 
613
613
  node_id = self._extract_node_id(body)
614
- body["nodeId"] = node_id
614
+ body["nodeId"] = self._resolve_actionable_node_id(context, app_key, apply_id, node_id)
615
615
  body["applyId"] = self._match_or_fill_int(body, field_name="applyId", expected_value=apply_id)
616
616
  body["formId"] = self._resolve_form_id(profile, context, app_key, explicit_form_id=body.get("formId"))
617
+ if body.get("answers") is None:
618
+ body["answers"] = self._fetch_current_todo_answers(context, app_key, apply_id, body["nodeId"])
617
619
 
618
620
  self._validate_approval_payload(body)
619
621
  return body
@@ -672,6 +674,54 @@ class ApprovalTools(ToolBase):
672
674
  elif alias_value is not None and payload.get(canonical_key) != alias_value:
673
675
  raise_tool_error(QingflowApiError.config_error(f"payload.{canonical_key} and payload.{alias_key} must match when both are provided"))
674
676
 
677
+ def _resolve_actionable_node_id(self, context, app_key: str, apply_id: int, node_id: int) -> int: # type: ignore[no-untyped-def]
678
+ infos = self.backend.request(
679
+ "GET",
680
+ context,
681
+ f"/app/{app_key}/apply/{apply_id}/auditInfo",
682
+ params={"type": 1},
683
+ )
684
+ if not isinstance(infos, list) or not infos:
685
+ raise_tool_error(
686
+ QingflowApiError.config_error(
687
+ f"apply_id={apply_id} is not currently actionable for the logged-in user in todo list"
688
+ )
689
+ )
690
+ actionable_node_ids = {
691
+ candidate
692
+ for item in infos
693
+ if isinstance(item, dict)
694
+ for candidate in (item.get("auditNodeId"), item.get("nodeId"))
695
+ if isinstance(candidate, int) and candidate > 0
696
+ }
697
+ if node_id not in actionable_node_ids:
698
+ raise_tool_error(
699
+ QingflowApiError.config_error(
700
+ f"payload.nodeId={node_id} is not an actionable todo node for apply_id={apply_id}"
701
+ )
702
+ )
703
+ return node_id
704
+
705
+ def _fetch_current_todo_answers(self, context, app_key: str, apply_id: int, node_id: int) -> list[dict[str, Any]]: # type: ignore[no-untyped-def]
706
+ detail = self.backend.request(
707
+ "GET",
708
+ context,
709
+ f"/app/{app_key}/apply/{apply_id}",
710
+ params={"role": 3, "listType": 1, "auditNodeId": node_id},
711
+ )
712
+ answers = detail.get("answers") if isinstance(detail, dict) else None
713
+ if not isinstance(answers, list):
714
+ raise_tool_error(
715
+ QingflowApiError.config_error(
716
+ f"cannot resolve current answers for apply_id={apply_id} nodeId={node_id}"
717
+ )
718
+ )
719
+ normalized_answers: list[dict[str, Any]] = []
720
+ for item in answers:
721
+ if isinstance(item, dict):
722
+ normalized_answers.append(dict(item))
723
+ return normalized_answers
724
+
675
725
  def _validate_approval_payload(self, payload: dict[str, Any]) -> None:
676
726
  self._reject_unsupported_fields(payload)
677
727
  if not isinstance(payload.get("formId"), int) or payload["formId"] <= 0:
@@ -680,6 +730,9 @@ class ApprovalTools(ToolBase):
680
730
  raise_tool_error(QingflowApiError.config_error("payload.applyId must be a positive integer"))
681
731
  if not isinstance(payload.get("nodeId"), int) or payload["nodeId"] <= 0:
682
732
  raise_tool_error(QingflowApiError.config_error("payload.nodeId must be a positive integer"))
733
+ answers = payload.get("answers")
734
+ if answers is not None and not isinstance(answers, list):
735
+ raise_tool_error(QingflowApiError.config_error("payload.answers must be an array when provided"))
683
736
 
684
737
  def _validate_audit_payload(self, payload: dict[str, Any], *, require_uid: bool = False) -> None:
685
738
  self._reject_unsupported_fields(payload)