@josephyan/qingflow-cli 1.1.4 → 1.1.5
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 +7 -3
- package/docs/local-agent-install.md +57 -6
- package/entry_point.py +1 -1
- package/npm/bin/qingflow-skills.mjs +5 -0
- package/npm/bin/qingflow.mjs +1 -34
- package/npm/lib/runtime.mjs +21 -101
- package/npm/scripts/postinstall.mjs +1 -10
- package/package.json +3 -2
- package/pyproject.toml +1 -1
- package/skills/qingflow-cli/SKILL.md +58 -44
- package/skills/qingflow-cli/manifest.yaml +1 -1
- package/skills/qingflow-cli/reference/00-INDEX.md +35 -0
- package/skills/qingflow-cli/reference/builder/10-build-single-app.md +38 -0
- package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +39 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md → builder/30-schema-fields.md} +52 -10
- package/skills/qingflow-cli/reference/builder/40-layout.md +52 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md → builder/50-views.md} +39 -15
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_CHARTS_WORKFLOW.md → builder/60-charts.md} +36 -13
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_PORTAL_WORKFLOW.md → builder/70-portal.md} +36 -13
- package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +41 -0
- package/skills/qingflow-cli/reference/builder/90-workflow.md +34 -0
- package/skills/qingflow-cli/reference/builder/99-publish-verify.md +46 -0
- package/skills/qingflow-cli/reference/builder/README.md +41 -0
- package/skills/qingflow-cli/reference/builder/code-integrations/README.md +130 -0
- package/skills/qingflow-cli/reference/builder/code-integrations/code-block.md +66 -0
- package/skills/qingflow-cli/reference/builder/code-integrations/q-linker.md +77 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md → builder/reference/app-delivery-sop.md} +26 -16
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/README.md +293 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-complete-system.md +809 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-single-app.md +830 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/complete-system-development-guide.md +123 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/create-app.md +182 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/environments.md +63 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/flow-actors-and-permissions.md +142 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/gotchas.md +108 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/match-rules.md +114 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/public-surface-sync.md +75 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/single-app-development-guide.md +58 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/solution-playbooks.md +52 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/tool-selection.md +107 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-flow.md +7 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-layout.md +7 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-schema.md +7 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-views.md +7 -0
- package/skills/qingflow-cli/reference/builder/workflow/01-overview.md +45 -0
- package/skills/qingflow-cli/reference/builder/workflow/02-update-mode.md +53 -0
- package/skills/qingflow-cli/reference/builder/workflow/03-flow-patterns.md +57 -0
- package/skills/qingflow-cli/reference/builder/workflow/04-stage1-business-modeling.md +131 -0
- package/skills/qingflow-cli/reference/builder/workflow/05-stage2-members-roles.md +29 -0
- package/skills/qingflow-cli/reference/builder/workflow/06-stage3-build-spec.md +165 -0
- package/skills/qingflow-cli/reference/builder/workflow/07-stage4-validate-spec.md +33 -0
- package/skills/qingflow-cli/reference/builder/workflow/08-stage5-apply-verify.md +51 -0
- package/skills/qingflow-cli/reference/builder/workflow/09-stage6-summary.md +88 -0
- package/skills/qingflow-cli/reference/builder/workflow/10-node-config-reference.md +93 -0
- package/skills/qingflow-cli/reference/builder/workflow/11-troubleshooting.md +15 -0
- package/skills/qingflow-cli/reference/builder/workflow/README.md +88 -0
- package/skills/qingflow-cli/reference/builder/workflow/workflow-schema.json +1754 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ADMIN_CHEATSHEET.md → core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md} +3 -3
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md → core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md} +6 -6
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_EXPLORATION_REPORT.md → core/QINGFLOW_CLI_EXPLORATION_REPORT.md} +2 -2
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_FIELD_DATA_TYPES.md → core/QINGFLOW_CLI_FIELD_DATA_TYPES.md} +11 -11
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_MEMBER_CHEATSHEET.md → core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md} +4 -4
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md → core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md} +4 -4
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md} +3 -3
- package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_DELETE_WORKFLOW.md +31 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md} +4 -4
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md} +7 -7
- package/skills/qingflow-cli/reference/record/analysis/README.md +130 -0
- package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +91 -0
- package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +112 -0
- package/skills/qingflow-cli/reference/record/analysis/business-context.md +74 -0
- package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +69 -0
- package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +106 -0
- package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +172 -0
- package/skills/qingflow-cli/reference/record/analysis/report-format.md +76 -0
- package/skills/qingflow-cli/reference/record/insert/README.md +75 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md → task/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md} +5 -5
- package/skills/qingflow-cli/reference/task/ops/README.md +131 -0
- package/skills/qingflow-cli/reference/task/ops/environments.md +43 -0
- package/skills/qingflow-cli/reference/task/ops/workflow-usage.md +26 -0
- package/skills/qingflow-cli/scripts/validate_system_build_summary.py +124 -0
- package/skills/qingflow-cli/scripts/workflow/diff_flow_spec.py +275 -0
- package/skills/qingflow-cli/scripts/workflow/validate_flow_spec.py +605 -0
- package/skills/qingflow-mcp-setup/SKILL.md +115 -0
- package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
- package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
- package/skills/qingflow-mcp-setup/references/environments.md +62 -0
- package/skills/qingflow-mcp-setup/references/generic-stdio.md +32 -0
- package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/__main__.py +6 -2
- package/src/qingflow_mcp/builder_facade/models.py +282 -102
- package/src/qingflow_mcp/builder_facade/service.py +4166 -929
- package/src/qingflow_mcp/cli/commands/builder.py +316 -298
- package/src/qingflow_mcp/cli/commands/chart.py +1 -1
- package/src/qingflow_mcp/cli/commands/common.py +12 -3
- package/src/qingflow_mcp/cli/commands/exports.py +2 -2
- package/src/qingflow_mcp/cli/commands/imports.py +3 -3
- package/src/qingflow_mcp/cli/commands/portal.py +2 -2
- package/src/qingflow_mcp/cli/commands/record.py +101 -27
- package/src/qingflow_mcp/cli/commands/task.py +28 -47
- package/src/qingflow_mcp/cli/commands/view.py +1 -1
- package/src/qingflow_mcp/cli/context.py +0 -3
- package/src/qingflow_mcp/cli/formatters.py +784 -16
- package/src/qingflow_mcp/cli/main.py +117 -33
- package/src/qingflow_mcp/errors.py +43 -2
- package/src/qingflow_mcp/public_surface.py +26 -17
- package/src/qingflow_mcp/response_trim.py +81 -17
- package/src/qingflow_mcp/server.py +14 -12
- package/src/qingflow_mcp/server_app_builder.py +65 -21
- package/src/qingflow_mcp/server_app_user.py +22 -16
- package/src/qingflow_mcp/session_store.py +11 -7
- package/src/qingflow_mcp/solution/compiler/__init__.py +3 -1
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
- package/src/qingflow_mcp/solution/executor.py +245 -18
- package/src/qingflow_mcp/tools/ai_builder_tools.py +1780 -406
- package/src/qingflow_mcp/tools/app_tools.py +184 -43
- package/src/qingflow_mcp/tools/approval_tools.py +197 -35
- package/src/qingflow_mcp/tools/auth_tools.py +92 -16
- package/src/qingflow_mcp/tools/code_block_tools.py +298 -40
- package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
- package/src/qingflow_mcp/tools/directory_tools.py +236 -72
- package/src/qingflow_mcp/tools/export_tools.py +244 -34
- package/src/qingflow_mcp/tools/feedback_tools.py +9 -0
- package/src/qingflow_mcp/tools/file_tools.py +9 -3
- package/src/qingflow_mcp/tools/import_tools.py +336 -49
- package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
- package/src/qingflow_mcp/tools/package_tools.py +118 -6
- package/src/qingflow_mcp/tools/portal_tools.py +39 -3
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
- package/src/qingflow_mcp/tools/record_tools.py +1141 -356
- package/src/qingflow_mcp/tools/resource_read_tools.py +188 -39
- package/src/qingflow_mcp/tools/role_tools.py +80 -9
- package/src/qingflow_mcp/tools/solution_tools.py +59 -45
- package/src/qingflow_mcp/tools/task_context_tools.py +662 -158
- package/src/qingflow_mcp/tools/task_tools.py +113 -29
- package/src/qingflow_mcp/tools/view_tools.py +106 -3
- package/src/qingflow_mcp/tools/workflow_tools.py +48 -4
- package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
- /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_MATCH_RULES.md → builder/reference/match-rules.md} +0 -0
- /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_WORKSPACE_ICONS.md → builder/reference/workspace-icons.md} +0 -0
- /package/skills/qingflow-cli/reference/{charts_remove.example.json → examples/charts/charts_remove.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{charts_reorder.example.json → examples/charts/charts_reorder.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{charts_upsert_bar.example.json → examples/charts/charts_upsert_bar.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{charts_upsert_dashboard_starter.example.json → examples/charts/charts_upsert_dashboard_starter.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{charts_upsert_minimal.example.json → examples/charts/charts_upsert_minimal.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{portal_sections_all_types.example.json → examples/portal/portal_sections_all_types.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{portal_sections_five_types.example.json → examples/portal/portal_sections_five_types.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{portal_sections_standard_workbench.example.json → examples/portal/portal_sections_standard_workbench.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{_batch_schema_complex.json → examples/schema/_batch_schema_complex.json} +0 -0
- /package/skills/qingflow-cli/reference/{_batch_schema_scalar.json → examples/schema/_batch_schema_scalar.json} +0 -0
- /package/skills/qingflow-cli/reference/{schema_add_fields_minimal.example.json → examples/schema/schema_add_fields_minimal.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{schema_apply_add_fields_all_types.json → examples/schema/schema_apply_add_fields_all_types.json} +0 -0
- /package/skills/qingflow-cli/reference/{views_upsert_table_minimal.example.json → examples/views/views_upsert_table_minimal.example.json} +0 -0
|
@@ -9,12 +9,12 @@ from mcp.server.fastmcp import FastMCP
|
|
|
9
9
|
|
|
10
10
|
from ..backend_client import BackendRequestContext
|
|
11
11
|
from ..config import DEFAULT_PROFILE
|
|
12
|
-
from ..errors import QingflowApiError, raise_tool_error
|
|
12
|
+
from ..errors import QingflowApiError, backend_code_int, backend_code_value_int, is_auth_like_error, message_looks_like_invalid_token, raise_tool_error
|
|
13
13
|
from ..id_utils import ids_equal, normalize_positive_id_int, normalize_positive_id_text, stringify_backend_id
|
|
14
14
|
from ..json_types import JSONObject
|
|
15
15
|
from .approval_tools import ApprovalTools, _approval_page_amount, _approval_page_items, _approval_page_total
|
|
16
16
|
from .base import ToolBase, tool_cn_name
|
|
17
|
-
from .qingbi_report_tools import _qingbi_base_url
|
|
17
|
+
from .qingbi_report_tools import _qingbi_base_url, _should_retry_asos_data
|
|
18
18
|
from .record_tools import (
|
|
19
19
|
FieldIndex,
|
|
20
20
|
LAYOUT_ONLY_QUE_TYPES,
|
|
@@ -38,6 +38,9 @@ from .record_tools import (
|
|
|
38
38
|
from .task_tools import TaskTools, _task_page_amount, _task_page_items, _task_page_total
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
TASK_LOCATOR_MAX_SCAN_PAGES_PER_BOX = 3
|
|
42
|
+
|
|
43
|
+
|
|
41
44
|
class TaskContextTools(ToolBase):
|
|
42
45
|
"""任务上下文工具(中文名:任务上下文与审批执行)。
|
|
43
46
|
|
|
@@ -85,33 +88,37 @@ class TaskContextTools(ToolBase):
|
|
|
85
88
|
page_size=page_size,
|
|
86
89
|
)
|
|
87
90
|
|
|
88
|
-
@mcp.tool(
|
|
91
|
+
@mcp.tool(
|
|
92
|
+
description=(
|
|
93
|
+
"Read one workflow task. Prefer task_id from task_list.data.items[].task_id; "
|
|
94
|
+
"task_id is not a row number, list index, record id, or workflow node id."
|
|
95
|
+
)
|
|
96
|
+
)
|
|
89
97
|
def task_get(
|
|
90
98
|
profile: str = DEFAULT_PROFILE,
|
|
91
99
|
task_id: str = "",
|
|
92
|
-
app_key: str = "",
|
|
93
|
-
record_id: str = "",
|
|
94
|
-
workflow_node_id: int = 0,
|
|
95
100
|
include_candidates: bool = True,
|
|
96
101
|
include_associated_reports: bool = True,
|
|
97
102
|
) -> dict[str, Any]:
|
|
98
103
|
return self.task_get(
|
|
99
104
|
profile=profile,
|
|
100
105
|
task_id=task_id,
|
|
101
|
-
app_key=
|
|
102
|
-
record_id=
|
|
103
|
-
workflow_node_id=
|
|
106
|
+
app_key="",
|
|
107
|
+
record_id="",
|
|
108
|
+
workflow_node_id=0,
|
|
104
109
|
include_candidates=include_candidates,
|
|
105
110
|
include_associated_reports=include_associated_reports,
|
|
106
111
|
)
|
|
107
112
|
|
|
108
|
-
@mcp.tool(
|
|
113
|
+
@mcp.tool(
|
|
114
|
+
description=(
|
|
115
|
+
self._high_risk_tool_description(operation="execute", target="workflow task action")
|
|
116
|
+
+ " Pass task_id from task_list.data.items[].task_id. Do not pass a row number, list index, record id, or workflow node id as task_id."
|
|
117
|
+
)
|
|
118
|
+
)
|
|
109
119
|
def task_action_execute(
|
|
110
120
|
profile: str = DEFAULT_PROFILE,
|
|
111
121
|
task_id: str = "",
|
|
112
|
-
app_key: str = "",
|
|
113
|
-
record_id: str = "",
|
|
114
|
-
workflow_node_id: int = 0,
|
|
115
122
|
action: str = "",
|
|
116
123
|
payload: dict[str, Any] | None = None,
|
|
117
124
|
fields: dict[str, Any] | None = None,
|
|
@@ -119,21 +126,20 @@ class TaskContextTools(ToolBase):
|
|
|
119
126
|
return self.task_action_execute(
|
|
120
127
|
profile=profile,
|
|
121
128
|
task_id=task_id,
|
|
122
|
-
app_key=app_key,
|
|
123
|
-
record_id=record_id,
|
|
124
|
-
workflow_node_id=workflow_node_id,
|
|
125
129
|
action=action,
|
|
126
130
|
payload=payload or {},
|
|
127
131
|
fields=fields or {},
|
|
128
132
|
)
|
|
129
133
|
|
|
130
|
-
@mcp.tool(
|
|
134
|
+
@mcp.tool(
|
|
135
|
+
description=(
|
|
136
|
+
"Read a task-associated report. Pass task_id from task_list.data.items[].task_id; "
|
|
137
|
+
"task_id is not a row number, list index, record id, or workflow node id."
|
|
138
|
+
)
|
|
139
|
+
)
|
|
131
140
|
def task_associated_report_detail_get(
|
|
132
141
|
profile: str = DEFAULT_PROFILE,
|
|
133
142
|
task_id: str = "",
|
|
134
|
-
app_key: str = "",
|
|
135
|
-
record_id: str = "",
|
|
136
|
-
workflow_node_id: int = 0,
|
|
137
143
|
report_id: int = 0,
|
|
138
144
|
page: int = 1,
|
|
139
145
|
page_size: int = 20,
|
|
@@ -141,28 +147,30 @@ class TaskContextTools(ToolBase):
|
|
|
141
147
|
return self.task_associated_report_detail_get(
|
|
142
148
|
profile=profile,
|
|
143
149
|
task_id=task_id,
|
|
144
|
-
app_key=
|
|
145
|
-
record_id=
|
|
146
|
-
workflow_node_id=
|
|
150
|
+
app_key="",
|
|
151
|
+
record_id="",
|
|
152
|
+
workflow_node_id=0,
|
|
147
153
|
report_id=report_id,
|
|
148
154
|
page=page,
|
|
149
155
|
page_size=page_size,
|
|
150
156
|
)
|
|
151
157
|
|
|
152
|
-
@mcp.tool(
|
|
158
|
+
@mcp.tool(
|
|
159
|
+
description=(
|
|
160
|
+
"Read workflow log for one task context. Pass task_id from task_list.data.items[].task_id; "
|
|
161
|
+
"task_id is not a row number, list index, record id, or workflow node id."
|
|
162
|
+
)
|
|
163
|
+
)
|
|
153
164
|
def task_workflow_log_get(
|
|
154
165
|
profile: str = DEFAULT_PROFILE,
|
|
155
166
|
task_id: str = "",
|
|
156
|
-
app_key: str = "",
|
|
157
|
-
record_id: str = "",
|
|
158
|
-
workflow_node_id: int = 0,
|
|
159
167
|
) -> dict[str, Any]:
|
|
160
168
|
return self.task_workflow_log_get(
|
|
161
169
|
profile=profile,
|
|
162
170
|
task_id=task_id,
|
|
163
|
-
app_key=
|
|
164
|
-
record_id=
|
|
165
|
-
workflow_node_id=
|
|
171
|
+
app_key="",
|
|
172
|
+
record_id="",
|
|
173
|
+
workflow_node_id=0,
|
|
166
174
|
)
|
|
167
175
|
|
|
168
176
|
@tool_cn_name("任务上下文列表")
|
|
@@ -271,6 +279,7 @@ class TaskContextTools(ToolBase):
|
|
|
271
279
|
resolved_app_key = str(locator["app_key"])
|
|
272
280
|
resolved_record_id = int(locator["record_id"])
|
|
273
281
|
resolved_workflow_node_id = int(locator["workflow_node_id"])
|
|
282
|
+
resolved_task_box = str(locator.get("task_box") or "todo")
|
|
274
283
|
self._require_app_record_and_node(resolved_app_key, resolved_record_id, resolved_workflow_node_id)
|
|
275
284
|
data = self._build_task_context(
|
|
276
285
|
profile=profile,
|
|
@@ -278,10 +287,12 @@ class TaskContextTools(ToolBase):
|
|
|
278
287
|
app_key=resolved_app_key,
|
|
279
288
|
record_id=resolved_record_id,
|
|
280
289
|
workflow_node_id=resolved_workflow_node_id,
|
|
290
|
+
task_box=resolved_task_box,
|
|
281
291
|
include_candidates=include_candidates,
|
|
282
292
|
include_associated_reports=include_associated_reports,
|
|
283
293
|
current_uid=session_profile.uid,
|
|
284
294
|
)
|
|
295
|
+
context_warnings = data.get("warnings") if isinstance(data.get("warnings"), list) else []
|
|
285
296
|
data = self._compact_task_get_context(data)
|
|
286
297
|
task_payload = data.get("task")
|
|
287
298
|
if isinstance(task_payload, dict) and task_id_text is not None:
|
|
@@ -291,7 +302,7 @@ class TaskContextTools(ToolBase):
|
|
|
291
302
|
"ws_id": session_profile.selected_ws_id,
|
|
292
303
|
"ok": True,
|
|
293
304
|
"request_route": self._request_route_payload(context),
|
|
294
|
-
"warnings":
|
|
305
|
+
"warnings": context_warnings,
|
|
295
306
|
"output_profile": "normal",
|
|
296
307
|
"data": data,
|
|
297
308
|
}
|
|
@@ -303,9 +314,7 @@ class TaskContextTools(ToolBase):
|
|
|
303
314
|
self,
|
|
304
315
|
*,
|
|
305
316
|
profile: str,
|
|
306
|
-
|
|
307
|
-
record_id: Any,
|
|
308
|
-
workflow_node_id: int,
|
|
317
|
+
task_id: Any,
|
|
309
318
|
fields: dict[str, Any] | None = None,
|
|
310
319
|
) -> dict[str, Any]:
|
|
311
320
|
"""执行任务相关逻辑。"""
|
|
@@ -314,9 +323,7 @@ class TaskContextTools(ToolBase):
|
|
|
314
323
|
raise_tool_error(QingflowApiError.config_error("fields is required and must be non-empty for task_save_only"))
|
|
315
324
|
return self.task_action_execute(
|
|
316
325
|
profile=profile,
|
|
317
|
-
|
|
318
|
-
record_id=record_id,
|
|
319
|
-
workflow_node_id=workflow_node_id,
|
|
326
|
+
task_id=task_id,
|
|
320
327
|
action="save_only",
|
|
321
328
|
payload={},
|
|
322
329
|
fields=field_updates,
|
|
@@ -324,6 +331,34 @@ class TaskContextTools(ToolBase):
|
|
|
324
331
|
|
|
325
332
|
@tool_cn_name("执行任务动作")
|
|
326
333
|
def task_action_execute(
|
|
334
|
+
self,
|
|
335
|
+
*,
|
|
336
|
+
profile: str,
|
|
337
|
+
task_id: Any,
|
|
338
|
+
action: str,
|
|
339
|
+
payload: dict[str, Any],
|
|
340
|
+
fields: dict[str, Any] | None = None,
|
|
341
|
+
) -> dict[str, Any]:
|
|
342
|
+
"""执行任务相关逻辑。"""
|
|
343
|
+
if task_id in (None, ""):
|
|
344
|
+
raise_tool_error(
|
|
345
|
+
QingflowApiError.config_error(
|
|
346
|
+
"task_id is required for task_action_execute; get it from task_list.data.items[].task_id"
|
|
347
|
+
)
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
return self._task_action_execute_with_locator(
|
|
351
|
+
profile=profile,
|
|
352
|
+
task_id=task_id,
|
|
353
|
+
app_key="",
|
|
354
|
+
record_id="",
|
|
355
|
+
workflow_node_id=0,
|
|
356
|
+
action=action,
|
|
357
|
+
payload=payload,
|
|
358
|
+
fields=fields,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
def _task_action_execute_with_locator(
|
|
327
362
|
self,
|
|
328
363
|
*,
|
|
329
364
|
profile: str,
|
|
@@ -335,10 +370,8 @@ class TaskContextTools(ToolBase):
|
|
|
335
370
|
payload: dict[str, Any],
|
|
336
371
|
fields: dict[str, Any] | None = None,
|
|
337
372
|
) -> dict[str, Any]:
|
|
338
|
-
"""执行任务相关逻辑。"""
|
|
339
373
|
if task_id in (None, ""):
|
|
340
374
|
normalize_positive_id_int(record_id, field_name="record_id")
|
|
341
|
-
|
|
342
375
|
normalized_action = (action or "").strip().lower()
|
|
343
376
|
if normalized_action not in {"approve", "reject", "rollback", "transfer", "urge", "save_only"}:
|
|
344
377
|
raise_tool_error(
|
|
@@ -368,8 +401,63 @@ class TaskContextTools(ToolBase):
|
|
|
368
401
|
resolved_record_id = int(locator["record_id"])
|
|
369
402
|
resolved_record_id_text = str(locator["record_id_text"] or "")
|
|
370
403
|
resolved_workflow_node_id = int(locator["workflow_node_id"])
|
|
404
|
+
resolved_task_box = str(locator.get("task_box") or "todo")
|
|
371
405
|
record_id_text = resolved_record_id_text
|
|
372
406
|
self._require_app_record_and_node(resolved_app_key, resolved_record_id, resolved_workflow_node_id)
|
|
407
|
+
if normalized_action == "urge":
|
|
408
|
+
raw = self._execute_task_action(
|
|
409
|
+
profile=profile,
|
|
410
|
+
app_key=resolved_app_key,
|
|
411
|
+
record_id=resolved_record_id,
|
|
412
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
413
|
+
normalized_action=normalized_action,
|
|
414
|
+
payload=body,
|
|
415
|
+
prepared_fields=None,
|
|
416
|
+
)
|
|
417
|
+
verification, verification_warnings = self._verify_task_action_runtime(
|
|
418
|
+
profile=profile,
|
|
419
|
+
context=context,
|
|
420
|
+
app_key=resolved_app_key,
|
|
421
|
+
record_id=resolved_record_id,
|
|
422
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
423
|
+
action=normalized_action,
|
|
424
|
+
before_apply_status=None,
|
|
425
|
+
runtime_baseline=None,
|
|
426
|
+
)
|
|
427
|
+
result = {
|
|
428
|
+
"profile": raw.get("profile", profile),
|
|
429
|
+
"ws_id": raw.get("ws_id", session_profile.selected_ws_id),
|
|
430
|
+
"ok": bool(raw.get("ok", True)),
|
|
431
|
+
"status": "success",
|
|
432
|
+
"error_code": None,
|
|
433
|
+
"action_executed": True,
|
|
434
|
+
"safe_to_retry": False,
|
|
435
|
+
"request_route": raw.get("request_route") or self._request_route_payload(context),
|
|
436
|
+
"warnings": verification_warnings,
|
|
437
|
+
"verification": verification,
|
|
438
|
+
"output_profile": "normal",
|
|
439
|
+
"data": {
|
|
440
|
+
"action": normalized_action,
|
|
441
|
+
"resource": {
|
|
442
|
+
"app_key": resolved_app_key,
|
|
443
|
+
"record_id": record_id_text,
|
|
444
|
+
"workflow_node_id": resolved_workflow_node_id,
|
|
445
|
+
},
|
|
446
|
+
"selection": {"action": normalized_action},
|
|
447
|
+
"result": raw.get("result"),
|
|
448
|
+
"human_review": True,
|
|
449
|
+
"field_update_applied": False,
|
|
450
|
+
},
|
|
451
|
+
}
|
|
452
|
+
if task_id_text is not None:
|
|
453
|
+
result["data"]["resource"]["task_id"] = task_id_text
|
|
454
|
+
return result
|
|
455
|
+
if resolved_task_box != "todo":
|
|
456
|
+
raise_tool_error(
|
|
457
|
+
QingflowApiError.config_error(
|
|
458
|
+
f"task_id={task_id_text or ''} resolved to task_box='{resolved_task_box}', but task_action_execute can only execute current todo tasks"
|
|
459
|
+
)
|
|
460
|
+
)
|
|
373
461
|
try:
|
|
374
462
|
task_context = self._build_task_context(
|
|
375
463
|
profile=profile,
|
|
@@ -377,12 +465,13 @@ class TaskContextTools(ToolBase):
|
|
|
377
465
|
app_key=resolved_app_key,
|
|
378
466
|
record_id=resolved_record_id,
|
|
379
467
|
workflow_node_id=resolved_workflow_node_id,
|
|
468
|
+
task_box=resolved_task_box,
|
|
380
469
|
include_candidates=False,
|
|
381
470
|
include_associated_reports=False,
|
|
382
471
|
current_uid=session_profile.uid,
|
|
383
472
|
)
|
|
384
473
|
except QingflowApiError as error:
|
|
385
|
-
if error
|
|
474
|
+
if backend_code_int(error) == 46001:
|
|
386
475
|
return self._task_action_visibility_unverified_response(
|
|
387
476
|
profile=profile,
|
|
388
477
|
session_profile=session_profile,
|
|
@@ -422,7 +511,7 @@ class TaskContextTools(ToolBase):
|
|
|
422
511
|
raise_tool_error(QingflowApiError.config_error(message))
|
|
423
512
|
raise_tool_error(
|
|
424
513
|
QingflowApiError.config_error(
|
|
425
|
-
f"task action '{normalized_action}' is not currently available for
|
|
514
|
+
f"task action '{normalized_action}' is not currently available for task_id='{task_id_text}'"
|
|
426
515
|
)
|
|
427
516
|
)
|
|
428
517
|
feedback_required_for = capabilities.get("action_constraints", {}).get("feedback_required_for") or []
|
|
@@ -470,7 +559,7 @@ class TaskContextTools(ToolBase):
|
|
|
470
559
|
prepared_fields=prepared_fields,
|
|
471
560
|
)
|
|
472
561
|
except QingflowApiError as error:
|
|
473
|
-
if error
|
|
562
|
+
if backend_code_int(error) == 46001:
|
|
474
563
|
return self._task_action_visibility_unverified_response(
|
|
475
564
|
profile=profile,
|
|
476
565
|
session_profile=session_profile,
|
|
@@ -518,6 +607,8 @@ class TaskContextTools(ToolBase):
|
|
|
518
607
|
"ok": bool(raw.get("ok", True)) and status != "failed",
|
|
519
608
|
"status": status,
|
|
520
609
|
"error_code": error_code,
|
|
610
|
+
"action_executed": True,
|
|
611
|
+
"safe_to_retry": False,
|
|
521
612
|
"request_route": raw.get("request_route") or self._request_route_payload(context),
|
|
522
613
|
"warnings": warnings,
|
|
523
614
|
"verification": verification,
|
|
@@ -680,6 +771,8 @@ class TaskContextTools(ToolBase):
|
|
|
680
771
|
"http_status": error.http_status,
|
|
681
772
|
"backend_code": error.backend_code,
|
|
682
773
|
"category": error.category,
|
|
774
|
+
"request_id": error.request_id,
|
|
775
|
+
"message": error.message,
|
|
683
776
|
}
|
|
684
777
|
|
|
685
778
|
log_items: list[dict[str, Any]] = []
|
|
@@ -707,6 +800,8 @@ class TaskContextTools(ToolBase):
|
|
|
707
800
|
"http_status": error.http_status,
|
|
708
801
|
"backend_code": error.backend_code,
|
|
709
802
|
"category": error.category,
|
|
803
|
+
"request_id": error.request_id,
|
|
804
|
+
"message": error.message,
|
|
710
805
|
}
|
|
711
806
|
|
|
712
807
|
todo_items = self._safe_task_list_items(profile=profile, task_box="todo", app_key=app_key)
|
|
@@ -756,7 +851,7 @@ class TaskContextTools(ToolBase):
|
|
|
756
851
|
runtime_consumed_after_action = bool(
|
|
757
852
|
runtime_verified
|
|
758
853
|
and isinstance(record_state_error, dict)
|
|
759
|
-
and record_state_error.get("backend_code") == 46001
|
|
854
|
+
and backend_code_value_int(record_state_error.get("backend_code")) == 46001
|
|
760
855
|
)
|
|
761
856
|
if runtime_consumed_after_action:
|
|
762
857
|
verification["record_state_scope"] = "current_node_runtime"
|
|
@@ -771,6 +866,28 @@ class TaskContextTools(ToolBase):
|
|
|
771
866
|
),
|
|
772
867
|
}
|
|
773
868
|
)
|
|
869
|
+
permission_blocked_sources: list[str] = []
|
|
870
|
+
if (
|
|
871
|
+
isinstance(verification.get("record_state_error"), dict)
|
|
872
|
+
and _is_permission_context_error_payload(verification["record_state_error"])
|
|
873
|
+
):
|
|
874
|
+
permission_blocked_sources.append("record_state")
|
|
875
|
+
if (
|
|
876
|
+
isinstance(verification.get("workflow_log_error"), dict)
|
|
877
|
+
and _is_permission_context_error_payload(verification["workflow_log_error"])
|
|
878
|
+
):
|
|
879
|
+
permission_blocked_sources.append("workflow_log")
|
|
880
|
+
if permission_blocked_sources:
|
|
881
|
+
warnings.append(
|
|
882
|
+
{
|
|
883
|
+
"code": "TASK_ACTION_VERIFICATION_PERMISSION_UNAVAILABLE",
|
|
884
|
+
"message": (
|
|
885
|
+
"task action executed, but some post-action verification reads are unavailable "
|
|
886
|
+
"in this permission context; do not treat the verification read failure as action denial."
|
|
887
|
+
),
|
|
888
|
+
"sources": permission_blocked_sources,
|
|
889
|
+
}
|
|
890
|
+
)
|
|
774
891
|
verification["runtime_continuation_verified"] = runtime_verified
|
|
775
892
|
if not runtime_verified:
|
|
776
893
|
warnings.append(
|
|
@@ -931,7 +1048,9 @@ class TaskContextTools(ToolBase):
|
|
|
931
1048
|
baseline["workflow_log_visible"] = True
|
|
932
1049
|
baseline["workflow_log_count"] = len(log_items)
|
|
933
1050
|
baseline["workflow_log_digest"] = self._workflow_log_digest(log_items)
|
|
934
|
-
except QingflowApiError:
|
|
1051
|
+
except QingflowApiError as exc:
|
|
1052
|
+
if is_auth_like_error(exc):
|
|
1053
|
+
raise
|
|
935
1054
|
pass
|
|
936
1055
|
todo_items = self._safe_task_list_items(profile=profile, task_box="todo", app_key=app_key)
|
|
937
1056
|
baseline["downstream_todo_nodes"] = sorted(
|
|
@@ -1058,7 +1177,9 @@ class TaskContextTools(ToolBase):
|
|
|
1058
1177
|
page=1,
|
|
1059
1178
|
page_size=50,
|
|
1060
1179
|
)
|
|
1061
|
-
except QingflowApiError:
|
|
1180
|
+
except QingflowApiError as exc:
|
|
1181
|
+
if not _is_task_optional_read_error(exc):
|
|
1182
|
+
raise
|
|
1062
1183
|
return []
|
|
1063
1184
|
items = response.get("items") if isinstance(response, dict) else None
|
|
1064
1185
|
if not isinstance(items, list):
|
|
@@ -1152,21 +1273,36 @@ class TaskContextTools(ToolBase):
|
|
|
1152
1273
|
task_id_text = normalize_positive_id_text(task_id, field_name="task_id")
|
|
1153
1274
|
searched_task_boxes = ("todo", "initiated", "cc", "done")
|
|
1154
1275
|
incomplete_task_boxes: list[str] = []
|
|
1276
|
+
inaccessible_task_boxes: list[dict[str, Any]] = []
|
|
1277
|
+
truncated_task_boxes: list[dict[str, Any]] = []
|
|
1155
1278
|
page_size = 100
|
|
1156
1279
|
for task_box in searched_task_boxes:
|
|
1157
1280
|
page = 1
|
|
1158
1281
|
page_amount: int | None = None
|
|
1159
1282
|
while True:
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1283
|
+
try:
|
|
1284
|
+
response = self._list_normalized_task_items(
|
|
1285
|
+
profile=profile,
|
|
1286
|
+
task_box=task_box,
|
|
1287
|
+
flow_status="all",
|
|
1288
|
+
app_key=None,
|
|
1289
|
+
workflow_node_id=None,
|
|
1290
|
+
query=None,
|
|
1291
|
+
page=page,
|
|
1292
|
+
page_size=page_size,
|
|
1293
|
+
)
|
|
1294
|
+
except QingflowApiError as exc:
|
|
1295
|
+
if not _is_optional_task_box_locator_error(exc):
|
|
1296
|
+
raise
|
|
1297
|
+
inaccessible_task_boxes.append(
|
|
1298
|
+
{
|
|
1299
|
+
"task_box": task_box,
|
|
1300
|
+
"backend_code": exc.backend_code,
|
|
1301
|
+
"http_status": exc.http_status,
|
|
1302
|
+
"request_id": exc.request_id,
|
|
1303
|
+
}
|
|
1304
|
+
)
|
|
1305
|
+
break
|
|
1170
1306
|
items = response.get("items") if isinstance(response.get("items"), list) else []
|
|
1171
1307
|
for item in items:
|
|
1172
1308
|
if not isinstance(item, dict) or not ids_equal(item.get("task_id"), task_id_text):
|
|
@@ -1192,17 +1328,52 @@ class TaskContextTools(ToolBase):
|
|
|
1192
1328
|
break
|
|
1193
1329
|
if not items or len(items) < page_size:
|
|
1194
1330
|
break
|
|
1331
|
+
if page >= TASK_LOCATOR_MAX_SCAN_PAGES_PER_BOX:
|
|
1332
|
+
truncated_task_boxes.append(
|
|
1333
|
+
{
|
|
1334
|
+
"task_box": task_box,
|
|
1335
|
+
"max_pages": TASK_LOCATOR_MAX_SCAN_PAGES_PER_BOX,
|
|
1336
|
+
"page_size": page_size,
|
|
1337
|
+
"reported_page_amount": page_amount,
|
|
1338
|
+
}
|
|
1339
|
+
)
|
|
1340
|
+
break
|
|
1195
1341
|
page += 1
|
|
1196
1342
|
if incomplete_task_boxes:
|
|
1197
1343
|
searched = ", ".join(incomplete_task_boxes)
|
|
1198
1344
|
raise_tool_error(
|
|
1199
1345
|
QingflowApiError.config_error(
|
|
1200
|
-
f"task_id={task_id_text} resolved to an incomplete task locator in task_box={searched};
|
|
1346
|
+
f"task_id={task_id_text} resolved to an incomplete task locator in task_box={searched}; rerun task_list and pass the exact data.items[].task_id. Do not substitute a row number or rebuild the locator from app_key/record_id/workflow_node_id."
|
|
1347
|
+
)
|
|
1348
|
+
)
|
|
1349
|
+
if inaccessible_task_boxes:
|
|
1350
|
+
searched = ", ".join(str(item.get("task_box")) for item in inaccessible_task_boxes)
|
|
1351
|
+
raise_tool_error(
|
|
1352
|
+
QingflowApiError.config_error(
|
|
1353
|
+
f"task_id={task_id_text} was not found in visible task boxes; some task boxes were not searchable in the current permission context: {searched}. Rerun task_list and use data.items[].task_id; do not substitute a row number or rebuild the locator.",
|
|
1354
|
+
details={
|
|
1355
|
+
"task_id": task_id_text,
|
|
1356
|
+
"searched_task_boxes": list(searched_task_boxes),
|
|
1357
|
+
"inaccessible_task_boxes": inaccessible_task_boxes,
|
|
1358
|
+
},
|
|
1359
|
+
)
|
|
1360
|
+
)
|
|
1361
|
+
if truncated_task_boxes:
|
|
1362
|
+
raise_tool_error(
|
|
1363
|
+
QingflowApiError.config_error(
|
|
1364
|
+
f"task_id={task_id_text} was not found in the first {TASK_LOCATOR_MAX_SCAN_PAGES_PER_BOX} pages of each visible task box. Rerun task_list with a query or the relevant page and pass the exact data.items[].task_id; do not guess task ids.",
|
|
1365
|
+
details={
|
|
1366
|
+
"task_id": task_id_text,
|
|
1367
|
+
"searched_task_boxes": list(searched_task_boxes),
|
|
1368
|
+
"truncated_task_boxes": truncated_task_boxes,
|
|
1369
|
+
"max_pages_per_task_box": TASK_LOCATOR_MAX_SCAN_PAGES_PER_BOX,
|
|
1370
|
+
"page_size": page_size,
|
|
1371
|
+
},
|
|
1201
1372
|
)
|
|
1202
1373
|
)
|
|
1203
1374
|
raise_tool_error(
|
|
1204
1375
|
QingflowApiError.config_error(
|
|
1205
|
-
f"task_id={task_id_text} was not found in the current visible task boxes (todo, initiated, cc, done)"
|
|
1376
|
+
f"task_id={task_id_text} was not found in the current visible task boxes (todo, initiated, cc, done). Rerun task_list and pass data.items[].task_id; do not use the displayed row number or guess app_key/record_id/workflow_node_id."
|
|
1206
1377
|
)
|
|
1207
1378
|
)
|
|
1208
1379
|
|
|
@@ -1224,6 +1395,7 @@ class TaskContextTools(ToolBase):
|
|
|
1224
1395
|
resolved_app_key = str(locator["app_key"])
|
|
1225
1396
|
resolved_record_id = normalize_positive_id_int(locator["record_id"], field_name="record_id")
|
|
1226
1397
|
resolved_workflow_node_id = int(locator["workflow_node_id"])
|
|
1398
|
+
resolved_task_box = str(locator.get("task_box") or "todo")
|
|
1227
1399
|
explicit_app_key = (app_key or "").strip()
|
|
1228
1400
|
if explicit_app_key and explicit_app_key != resolved_app_key:
|
|
1229
1401
|
raise_tool_error(
|
|
@@ -1248,8 +1420,10 @@ class TaskContextTools(ToolBase):
|
|
|
1248
1420
|
else:
|
|
1249
1421
|
resolved_record_id = normalize_positive_id_int(record_id, field_name="record_id")
|
|
1250
1422
|
resolved_workflow_node_id = int(workflow_node_id)
|
|
1423
|
+
resolved_task_box = "todo"
|
|
1251
1424
|
return {
|
|
1252
1425
|
"task_id": task_id_text,
|
|
1426
|
+
"task_box": resolved_task_box,
|
|
1253
1427
|
"app_key": resolved_app_key,
|
|
1254
1428
|
"record_id": resolved_record_id,
|
|
1255
1429
|
"record_id_text": stringify_backend_id(resolved_record_id),
|
|
@@ -1307,6 +1481,7 @@ class TaskContextTools(ToolBase):
|
|
|
1307
1481
|
resolved_record_id = int(locator["record_id"])
|
|
1308
1482
|
record_id_text = str(locator["record_id_text"] or "")
|
|
1309
1483
|
resolved_workflow_node_id = int(locator["workflow_node_id"])
|
|
1484
|
+
resolved_task_box = str(locator.get("task_box") or "todo")
|
|
1310
1485
|
self._require_app_record_and_node(resolved_app_key, resolved_record_id, resolved_workflow_node_id)
|
|
1311
1486
|
task_context = self._build_task_context(
|
|
1312
1487
|
profile=profile,
|
|
@@ -1314,6 +1489,7 @@ class TaskContextTools(ToolBase):
|
|
|
1314
1489
|
app_key=resolved_app_key,
|
|
1315
1490
|
record_id=resolved_record_id,
|
|
1316
1491
|
workflow_node_id=resolved_workflow_node_id,
|
|
1492
|
+
task_box=resolved_task_box,
|
|
1317
1493
|
include_candidates=False,
|
|
1318
1494
|
include_associated_reports=True,
|
|
1319
1495
|
current_uid=session_profile.uid,
|
|
@@ -1387,7 +1563,7 @@ class TaskContextTools(ToolBase):
|
|
|
1387
1563
|
|
|
1388
1564
|
chart_key = str(report_item.get("chart_key") or "")
|
|
1389
1565
|
source_type = report_item.get("source_type")
|
|
1390
|
-
if source_type
|
|
1566
|
+
if source_type in {"qingbi", "bi_qingflow", "bi_dataset"}:
|
|
1391
1567
|
qingbi_context = BackendRequestContext(
|
|
1392
1568
|
base_url=_qingbi_base_url(context.base_url),
|
|
1393
1569
|
token=context.token,
|
|
@@ -1396,20 +1572,44 @@ class TaskContextTools(ToolBase):
|
|
|
1396
1572
|
qf_version=context.qf_version,
|
|
1397
1573
|
qf_version_source=context.qf_version_source,
|
|
1398
1574
|
)
|
|
1399
|
-
|
|
1400
|
-
"
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
"
|
|
1411
|
-
|
|
1412
|
-
|
|
1575
|
+
chart_payload = {
|
|
1576
|
+
"asosChartId": report_id,
|
|
1577
|
+
"keyQueValues": association_query.get("keyQueValues") or [],
|
|
1578
|
+
}
|
|
1579
|
+
chart_params = {
|
|
1580
|
+
"qfUUID": uuid4().hex,
|
|
1581
|
+
"pageNum": page,
|
|
1582
|
+
"pageSize": page_size,
|
|
1583
|
+
}
|
|
1584
|
+
try:
|
|
1585
|
+
chart_result = self.backend.request(
|
|
1586
|
+
"POST",
|
|
1587
|
+
qingbi_context,
|
|
1588
|
+
f"/qingbi/charts/data/qflow/{chart_key}/detail",
|
|
1589
|
+
params=chart_params,
|
|
1590
|
+
json_body=chart_payload,
|
|
1591
|
+
)
|
|
1592
|
+
except QingflowApiError as error:
|
|
1593
|
+
if not _should_retry_asos_data(error):
|
|
1594
|
+
raise
|
|
1595
|
+
try:
|
|
1596
|
+
chart_result = self.backend.request(
|
|
1597
|
+
"POST",
|
|
1598
|
+
qingbi_context,
|
|
1599
|
+
f"/qingbi/charts/data/qflow/{chart_key}/asos",
|
|
1600
|
+
params=chart_params,
|
|
1601
|
+
json_body=chart_payload,
|
|
1602
|
+
)
|
|
1603
|
+
except QingflowApiError as fallback_error:
|
|
1604
|
+
if not _should_retry_asos_data(fallback_error):
|
|
1605
|
+
raise
|
|
1606
|
+
chart_result = self.backend.request(
|
|
1607
|
+
"POST",
|
|
1608
|
+
qingbi_context,
|
|
1609
|
+
f"/qingbi/charts/data/{chart_key}",
|
|
1610
|
+
params=chart_params,
|
|
1611
|
+
json_body=chart_payload,
|
|
1612
|
+
)
|
|
1413
1613
|
request_route = {
|
|
1414
1614
|
"base_url": qingbi_context.base_url,
|
|
1415
1615
|
"qf_version": qingbi_context.qf_version,
|
|
@@ -1509,6 +1709,7 @@ class TaskContextTools(ToolBase):
|
|
|
1509
1709
|
resolved_record_id = int(locator["record_id"])
|
|
1510
1710
|
record_id_text = str(locator["record_id_text"] or "")
|
|
1511
1711
|
resolved_workflow_node_id = int(locator["workflow_node_id"])
|
|
1712
|
+
resolved_task_box = str(locator.get("task_box") or "todo")
|
|
1512
1713
|
self._require_app_record_and_node(resolved_app_key, resolved_record_id, resolved_workflow_node_id)
|
|
1513
1714
|
task_context = self._build_task_context(
|
|
1514
1715
|
profile=profile,
|
|
@@ -1516,12 +1717,16 @@ class TaskContextTools(ToolBase):
|
|
|
1516
1717
|
app_key=resolved_app_key,
|
|
1517
1718
|
record_id=resolved_record_id,
|
|
1518
1719
|
workflow_node_id=resolved_workflow_node_id,
|
|
1720
|
+
task_box=resolved_task_box,
|
|
1519
1721
|
include_candidates=False,
|
|
1520
1722
|
include_associated_reports=False,
|
|
1521
1723
|
current_uid=session_profile.uid,
|
|
1522
1724
|
)
|
|
1523
1725
|
visibility = task_context.get("visibility") or {}
|
|
1524
|
-
|
|
1726
|
+
node = task_context.get("node") if isinstance(task_context.get("node"), dict) else {}
|
|
1727
|
+
raw_node = node.get("raw") if isinstance(node.get("raw"), dict) else {}
|
|
1728
|
+
audit_visibility_explicit = "auditRecordVisible" in raw_node
|
|
1729
|
+
if audit_visibility_explicit and not visibility.get("audit_record_visible"):
|
|
1525
1730
|
raise_tool_error(
|
|
1526
1731
|
QingflowApiError.config_error(
|
|
1527
1732
|
f"workflow logs are not visible for app_key='{resolved_app_key}' record_id={record_id_text} workflow_node_id={resolved_workflow_node_id}"
|
|
@@ -1535,7 +1740,7 @@ class TaskContextTools(ToolBase):
|
|
|
1535
1740
|
"key": resolved_app_key,
|
|
1536
1741
|
"rowRecordId": resolved_record_id,
|
|
1537
1742
|
"nodeId": resolved_workflow_node_id,
|
|
1538
|
-
"role":
|
|
1743
|
+
"role": _task_box_record_role(resolved_task_box),
|
|
1539
1744
|
"pageNum": 1,
|
|
1540
1745
|
"pageSize": 200,
|
|
1541
1746
|
},
|
|
@@ -1546,7 +1751,7 @@ class TaskContextTools(ToolBase):
|
|
|
1546
1751
|
"ws_id": session_profile.selected_ws_id,
|
|
1547
1752
|
"ok": True,
|
|
1548
1753
|
"request_route": self._request_route_payload(context),
|
|
1549
|
-
"warnings": [],
|
|
1754
|
+
"warnings": task_context.get("warnings") or [],
|
|
1550
1755
|
"output_profile": "normal",
|
|
1551
1756
|
"data": {
|
|
1552
1757
|
"selection": {
|
|
@@ -1555,7 +1760,9 @@ class TaskContextTools(ToolBase):
|
|
|
1555
1760
|
"workflow_node_id": resolved_workflow_node_id,
|
|
1556
1761
|
},
|
|
1557
1762
|
"visibility": {
|
|
1558
|
-
"audit_record_visible":
|
|
1763
|
+
"audit_record_visible": (
|
|
1764
|
+
visibility.get("audit_record_visible") if audit_visibility_explicit else None
|
|
1765
|
+
),
|
|
1559
1766
|
"qrobot_record_visible": visibility.get("qrobot_record_visible"),
|
|
1560
1767
|
},
|
|
1561
1768
|
"items": items,
|
|
@@ -1580,43 +1787,94 @@ class TaskContextTools(ToolBase):
|
|
|
1580
1787
|
include_candidates: bool,
|
|
1581
1788
|
include_associated_reports: bool,
|
|
1582
1789
|
current_uid: int | None = None,
|
|
1790
|
+
task_box: str = "todo",
|
|
1583
1791
|
) -> dict[str, Any]:
|
|
1584
1792
|
"""执行内部辅助逻辑。"""
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
node_info = self._select_task_node(audit_infos, workflow_node_id, app_key=app_key, record_id=record_id)
|
|
1592
|
-
detail = self.backend.request(
|
|
1593
|
-
"GET",
|
|
1594
|
-
context,
|
|
1595
|
-
f"/app/{app_key}/apply/{record_id}",
|
|
1596
|
-
params={"role": 3, "listType": 1, "auditNodeId": workflow_node_id},
|
|
1597
|
-
)
|
|
1598
|
-
app_name = self._task_app_name(context=context, app_key=app_key, detail=detail, node_info=node_info)
|
|
1599
|
-
associated_report_visible = self._resolve_associated_report_visible(node_info, detail)
|
|
1600
|
-
associated_reports = {"visible": associated_report_visible, "loaded": False, "count": 0, "items": []}
|
|
1601
|
-
if include_associated_reports and associated_report_visible:
|
|
1602
|
-
asos_chart_list = self.backend.request(
|
|
1793
|
+
context_warnings: list[JSONObject] = []
|
|
1794
|
+
detail: dict[str, Any] | None = None
|
|
1795
|
+
list_type = _task_box_record_list_type(task_box)
|
|
1796
|
+
role = _task_box_record_role(task_box)
|
|
1797
|
+
try:
|
|
1798
|
+
audit_infos = self.backend.request(
|
|
1603
1799
|
"GET",
|
|
1604
1800
|
context,
|
|
1605
|
-
f"/app/{app_key}/
|
|
1606
|
-
params={"
|
|
1801
|
+
f"/app/{app_key}/apply/{record_id}/auditInfo",
|
|
1802
|
+
params={"type": list_type},
|
|
1607
1803
|
)
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
"
|
|
1616
|
-
"
|
|
1617
|
-
|
|
1618
|
-
|
|
1804
|
+
node_info = self._select_task_node(audit_infos, workflow_node_id, app_key=app_key, record_id=record_id)
|
|
1805
|
+
except QingflowApiError as exc:
|
|
1806
|
+
if not _is_optional_task_audit_info_error(exc):
|
|
1807
|
+
raise
|
|
1808
|
+
detail = self.backend.request(
|
|
1809
|
+
"GET",
|
|
1810
|
+
context,
|
|
1811
|
+
f"/app/{app_key}/apply/{record_id}",
|
|
1812
|
+
params={"role": role, "listType": list_type, "auditNodeId": workflow_node_id},
|
|
1813
|
+
)
|
|
1814
|
+
node_info = self._fallback_task_node_from_detail(
|
|
1815
|
+
detail,
|
|
1816
|
+
workflow_node_id=workflow_node_id,
|
|
1817
|
+
source_error=exc,
|
|
1818
|
+
)
|
|
1819
|
+
context_warnings.append(
|
|
1820
|
+
{
|
|
1821
|
+
"code": "TASK_AUDIT_INFO_UNAVAILABLE",
|
|
1822
|
+
"message": "Task context used the current task detail because auditInfo was unavailable in this permission context.",
|
|
1823
|
+
"backend_code": exc.backend_code,
|
|
1824
|
+
"request_id": exc.request_id,
|
|
1825
|
+
"http_status": exc.http_status,
|
|
1826
|
+
}
|
|
1827
|
+
)
|
|
1828
|
+
if detail is None:
|
|
1829
|
+
detail = self.backend.request(
|
|
1830
|
+
"GET",
|
|
1831
|
+
context,
|
|
1832
|
+
f"/app/{app_key}/apply/{record_id}",
|
|
1833
|
+
params={"role": role, "listType": list_type, "auditNodeId": workflow_node_id},
|
|
1834
|
+
)
|
|
1835
|
+
app_name = self._task_app_name(context=context, app_key=app_key, detail=detail, node_info=node_info)
|
|
1836
|
+
associated_report_visible = self._resolve_associated_report_visible(node_info, detail)
|
|
1837
|
+
associated_reports = {"visible": associated_report_visible, "loaded": False, "count": 0, "items": [], "warnings": []}
|
|
1838
|
+
if include_associated_reports and associated_report_visible:
|
|
1839
|
+
try:
|
|
1840
|
+
asos_chart_list = self.backend.request(
|
|
1841
|
+
"GET",
|
|
1842
|
+
context,
|
|
1843
|
+
f"/app/{app_key}/asosChart",
|
|
1844
|
+
params={"role": role, "auditNodeId": workflow_node_id, "beingDraft": False},
|
|
1845
|
+
)
|
|
1846
|
+
associated_items = [
|
|
1847
|
+
self._normalize_associated_report(item)
|
|
1848
|
+
for item in (asos_chart_list.get("asosCharts") or [])
|
|
1849
|
+
if isinstance(item, dict)
|
|
1850
|
+
]
|
|
1851
|
+
associated_reports = {
|
|
1852
|
+
"visible": True,
|
|
1853
|
+
"loaded": True,
|
|
1854
|
+
"count": len(associated_items),
|
|
1855
|
+
"items": associated_items,
|
|
1856
|
+
"warnings": [],
|
|
1857
|
+
}
|
|
1858
|
+
except QingflowApiError as exc:
|
|
1859
|
+
if not _is_task_optional_read_error(exc):
|
|
1860
|
+
raise
|
|
1861
|
+
associated_reports = {
|
|
1862
|
+
"visible": True,
|
|
1863
|
+
"loaded": False,
|
|
1864
|
+
"count": 0,
|
|
1865
|
+
"items": [],
|
|
1866
|
+
"warnings": [
|
|
1867
|
+
{
|
|
1868
|
+
"code": "TASK_ASSOCIATED_REPORTS_UNAVAILABLE",
|
|
1869
|
+
"message": "Associated reports are not readable in this permission context; task detail remains available.",
|
|
1870
|
+
"backend_code": exc.backend_code,
|
|
1871
|
+
"request_id": exc.request_id,
|
|
1872
|
+
"http_status": exc.http_status,
|
|
1873
|
+
}
|
|
1874
|
+
],
|
|
1875
|
+
}
|
|
1619
1876
|
rollback_items: list[dict[str, Any]] = []
|
|
1877
|
+
rollback_warnings: list[JSONObject] = []
|
|
1620
1878
|
transfer_items: list[dict[str, Any]] = []
|
|
1621
1879
|
transfer_warnings: list[JSONObject] = []
|
|
1622
1880
|
transfer_pagination: JSONObject = {
|
|
@@ -1628,13 +1886,26 @@ class TaskContextTools(ToolBase):
|
|
|
1628
1886
|
"truncated": False,
|
|
1629
1887
|
}
|
|
1630
1888
|
if include_candidates:
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1889
|
+
try:
|
|
1890
|
+
rollback_result = self.backend.request(
|
|
1891
|
+
"GET",
|
|
1892
|
+
context,
|
|
1893
|
+
f"/app/{app_key}/apply/{record_id}/revertNode",
|
|
1894
|
+
params={"auditNodeId": workflow_node_id},
|
|
1895
|
+
)
|
|
1896
|
+
rollback_items = self._rollback_candidate_items(rollback_result)
|
|
1897
|
+
except QingflowApiError as exc:
|
|
1898
|
+
if not _is_task_optional_read_error(exc):
|
|
1899
|
+
raise
|
|
1900
|
+
rollback_warnings.append(
|
|
1901
|
+
{
|
|
1902
|
+
"code": "TASK_ROLLBACK_CANDIDATES_UNAVAILABLE",
|
|
1903
|
+
"message": "Rollback candidates are not readable in this permission context; task detail remains available.",
|
|
1904
|
+
"backend_code": exc.backend_code,
|
|
1905
|
+
"request_id": exc.request_id,
|
|
1906
|
+
"http_status": exc.http_status,
|
|
1907
|
+
}
|
|
1908
|
+
)
|
|
1638
1909
|
transfer_items, transfer_warnings, transfer_pagination = self._transfer_candidate_items(
|
|
1639
1910
|
context,
|
|
1640
1911
|
app_key=app_key,
|
|
@@ -1657,6 +1928,7 @@ class TaskContextTools(ToolBase):
|
|
|
1657
1928
|
context,
|
|
1658
1929
|
app_key=app_key,
|
|
1659
1930
|
workflow_node_id=workflow_node_id,
|
|
1931
|
+
node_info=node_info,
|
|
1660
1932
|
)
|
|
1661
1933
|
capabilities = self._build_capabilities(
|
|
1662
1934
|
node_info,
|
|
@@ -1703,7 +1975,9 @@ class TaskContextTools(ToolBase):
|
|
|
1703
1975
|
"transfer_members": transfer_items,
|
|
1704
1976
|
"loaded": include_candidates,
|
|
1705
1977
|
"transfer_pagination": transfer_pagination,
|
|
1706
|
-
"
|
|
1978
|
+
"rollback_warnings": rollback_warnings,
|
|
1979
|
+
"transfer_warnings": transfer_warnings,
|
|
1980
|
+
"warnings": [*rollback_warnings, *transfer_warnings],
|
|
1707
1981
|
},
|
|
1708
1982
|
"workflow_log_summary": {
|
|
1709
1983
|
"visible": visibility["audit_record_visible"],
|
|
@@ -1712,6 +1986,7 @@ class TaskContextTools(ToolBase):
|
|
|
1712
1986
|
"qrobot_log_visible": visibility["qrobot_record_visible"],
|
|
1713
1987
|
},
|
|
1714
1988
|
"update_schema": update_schema,
|
|
1989
|
+
"warnings": context_warnings,
|
|
1715
1990
|
}
|
|
1716
1991
|
|
|
1717
1992
|
def _compact_task_get_context(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -1781,12 +2056,14 @@ class TaskContextTools(ToolBase):
|
|
|
1781
2056
|
"loaded": bool(associated_reports.get("loaded")),
|
|
1782
2057
|
"count": len(associated_items),
|
|
1783
2058
|
"items": associated_items,
|
|
2059
|
+
"warnings": associated_reports.get("warnings") or [],
|
|
1784
2060
|
},
|
|
1785
2061
|
"rollback_candidates": {
|
|
1786
2062
|
"available": "rollback" in available_actions,
|
|
1787
2063
|
"loaded": bool(candidates.get("loaded")),
|
|
1788
2064
|
"count": len(rollback_items),
|
|
1789
2065
|
"items": rollback_items,
|
|
2066
|
+
"warnings": candidates.get("rollback_warnings") or [],
|
|
1790
2067
|
},
|
|
1791
2068
|
"transfer_candidates": {
|
|
1792
2069
|
"available": "transfer" in available_actions,
|
|
@@ -1794,7 +2071,7 @@ class TaskContextTools(ToolBase):
|
|
|
1794
2071
|
"count": len(transfer_items),
|
|
1795
2072
|
"items": transfer_items,
|
|
1796
2073
|
"pagination": transfer_pagination,
|
|
1797
|
-
"warnings": candidates.get("
|
|
2074
|
+
"warnings": candidates.get("transfer_warnings") or [],
|
|
1798
2075
|
},
|
|
1799
2076
|
},
|
|
1800
2077
|
}
|
|
@@ -1829,6 +2106,8 @@ class TaskContextTools(ToolBase):
|
|
|
1829
2106
|
metadata["blockers"] = blockers
|
|
1830
2107
|
if warnings:
|
|
1831
2108
|
metadata["warnings"] = warnings
|
|
2109
|
+
if update_schema.get("editable_question_ids_source"):
|
|
2110
|
+
metadata["editable_question_ids_source"] = update_schema.get("editable_question_ids_source")
|
|
1832
2111
|
return metadata
|
|
1833
2112
|
|
|
1834
2113
|
def _compact_initiator(self, payload: Any) -> dict[str, Any] | None:
|
|
@@ -1877,7 +2156,9 @@ class TaskContextTools(ToolBase):
|
|
|
1877
2156
|
) -> str | None:
|
|
1878
2157
|
try:
|
|
1879
2158
|
base_info = self.backend.request("GET", context, f"/app/{app_key}/baseInfo")
|
|
1880
|
-
except QingflowApiError:
|
|
2159
|
+
except QingflowApiError as exc:
|
|
2160
|
+
if not _is_task_optional_read_error(exc):
|
|
2161
|
+
raise
|
|
1881
2162
|
return None
|
|
1882
2163
|
if not isinstance(base_info, dict):
|
|
1883
2164
|
return None
|
|
@@ -1895,7 +2176,9 @@ class TaskContextTools(ToolBase):
|
|
|
1895
2176
|
) -> str | None:
|
|
1896
2177
|
try:
|
|
1897
2178
|
visible_apps = self.backend.request("GET", context, "/tag/apps")
|
|
1898
|
-
except QingflowApiError:
|
|
2179
|
+
except QingflowApiError as exc:
|
|
2180
|
+
if not _is_task_optional_read_error(exc):
|
|
2181
|
+
raise
|
|
1899
2182
|
return None
|
|
1900
2183
|
return self._find_task_app_name_in_visible_apps(visible_apps, app_key=app_key)
|
|
1901
2184
|
|
|
@@ -2095,6 +2378,54 @@ class TaskContextTools(ToolBase):
|
|
|
2095
2378
|
)
|
|
2096
2379
|
)
|
|
2097
2380
|
|
|
2381
|
+
def _fallback_task_node_from_detail(
|
|
2382
|
+
self,
|
|
2383
|
+
detail: dict[str, Any],
|
|
2384
|
+
*,
|
|
2385
|
+
workflow_node_id: int,
|
|
2386
|
+
source_error: QingflowApiError,
|
|
2387
|
+
) -> dict[str, Any]:
|
|
2388
|
+
"""Build a conservative task node snapshot from the readable task detail."""
|
|
2389
|
+
node_info: dict[str, Any] = {
|
|
2390
|
+
"auditNodeId": workflow_node_id,
|
|
2391
|
+
"nodeId": workflow_node_id,
|
|
2392
|
+
"auditNodeName": (
|
|
2393
|
+
detail.get("auditNodeName")
|
|
2394
|
+
or detail.get("nodeName")
|
|
2395
|
+
or detail.get("workflowNodeName")
|
|
2396
|
+
or detail.get("currentNodeName")
|
|
2397
|
+
),
|
|
2398
|
+
"_auditInfoUnavailable": True,
|
|
2399
|
+
"_auditInfoError": {
|
|
2400
|
+
"http_status": source_error.http_status,
|
|
2401
|
+
"backend_code": source_error.backend_code,
|
|
2402
|
+
"category": source_error.category,
|
|
2403
|
+
},
|
|
2404
|
+
}
|
|
2405
|
+
for key in (
|
|
2406
|
+
"canTransfer",
|
|
2407
|
+
"canRevert",
|
|
2408
|
+
"canUrge",
|
|
2409
|
+
"rejectBtnStatus",
|
|
2410
|
+
"canRevoke",
|
|
2411
|
+
"beingEndWorkflow",
|
|
2412
|
+
"beingCanApplyAgain",
|
|
2413
|
+
"beingSubmitCheck",
|
|
2414
|
+
"beingSubmitPreview",
|
|
2415
|
+
"feedbackRequiredOperationType",
|
|
2416
|
+
"queAuthSetting",
|
|
2417
|
+
"auditRecordVisible",
|
|
2418
|
+
"qrobotRecordBeingVisible",
|
|
2419
|
+
"beingWorkflowNodeFutureListVisible",
|
|
2420
|
+
"commentStatus",
|
|
2421
|
+
"asosChartVisible",
|
|
2422
|
+
):
|
|
2423
|
+
if key in detail:
|
|
2424
|
+
node_info[key] = detail[key]
|
|
2425
|
+
if "viewAsosChartVisible" in detail and "asosChartVisible" not in node_info:
|
|
2426
|
+
node_info["asosChartVisible"] = detail.get("viewAsosChartVisible")
|
|
2427
|
+
return node_info
|
|
2428
|
+
|
|
2098
2429
|
def _build_capabilities(
|
|
2099
2430
|
self,
|
|
2100
2431
|
node_info: dict[str, Any],
|
|
@@ -2159,35 +2490,18 @@ class TaskContextTools(ToolBase):
|
|
|
2159
2490
|
try:
|
|
2160
2491
|
app_schema = self._record_tools._get_form_schema(profile, context, app_key, force_refresh=False)
|
|
2161
2492
|
except QingflowApiError as error:
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
"app_key": app_key,
|
|
2175
|
-
"record_id": record_id_text,
|
|
2176
|
-
"workflow_node_id": workflow_node_id,
|
|
2177
|
-
},
|
|
2178
|
-
"transport_error": {
|
|
2179
|
-
"http_status": error.http_status,
|
|
2180
|
-
"backend_code": error.backend_code,
|
|
2181
|
-
"category": error.category,
|
|
2182
|
-
},
|
|
2183
|
-
}
|
|
2184
|
-
return {
|
|
2185
|
-
"public_schema": public_schema,
|
|
2186
|
-
"index": None,
|
|
2187
|
-
"editable_question_ids": [],
|
|
2188
|
-
"effective_editable_question_ids": [],
|
|
2189
|
-
"editable_question_ids_source": "schema_unavailable",
|
|
2190
|
-
}
|
|
2493
|
+
if not _is_task_optional_read_error(error):
|
|
2494
|
+
raise
|
|
2495
|
+
return self._build_task_update_schema_from_runtime_answers(
|
|
2496
|
+
profile=profile,
|
|
2497
|
+
context=context,
|
|
2498
|
+
app_key=app_key,
|
|
2499
|
+
record_id=record_id,
|
|
2500
|
+
workflow_node_id=workflow_node_id,
|
|
2501
|
+
node_info=node_info,
|
|
2502
|
+
current_answers=current_answers,
|
|
2503
|
+
source_error=error,
|
|
2504
|
+
)
|
|
2191
2505
|
|
|
2192
2506
|
question_relations = _collect_question_relations(app_schema)
|
|
2193
2507
|
linked_field_ids = _collect_linked_required_field_ids(question_relations)
|
|
@@ -2260,6 +2574,7 @@ class TaskContextTools(ToolBase):
|
|
|
2260
2574
|
"record_id": record_id_text,
|
|
2261
2575
|
"workflow_node_id": workflow_node_id,
|
|
2262
2576
|
},
|
|
2577
|
+
"editable_question_ids_source": source,
|
|
2263
2578
|
}
|
|
2264
2579
|
return {
|
|
2265
2580
|
"public_schema": public_schema,
|
|
@@ -2269,6 +2584,96 @@ class TaskContextTools(ToolBase):
|
|
|
2269
2584
|
"editable_question_ids_source": source,
|
|
2270
2585
|
}
|
|
2271
2586
|
|
|
2587
|
+
def _build_task_update_schema_from_runtime_answers(
|
|
2588
|
+
self,
|
|
2589
|
+
*,
|
|
2590
|
+
profile: str,
|
|
2591
|
+
context: BackendRequestContext,
|
|
2592
|
+
app_key: str,
|
|
2593
|
+
record_id: int,
|
|
2594
|
+
workflow_node_id: int,
|
|
2595
|
+
node_info: dict[str, Any],
|
|
2596
|
+
current_answers: Any,
|
|
2597
|
+
source_error: QingflowApiError,
|
|
2598
|
+
) -> dict[str, Any]:
|
|
2599
|
+
"""Build node-scoped edit schema from the task detail when app schema is unavailable."""
|
|
2600
|
+
record_id_text = stringify_backend_id(record_id)
|
|
2601
|
+
editable_question_ids, schema_warnings, source = self._resolve_task_editable_question_ids(
|
|
2602
|
+
context,
|
|
2603
|
+
app_key=app_key,
|
|
2604
|
+
workflow_node_id=workflow_node_id,
|
|
2605
|
+
node_info=node_info,
|
|
2606
|
+
)
|
|
2607
|
+
schema_warnings.insert(
|
|
2608
|
+
0,
|
|
2609
|
+
{
|
|
2610
|
+
"code": "TASK_UPDATE_SCHEMA_APP_SCHEMA_UNAVAILABLE",
|
|
2611
|
+
"message": "task update schema used current task answers because the app applicant schema was unavailable in this permission context.",
|
|
2612
|
+
"transport_error": {
|
|
2613
|
+
"http_status": source_error.http_status,
|
|
2614
|
+
"backend_code": source_error.backend_code,
|
|
2615
|
+
"category": source_error.category,
|
|
2616
|
+
},
|
|
2617
|
+
},
|
|
2618
|
+
)
|
|
2619
|
+
index = _build_answer_backed_field_index(
|
|
2620
|
+
current_answers,
|
|
2621
|
+
field_id_filter={str(que_id) for que_id in editable_question_ids if que_id > 0},
|
|
2622
|
+
)
|
|
2623
|
+
effective_editable_ids = set(editable_question_ids)
|
|
2624
|
+
writable_fields: list[JSONObject] = []
|
|
2625
|
+
for field in index.by_id.values():
|
|
2626
|
+
if field.que_type in LAYOUT_ONLY_QUE_TYPES or field.que_id not in effective_editable_ids:
|
|
2627
|
+
continue
|
|
2628
|
+
editable_field = _clone_form_field(field, readonly=False)
|
|
2629
|
+
write_hints = self._record_tools._schema_write_hints(editable_field)
|
|
2630
|
+
if not bool(write_hints.get("writable")):
|
|
2631
|
+
continue
|
|
2632
|
+
writable_field = self._record_tools._ready_schema_field_payload(
|
|
2633
|
+
profile,
|
|
2634
|
+
context,
|
|
2635
|
+
editable_field,
|
|
2636
|
+
ws_id=context.ws_id,
|
|
2637
|
+
required_override=False,
|
|
2638
|
+
linkage_payloads_by_field_id={},
|
|
2639
|
+
)
|
|
2640
|
+
writable_field.setdefault("field_id", editable_field.que_id)
|
|
2641
|
+
writable_fields.append(writable_field)
|
|
2642
|
+
|
|
2643
|
+
blockers: list[str] = []
|
|
2644
|
+
if not writable_fields:
|
|
2645
|
+
blockers.append("NO_TASK_EDITABLE_FIELDS")
|
|
2646
|
+
schema_warnings.append(
|
|
2647
|
+
{
|
|
2648
|
+
"code": "NO_TASK_EDITABLE_FIELDS",
|
|
2649
|
+
"message": "the current task node does not expose any writable fields in the current task detail.",
|
|
2650
|
+
}
|
|
2651
|
+
)
|
|
2652
|
+
public_schema: JSONObject = {
|
|
2653
|
+
"schema_scope": "task_update_ready",
|
|
2654
|
+
"writable_fields": writable_fields,
|
|
2655
|
+
"payload_template": {
|
|
2656
|
+
item["title"]: self._record_tools._ready_schema_template_value(item)
|
|
2657
|
+
for item in writable_fields
|
|
2658
|
+
if isinstance(item, dict) and item.get("title")
|
|
2659
|
+
},
|
|
2660
|
+
"blockers": blockers,
|
|
2661
|
+
"warnings": schema_warnings,
|
|
2662
|
+
"selection": {
|
|
2663
|
+
"app_key": app_key,
|
|
2664
|
+
"record_id": record_id_text,
|
|
2665
|
+
"workflow_node_id": workflow_node_id,
|
|
2666
|
+
},
|
|
2667
|
+
"editable_question_ids_source": f"{source}_runtime_answers",
|
|
2668
|
+
}
|
|
2669
|
+
return {
|
|
2670
|
+
"public_schema": public_schema,
|
|
2671
|
+
"index": index,
|
|
2672
|
+
"editable_question_ids": sorted(editable_question_ids),
|
|
2673
|
+
"effective_editable_question_ids": sorted(effective_editable_ids),
|
|
2674
|
+
"editable_question_ids_source": f"{source}_runtime_answers",
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2272
2677
|
def _augment_task_editable_field_index(
|
|
2273
2678
|
self,
|
|
2274
2679
|
*,
|
|
@@ -2328,12 +2733,15 @@ class TaskContextTools(ToolBase):
|
|
|
2328
2733
|
if question_ids:
|
|
2329
2734
|
return question_ids, warnings, "workflow_editable_que_ids"
|
|
2330
2735
|
except QingflowApiError as error:
|
|
2331
|
-
if
|
|
2736
|
+
if not _is_task_optional_read_error(error):
|
|
2332
2737
|
raise
|
|
2333
2738
|
warnings.append(
|
|
2334
2739
|
{
|
|
2335
2740
|
"code": "TASK_EDITABLE_IDS_FALLBACK",
|
|
2336
2741
|
"message": "editable question ids endpoint is unavailable in the current route; task update schema fell back to queAuthSetting and may be conservative.",
|
|
2742
|
+
"backend_code": error.backend_code,
|
|
2743
|
+
"http_status": error.http_status,
|
|
2744
|
+
"request_id": error.request_id,
|
|
2337
2745
|
}
|
|
2338
2746
|
)
|
|
2339
2747
|
fallback_ids = self._editable_ids_from_que_auth_setting(node_info.get("queAuthSetting"))
|
|
@@ -2345,6 +2753,7 @@ class TaskContextTools(ToolBase):
|
|
|
2345
2753
|
*,
|
|
2346
2754
|
app_key: str,
|
|
2347
2755
|
workflow_node_id: int,
|
|
2756
|
+
node_info: dict[str, Any],
|
|
2348
2757
|
) -> tuple[bool, list[JSONObject], str]:
|
|
2349
2758
|
"""执行内部辅助逻辑。"""
|
|
2350
2759
|
try:
|
|
@@ -2354,15 +2763,28 @@ class TaskContextTools(ToolBase):
|
|
|
2354
2763
|
f"/app/{app_key}/auditNode/{workflow_node_id}/editableQueIds",
|
|
2355
2764
|
)
|
|
2356
2765
|
except QingflowApiError as error:
|
|
2766
|
+
if not _is_task_optional_read_error(error):
|
|
2767
|
+
raise
|
|
2768
|
+
fallback_ids = self._editable_ids_from_que_auth_setting(node_info.get("queAuthSetting"))
|
|
2769
|
+
fallback_source = "que_auth_setting" if fallback_ids else "backend_editable_que_ids_unavailable"
|
|
2357
2770
|
warning: JSONObject = {
|
|
2358
|
-
"code": "TASK_SAVE_ONLY_SIGNAL_UNAVAILABLE",
|
|
2359
|
-
"message":
|
|
2771
|
+
"code": "TASK_SAVE_ONLY_EDITABLE_IDS_FALLBACK" if fallback_ids else "TASK_SAVE_ONLY_SIGNAL_UNAVAILABLE",
|
|
2772
|
+
"message": (
|
|
2773
|
+
"save_only availability used current task queAuthSetting because backend editableQueIds was unavailable in this permission context."
|
|
2774
|
+
if fallback_ids
|
|
2775
|
+
else "save_only is hidden because backend editableQueIds is unavailable and current task detail does not expose editable fields."
|
|
2776
|
+
),
|
|
2360
2777
|
}
|
|
2361
2778
|
if error.backend_code is not None:
|
|
2362
2779
|
warning["backend_code"] = error.backend_code
|
|
2363
2780
|
if error.http_status is not None:
|
|
2364
2781
|
warning["http_status"] = error.http_status
|
|
2365
|
-
|
|
2782
|
+
if error.request_id is not None:
|
|
2783
|
+
warning["request_id"] = error.request_id
|
|
2784
|
+
if fallback_ids:
|
|
2785
|
+
warning["field_ids"] = sorted(fallback_ids)
|
|
2786
|
+
return True, [warning], fallback_source
|
|
2787
|
+
return False, [warning], fallback_source
|
|
2366
2788
|
return bool(self._extract_question_ids(payload)), [], "workflow_editable_que_ids"
|
|
2367
2789
|
|
|
2368
2790
|
def _extract_question_ids(self, payload: Any) -> set[int]:
|
|
@@ -2655,12 +3077,33 @@ class TaskContextTools(ToolBase):
|
|
|
2655
3077
|
warnings: list[JSONObject] = []
|
|
2656
3078
|
|
|
2657
3079
|
while page_num <= max_pages:
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
3080
|
+
try:
|
|
3081
|
+
result = self.backend.request(
|
|
3082
|
+
"GET",
|
|
3083
|
+
context,
|
|
3084
|
+
f"/app/{app_key}/apply/{record_id}/transfer/member",
|
|
3085
|
+
params={"pageNum": page_num, "pageSize": page_size, "auditNodeId": workflow_node_id},
|
|
3086
|
+
)
|
|
3087
|
+
except QingflowApiError as exc:
|
|
3088
|
+
if not _is_task_optional_read_error(exc):
|
|
3089
|
+
raise
|
|
3090
|
+
warnings.append(
|
|
3091
|
+
{
|
|
3092
|
+
"code": "TASK_TRANSFER_CANDIDATES_UNAVAILABLE",
|
|
3093
|
+
"message": "Transfer candidates are not readable in this permission context; task detail remains available.",
|
|
3094
|
+
"backend_code": exc.backend_code,
|
|
3095
|
+
"request_id": exc.request_id,
|
|
3096
|
+
"http_status": exc.http_status,
|
|
3097
|
+
}
|
|
3098
|
+
)
|
|
3099
|
+
return items, warnings, {
|
|
3100
|
+
"loaded": False,
|
|
3101
|
+
"page_size": page_size,
|
|
3102
|
+
"fetched_pages": fetched_pages,
|
|
3103
|
+
"reported_total": reported_total,
|
|
3104
|
+
"page_amount": page_amount,
|
|
3105
|
+
"truncated": False,
|
|
3106
|
+
}
|
|
2664
3107
|
fetched_pages += 1
|
|
2665
3108
|
raw_items = _approval_page_items(result)
|
|
2666
3109
|
fetched_raw_count += len(raw_items)
|
|
@@ -2825,7 +3268,9 @@ class TaskContextTools(ToolBase):
|
|
|
2825
3268
|
context,
|
|
2826
3269
|
f"/chart/{chart_key}/auth",
|
|
2827
3270
|
)
|
|
2828
|
-
except QingflowApiError:
|
|
3271
|
+
except QingflowApiError as error:
|
|
3272
|
+
if not _is_task_optional_read_error(error):
|
|
3273
|
+
raise
|
|
2829
3274
|
return False
|
|
2830
3275
|
if not isinstance(auth, dict):
|
|
2831
3276
|
return False
|
|
@@ -3002,3 +3447,62 @@ class TaskContextTools(ToolBase):
|
|
|
3002
3447
|
"qf_version": context.qf_version,
|
|
3003
3448
|
"qf_version_source": context.qf_version_source or ("context" if context.qf_version else "unknown"),
|
|
3004
3449
|
}
|
|
3450
|
+
|
|
3451
|
+
|
|
3452
|
+
def _is_optional_task_audit_info_error(error: QingflowApiError) -> bool:
|
|
3453
|
+
"""Whether task auditInfo may be replaced by current task detail."""
|
|
3454
|
+
if is_auth_like_error(error):
|
|
3455
|
+
return False
|
|
3456
|
+
return _is_task_permission_context_error(error) or error.http_status == 404
|
|
3457
|
+
|
|
3458
|
+
|
|
3459
|
+
def _task_box_record_list_type(task_box: str | None) -> int:
|
|
3460
|
+
normalized = str(task_box or "todo").strip().lower()
|
|
3461
|
+
if normalized == "initiated":
|
|
3462
|
+
return 14
|
|
3463
|
+
if normalized == "done":
|
|
3464
|
+
return 2
|
|
3465
|
+
if normalized == "cc":
|
|
3466
|
+
return 12
|
|
3467
|
+
return 1
|
|
3468
|
+
|
|
3469
|
+
|
|
3470
|
+
def _task_box_record_role(task_box: str | None) -> int:
|
|
3471
|
+
normalized = str(task_box or "todo").strip().lower()
|
|
3472
|
+
if normalized == "initiated":
|
|
3473
|
+
return 2
|
|
3474
|
+
return 3
|
|
3475
|
+
|
|
3476
|
+
|
|
3477
|
+
def _is_optional_task_box_locator_error(error: QingflowApiError) -> bool:
|
|
3478
|
+
"""Whether one task box may be skipped while resolving a task_id."""
|
|
3479
|
+
if is_auth_like_error(error):
|
|
3480
|
+
return False
|
|
3481
|
+
return _is_task_permission_context_error(error) or error.http_status == 404
|
|
3482
|
+
|
|
3483
|
+
|
|
3484
|
+
def _is_task_optional_read_error(error: QingflowApiError) -> bool:
|
|
3485
|
+
if is_auth_like_error(error):
|
|
3486
|
+
return False
|
|
3487
|
+
backend_code = _task_backend_code(error)
|
|
3488
|
+
return backend_code in {40002, 40027, 404} or error.http_status == 404
|
|
3489
|
+
|
|
3490
|
+
|
|
3491
|
+
def _is_task_permission_context_error(error: QingflowApiError) -> bool:
|
|
3492
|
+
if is_auth_like_error(error):
|
|
3493
|
+
return False
|
|
3494
|
+
return _task_backend_code(error) in {40002, 40027}
|
|
3495
|
+
|
|
3496
|
+
|
|
3497
|
+
def _is_permission_context_error_payload(payload: dict[str, Any]) -> bool:
|
|
3498
|
+
if str(payload.get("category") or "").strip().lower() == "auth":
|
|
3499
|
+
return False
|
|
3500
|
+
if message_looks_like_invalid_token(payload.get("message")):
|
|
3501
|
+
return False
|
|
3502
|
+
if backend_code_value_int(payload.get("http_status")) == 401:
|
|
3503
|
+
return False
|
|
3504
|
+
return backend_code_value_int(payload.get("backend_code")) in {40002, 40027}
|
|
3505
|
+
|
|
3506
|
+
|
|
3507
|
+
def _task_backend_code(error: QingflowApiError) -> int | None:
|
|
3508
|
+
return backend_code_int(error)
|