@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
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
from typing import Any
|
|
4
5
|
|
|
5
6
|
from mcp.server.fastmcp import FastMCP
|
|
6
7
|
|
|
7
8
|
from ..config import DEFAULT_PROFILE
|
|
8
|
-
from ..errors import QingflowApiError, raise_tool_error
|
|
9
|
+
from ..errors import QingflowApiError, backend_code_int, backend_code_value_int, is_auth_like_error, message_looks_like_invalid_token, raise_tool_error
|
|
9
10
|
from ..json_types import JSONObject
|
|
10
11
|
from ..list_type_labels import get_record_list_type_label
|
|
11
12
|
from .base import ToolBase, tool_cn_name
|
|
@@ -172,15 +173,26 @@ class ApprovalTools(ToolBase):
|
|
|
172
173
|
keyword: str | None = None,
|
|
173
174
|
) -> dict[str, Any]:
|
|
174
175
|
"""执行记录相关逻辑。"""
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
176
|
+
selection = {"app_key": app_key, "record_id": record_id, "list_type": list_type, "keyword": keyword}
|
|
177
|
+
try:
|
|
178
|
+
raw = self.record_comment_mention_candidates(
|
|
179
|
+
profile=profile,
|
|
180
|
+
app_key=app_key,
|
|
181
|
+
apply_id=record_id,
|
|
182
|
+
page_size=page_size,
|
|
183
|
+
page_num=page_num,
|
|
184
|
+
list_type=list_type,
|
|
185
|
+
keyword=keyword,
|
|
186
|
+
)
|
|
187
|
+
except RuntimeError as exc:
|
|
188
|
+
if not _is_optional_approval_runtime_error(exc):
|
|
189
|
+
raise
|
|
190
|
+
return self._runtime_error_as_auxiliary_result(
|
|
191
|
+
exc,
|
|
192
|
+
error_code="RECORD_COMMENT_MENTIONS_UNAVAILABLE",
|
|
193
|
+
selection=selection,
|
|
194
|
+
fallback_hint="Mention candidates are unavailable in this permission context; write a plain comment or retry without mentions.",
|
|
195
|
+
)
|
|
184
196
|
items = _approval_page_items(raw.get("page"))
|
|
185
197
|
return self._public_page_response(
|
|
186
198
|
raw,
|
|
@@ -192,7 +204,7 @@ class ApprovalTools(ToolBase):
|
|
|
192
204
|
"page_amount": _approval_page_amount(raw.get("page")),
|
|
193
205
|
"reported_total": _approval_page_total(raw.get("page")),
|
|
194
206
|
},
|
|
195
|
-
selection=
|
|
207
|
+
selection=selection,
|
|
196
208
|
)
|
|
197
209
|
|
|
198
210
|
@tool_cn_name("任务通过")
|
|
@@ -242,13 +254,24 @@ class ApprovalTools(ToolBase):
|
|
|
242
254
|
@tool_cn_name("任务退回候选")
|
|
243
255
|
def task_rollback_candidates(self, *, profile: str, app_key: str, record_id: int, workflow_node_id: int) -> dict[str, Any]:
|
|
244
256
|
"""执行任务相关逻辑。"""
|
|
245
|
-
|
|
257
|
+
selection = {"app_key": app_key, "record_id": record_id, "workflow_node_id": workflow_node_id}
|
|
258
|
+
try:
|
|
259
|
+
raw = self.record_rollback_candidates(profile=profile, app_key=app_key, apply_id=record_id, audit_node_id=workflow_node_id)
|
|
260
|
+
except RuntimeError as exc:
|
|
261
|
+
if not _is_optional_approval_runtime_error(exc):
|
|
262
|
+
raise
|
|
263
|
+
return self._runtime_error_as_auxiliary_result(
|
|
264
|
+
exc,
|
|
265
|
+
error_code="TASK_ROLLBACK_CANDIDATES_UNAVAILABLE",
|
|
266
|
+
selection=selection,
|
|
267
|
+
fallback_hint="Rollback candidates are unavailable in this permission context; use task get for current context or retry with a valid actionable node.",
|
|
268
|
+
)
|
|
246
269
|
items = _approval_page_items(raw.get("result"))
|
|
247
270
|
return self._public_page_response(
|
|
248
271
|
raw,
|
|
249
272
|
items=items,
|
|
250
273
|
pagination={"returned_items": len(items)},
|
|
251
|
-
selection=
|
|
274
|
+
selection=selection,
|
|
252
275
|
)
|
|
253
276
|
|
|
254
277
|
@tool_cn_name("任务退回")
|
|
@@ -331,15 +354,26 @@ class ApprovalTools(ToolBase):
|
|
|
331
354
|
keyword: str | None = None,
|
|
332
355
|
) -> dict[str, Any]:
|
|
333
356
|
"""执行任务相关逻辑。"""
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
357
|
+
selection = {"app_key": app_key, "record_id": record_id, "workflow_node_id": workflow_node_id, "keyword": keyword}
|
|
358
|
+
try:
|
|
359
|
+
raw = self.record_transfer_candidates(
|
|
360
|
+
profile=profile,
|
|
361
|
+
app_key=app_key,
|
|
362
|
+
apply_id=record_id,
|
|
363
|
+
page_size=page_size,
|
|
364
|
+
page_num=page_num,
|
|
365
|
+
audit_node_id=workflow_node_id,
|
|
366
|
+
keyword=keyword,
|
|
367
|
+
)
|
|
368
|
+
except RuntimeError as exc:
|
|
369
|
+
if not _is_optional_approval_runtime_error(exc):
|
|
370
|
+
raise
|
|
371
|
+
return self._runtime_error_as_auxiliary_result(
|
|
372
|
+
exc,
|
|
373
|
+
error_code="TASK_TRANSFER_CANDIDATES_UNAVAILABLE",
|
|
374
|
+
selection=selection,
|
|
375
|
+
fallback_hint="Transfer candidates are unavailable in this permission context; use task get for current context or retry with a valid actionable node.",
|
|
376
|
+
)
|
|
343
377
|
original_items = _approval_page_items(raw.get("page"))
|
|
344
378
|
items = self._filter_self_transfer_candidates(profile=profile, items=original_items)
|
|
345
379
|
filtered_count = max(len(original_items) - len(items), 0)
|
|
@@ -353,7 +387,7 @@ class ApprovalTools(ToolBase):
|
|
|
353
387
|
"page_amount": _approval_page_amount(raw.get("page")),
|
|
354
388
|
"reported_total": max(_approval_page_total(raw.get("page")) - filtered_count, 0),
|
|
355
389
|
},
|
|
356
|
-
selection=
|
|
390
|
+
selection=selection,
|
|
357
391
|
)
|
|
358
392
|
|
|
359
393
|
@tool_cn_name("新增评论")
|
|
@@ -763,9 +797,18 @@ class ApprovalTools(ToolBase):
|
|
|
763
797
|
node_id = self._extract_node_id(body)
|
|
764
798
|
body["nodeId"] = self._resolve_actionable_node_id(context, app_key, apply_id, node_id)
|
|
765
799
|
body["applyId"] = self._match_or_fill_int(body, field_name="applyId", expected_value=apply_id)
|
|
766
|
-
|
|
800
|
+
current_detail: dict[str, Any] | None = None
|
|
801
|
+
if body.get("answers") is None or body.get("formId") is None:
|
|
802
|
+
current_detail = self._fetch_current_todo_detail(context, app_key, apply_id, body["nodeId"])
|
|
803
|
+
body["formId"] = self._resolve_form_id(
|
|
804
|
+
profile,
|
|
805
|
+
context,
|
|
806
|
+
app_key,
|
|
807
|
+
explicit_form_id=body.get("formId"),
|
|
808
|
+
current_detail=current_detail,
|
|
809
|
+
)
|
|
767
810
|
if body.get("answers") is None:
|
|
768
|
-
body["answers"] = self.
|
|
811
|
+
body["answers"] = self._extract_current_todo_answers(current_detail, apply_id=apply_id, node_id=body["nodeId"])
|
|
769
812
|
|
|
770
813
|
self._validate_approval_payload(body)
|
|
771
814
|
return body
|
|
@@ -782,12 +825,27 @@ class ApprovalTools(ToolBase):
|
|
|
782
825
|
raise_tool_error(QingflowApiError.config_error("payload.nodeId or payload.auditNodeId must be a positive integer"))
|
|
783
826
|
return node_id
|
|
784
827
|
|
|
785
|
-
def _resolve_form_id(
|
|
828
|
+
def _resolve_form_id(
|
|
829
|
+
self,
|
|
830
|
+
profile: str,
|
|
831
|
+
context,
|
|
832
|
+
app_key: str,
|
|
833
|
+
*,
|
|
834
|
+
explicit_form_id: Any | None,
|
|
835
|
+
current_detail: dict[str, Any] | None = None,
|
|
836
|
+
) -> int: # type: ignore[no-untyped-def]
|
|
786
837
|
"""执行内部辅助逻辑。"""
|
|
838
|
+
detail_form_id = self._extract_form_id_from_current_detail(current_detail)
|
|
787
839
|
if explicit_form_id is not None:
|
|
788
840
|
if not isinstance(explicit_form_id, int) or explicit_form_id <= 0:
|
|
789
841
|
raise_tool_error(QingflowApiError.config_error("payload.formId must be a positive integer"))
|
|
790
|
-
|
|
842
|
+
try:
|
|
843
|
+
form_id = detail_form_id or self._get_form_id(profile, context, app_key)
|
|
844
|
+
except QingflowApiError as exc:
|
|
845
|
+
if not _is_optional_approval_precheck_error(exc):
|
|
846
|
+
raise
|
|
847
|
+
self._form_id_cache[f"{profile}:{app_key}"] = explicit_form_id
|
|
848
|
+
return explicit_form_id
|
|
791
849
|
if form_id != explicit_form_id:
|
|
792
850
|
raise_tool_error(
|
|
793
851
|
QingflowApiError.config_error(
|
|
@@ -795,6 +853,9 @@ class ApprovalTools(ToolBase):
|
|
|
795
853
|
)
|
|
796
854
|
)
|
|
797
855
|
return explicit_form_id
|
|
856
|
+
if detail_form_id is not None:
|
|
857
|
+
self._form_id_cache[f"{profile}:{app_key}"] = detail_form_id
|
|
858
|
+
return detail_form_id
|
|
798
859
|
return self._get_form_id(profile, context, app_key)
|
|
799
860
|
|
|
800
861
|
def _get_form_id(self, profile: str, context, app_key: str) -> int: # type: ignore[no-untyped-def]
|
|
@@ -831,12 +892,18 @@ class ApprovalTools(ToolBase):
|
|
|
831
892
|
|
|
832
893
|
def _resolve_actionable_node_id(self, context, app_key: str, apply_id: int, node_id: int) -> int: # type: ignore[no-untyped-def]
|
|
833
894
|
"""执行内部辅助逻辑。"""
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
895
|
+
try:
|
|
896
|
+
infos = self.backend.request(
|
|
897
|
+
"GET",
|
|
898
|
+
context,
|
|
899
|
+
f"/app/{app_key}/apply/{apply_id}/auditInfo",
|
|
900
|
+
params={"type": 1},
|
|
901
|
+
)
|
|
902
|
+
except QingflowApiError as exc:
|
|
903
|
+
if not _is_optional_approval_precheck_error(exc):
|
|
904
|
+
raise
|
|
905
|
+
self._fetch_current_todo_detail(context, app_key, apply_id, node_id)
|
|
906
|
+
return node_id
|
|
840
907
|
if not isinstance(infos, list) or not infos:
|
|
841
908
|
raise_tool_error(
|
|
842
909
|
QingflowApiError.config_error(
|
|
@@ -858,7 +925,7 @@ class ApprovalTools(ToolBase):
|
|
|
858
925
|
)
|
|
859
926
|
return node_id
|
|
860
927
|
|
|
861
|
-
def
|
|
928
|
+
def _fetch_current_todo_detail(self, context, app_key: str, apply_id: int, node_id: int) -> dict[str, Any]: # type: ignore[no-untyped-def]
|
|
862
929
|
"""执行内部辅助逻辑。"""
|
|
863
930
|
detail = self.backend.request(
|
|
864
931
|
"GET",
|
|
@@ -866,6 +933,21 @@ class ApprovalTools(ToolBase):
|
|
|
866
933
|
f"/app/{app_key}/apply/{apply_id}",
|
|
867
934
|
params={"role": 3, "listType": 1, "auditNodeId": node_id},
|
|
868
935
|
)
|
|
936
|
+
if not isinstance(detail, dict):
|
|
937
|
+
raise_tool_error(
|
|
938
|
+
QingflowApiError.config_error(
|
|
939
|
+
f"cannot resolve current todo detail for apply_id={apply_id} nodeId={node_id}"
|
|
940
|
+
)
|
|
941
|
+
)
|
|
942
|
+
return detail
|
|
943
|
+
|
|
944
|
+
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]
|
|
945
|
+
"""执行内部辅助逻辑。"""
|
|
946
|
+
detail = self._fetch_current_todo_detail(context, app_key, apply_id, node_id)
|
|
947
|
+
return self._extract_current_todo_answers(detail, apply_id=apply_id, node_id=node_id)
|
|
948
|
+
|
|
949
|
+
def _extract_current_todo_answers(self, detail: dict[str, Any] | None, *, apply_id: int, node_id: int) -> list[dict[str, Any]]:
|
|
950
|
+
"""执行内部辅助逻辑。"""
|
|
869
951
|
answers = detail.get("answers") if isinstance(detail, dict) else None
|
|
870
952
|
if not isinstance(answers, list):
|
|
871
953
|
raise_tool_error(
|
|
@@ -879,6 +961,21 @@ class ApprovalTools(ToolBase):
|
|
|
879
961
|
normalized_answers.append(dict(item))
|
|
880
962
|
return normalized_answers
|
|
881
963
|
|
|
964
|
+
def _extract_form_id_from_current_detail(self, detail: dict[str, Any] | None) -> int | None:
|
|
965
|
+
"""执行内部辅助逻辑。"""
|
|
966
|
+
if not isinstance(detail, dict):
|
|
967
|
+
return None
|
|
968
|
+
for key in ("formId", "form_id"):
|
|
969
|
+
value = detail.get(key)
|
|
970
|
+
if isinstance(value, int) and value > 0:
|
|
971
|
+
return value
|
|
972
|
+
form = detail.get("form")
|
|
973
|
+
if isinstance(form, dict):
|
|
974
|
+
value = form.get("formId") or form.get("form_id")
|
|
975
|
+
if isinstance(value, int) and value > 0:
|
|
976
|
+
return value
|
|
977
|
+
return None
|
|
978
|
+
|
|
882
979
|
def _validate_approval_payload(self, payload: dict[str, Any]) -> None:
|
|
883
980
|
"""执行内部辅助逻辑。"""
|
|
884
981
|
self._reject_unsupported_fields(payload)
|
|
@@ -958,7 +1055,7 @@ class ApprovalTools(ToolBase):
|
|
|
958
1055
|
if node_id is None:
|
|
959
1056
|
node_payload = dict(payload or {})
|
|
960
1057
|
node_id = self._extract_node_id(node_payload)
|
|
961
|
-
delegated = TaskContextTools(self.sessions, self.backend).
|
|
1058
|
+
delegated = TaskContextTools(self.sessions, self.backend)._task_action_execute_with_locator(
|
|
962
1059
|
profile=profile,
|
|
963
1060
|
app_key=app_key,
|
|
964
1061
|
record_id=record_id,
|
|
@@ -995,6 +1092,48 @@ class ApprovalTools(ToolBase):
|
|
|
995
1092
|
}
|
|
996
1093
|
return response
|
|
997
1094
|
|
|
1095
|
+
def _runtime_error_as_auxiliary_result(
|
|
1096
|
+
self,
|
|
1097
|
+
error: RuntimeError,
|
|
1098
|
+
*,
|
|
1099
|
+
error_code: str,
|
|
1100
|
+
selection: dict[str, Any],
|
|
1101
|
+
fallback_hint: str,
|
|
1102
|
+
) -> dict[str, Any]:
|
|
1103
|
+
"""Return a structured failure for optional approval/comment helpers."""
|
|
1104
|
+
try:
|
|
1105
|
+
payload = json.loads(str(error))
|
|
1106
|
+
except json.JSONDecodeError:
|
|
1107
|
+
payload = {"message": str(error)}
|
|
1108
|
+
details = payload.get("details") if isinstance(payload.get("details"), dict) else {}
|
|
1109
|
+
warning: dict[str, Any] = {
|
|
1110
|
+
"code": error_code,
|
|
1111
|
+
"message": fallback_hint,
|
|
1112
|
+
}
|
|
1113
|
+
for key in ("category", "backend_code", "request_id", "http_status"):
|
|
1114
|
+
if payload.get(key) is not None:
|
|
1115
|
+
warning[key] = payload.get(key)
|
|
1116
|
+
response: dict[str, Any] = {
|
|
1117
|
+
"ok": False,
|
|
1118
|
+
"status": "failed",
|
|
1119
|
+
"error_code": details.get("error_code") or error_code,
|
|
1120
|
+
"message": payload.get("message") or str(error),
|
|
1121
|
+
"warnings": [warning],
|
|
1122
|
+
"output_profile": "normal",
|
|
1123
|
+
"data": {
|
|
1124
|
+
"items": [],
|
|
1125
|
+
"pagination": {"returned_items": 0},
|
|
1126
|
+
"selection": selection,
|
|
1127
|
+
"fallback_hint": fallback_hint,
|
|
1128
|
+
},
|
|
1129
|
+
}
|
|
1130
|
+
for key in ("category", "backend_code", "request_id", "http_status"):
|
|
1131
|
+
if payload.get(key) is not None:
|
|
1132
|
+
response[key] = payload.get(key)
|
|
1133
|
+
if details:
|
|
1134
|
+
response["details"] = details
|
|
1135
|
+
return response
|
|
1136
|
+
|
|
998
1137
|
def _public_action_response(
|
|
999
1138
|
self,
|
|
1000
1139
|
raw: dict[str, Any],
|
|
@@ -1060,3 +1199,26 @@ def _approval_page_total(payload: Any) -> Any:
|
|
|
1060
1199
|
if isinstance(payload, dict):
|
|
1061
1200
|
return payload.get("total", payload.get("count"))
|
|
1062
1201
|
return None
|
|
1202
|
+
|
|
1203
|
+
|
|
1204
|
+
def _is_optional_approval_precheck_error(error: QingflowApiError) -> bool:
|
|
1205
|
+
if is_auth_like_error(error):
|
|
1206
|
+
return False
|
|
1207
|
+
backend_code = backend_code_int(error)
|
|
1208
|
+
return backend_code in {40002, 40027, 404} or error.http_status == 404
|
|
1209
|
+
|
|
1210
|
+
|
|
1211
|
+
def _is_optional_approval_runtime_error(error: RuntimeError) -> bool:
|
|
1212
|
+
try:
|
|
1213
|
+
payload = json.loads(str(error))
|
|
1214
|
+
except json.JSONDecodeError:
|
|
1215
|
+
return False
|
|
1216
|
+
if not isinstance(payload, dict):
|
|
1217
|
+
return False
|
|
1218
|
+
if str(payload.get("category") or "").strip().lower() == "auth":
|
|
1219
|
+
return False
|
|
1220
|
+
if message_looks_like_invalid_token(payload.get("message")):
|
|
1221
|
+
return False
|
|
1222
|
+
if backend_code_value_int(payload.get("http_status")) == 401:
|
|
1223
|
+
return False
|
|
1224
|
+
return backend_code_value_int(payload.get("backend_code")) in {40002, 40027, 404} or backend_code_value_int(payload.get("http_status")) == 404
|
|
@@ -14,7 +14,7 @@ from ..config import (
|
|
|
14
14
|
get_mcporter_config_path,
|
|
15
15
|
normalize_base_url,
|
|
16
16
|
)
|
|
17
|
-
from ..errors import QingflowApiError, raise_tool_error
|
|
17
|
+
from ..errors import QingflowApiError, backend_code_int, is_auth_like_error, raise_tool_error
|
|
18
18
|
from ..session_store import SessionStore
|
|
19
19
|
from .base import ToolBase, tool_cn_name
|
|
20
20
|
|
|
@@ -650,7 +650,9 @@ class AuthTools(ToolBase):
|
|
|
650
650
|
qf_version=qf_version,
|
|
651
651
|
qf_version_source=qf_version_source,
|
|
652
652
|
)
|
|
653
|
-
except QingflowApiError:
|
|
653
|
+
except QingflowApiError as exc:
|
|
654
|
+
if is_auth_like_error(exc):
|
|
655
|
+
raise
|
|
654
656
|
return None, None
|
|
655
657
|
|
|
656
658
|
def _fetch_first_workspace(
|
|
@@ -765,7 +767,9 @@ class AuthTools(ToolBase):
|
|
|
765
767
|
qf_version=session_profile.qf_version,
|
|
766
768
|
qf_version_source=session_profile.qf_version_source,
|
|
767
769
|
)
|
|
768
|
-
except QingflowApiError:
|
|
770
|
+
except QingflowApiError as exc:
|
|
771
|
+
if is_auth_like_error(exc):
|
|
772
|
+
raise
|
|
769
773
|
return None
|
|
770
774
|
|
|
771
775
|
ws_name = session_profile.selected_ws_name
|
|
@@ -823,6 +827,18 @@ class AuthTools(ToolBase):
|
|
|
823
827
|
)
|
|
824
828
|
payload = dict(default_payload)
|
|
825
829
|
payload["permission_level"] = permission_level
|
|
830
|
+
warnings: list[dict[str, Any]] = []
|
|
831
|
+
if permission_level is None:
|
|
832
|
+
warnings.append(
|
|
833
|
+
{
|
|
834
|
+
"code": "WORKSPACE_PERMISSION_LEVEL_UNAVAILABLE",
|
|
835
|
+
"message": (
|
|
836
|
+
"auth_whoami could not resolve the selected workspace permission level; "
|
|
837
|
+
"do not infer the user is a basic member or an administrator from this null value."
|
|
838
|
+
),
|
|
839
|
+
"ws_id": ws_id,
|
|
840
|
+
}
|
|
841
|
+
)
|
|
826
842
|
|
|
827
843
|
context = BackendRequestContext(
|
|
828
844
|
base_url=backend_session.base_url,
|
|
@@ -831,14 +847,17 @@ class AuthTools(ToolBase):
|
|
|
831
847
|
qf_version=backend_session.qf_version,
|
|
832
848
|
qf_version_source=backend_session.qf_version_source,
|
|
833
849
|
)
|
|
850
|
+
member_lookup_warnings: list[dict[str, Any]] = []
|
|
834
851
|
member = self._lookup_current_member(
|
|
835
852
|
context=context,
|
|
836
853
|
uid=session_profile.uid,
|
|
837
854
|
email=session_profile.email,
|
|
838
855
|
nick_name=session_profile.nick_name,
|
|
856
|
+
warnings=member_lookup_warnings,
|
|
839
857
|
)
|
|
858
|
+
warnings.extend(member_lookup_warnings)
|
|
840
859
|
if member is None:
|
|
841
|
-
|
|
860
|
+
warnings.append(
|
|
842
861
|
{
|
|
843
862
|
"code": "CURRENT_MEMBER_PROFILE_UNAVAILABLE",
|
|
844
863
|
"message": (
|
|
@@ -846,11 +865,12 @@ class AuthTools(ToolBase):
|
|
|
846
865
|
f"in workspace {ws_id}."
|
|
847
866
|
),
|
|
848
867
|
}
|
|
849
|
-
|
|
868
|
+
)
|
|
869
|
+
return payload, warnings
|
|
850
870
|
|
|
851
871
|
payload["departments"] = self._compact_departments(member)
|
|
852
872
|
payload["roles"] = self._compact_roles(member)
|
|
853
|
-
return payload,
|
|
873
|
+
return payload, warnings
|
|
854
874
|
|
|
855
875
|
def _workspace_permission_level(
|
|
856
876
|
self,
|
|
@@ -882,7 +902,9 @@ class AuthTools(ToolBase):
|
|
|
882
902
|
"""执行内部辅助逻辑。"""
|
|
883
903
|
try:
|
|
884
904
|
workspace = self.backend.request("GET", context, f"/user/workspace/{ws_id}")
|
|
885
|
-
except QingflowApiError:
|
|
905
|
+
except QingflowApiError as exc:
|
|
906
|
+
if not _is_optional_auth_lookup_error(exc):
|
|
907
|
+
raise
|
|
886
908
|
return None
|
|
887
909
|
if not isinstance(workspace, dict):
|
|
888
910
|
return None
|
|
@@ -897,7 +919,9 @@ class AuthTools(ToolBase):
|
|
|
897
919
|
"/user/workspaceList/pageQuery",
|
|
898
920
|
json_body={"pageNum": 1, "pageSize": 100, "authList": [0, 1, 2, 3]},
|
|
899
921
|
)
|
|
900
|
-
except QingflowApiError:
|
|
922
|
+
except QingflowApiError as exc:
|
|
923
|
+
if not _is_optional_auth_lookup_error(exc):
|
|
924
|
+
raise
|
|
901
925
|
return None
|
|
902
926
|
workspaces = payload.get("list") if isinstance(payload, dict) else []
|
|
903
927
|
if not isinstance(workspaces, list):
|
|
@@ -915,20 +939,21 @@ class AuthTools(ToolBase):
|
|
|
915
939
|
uid: int | None,
|
|
916
940
|
email: str | None,
|
|
917
941
|
nick_name: str | None,
|
|
942
|
+
warnings: list[dict[str, Any]] | None = None,
|
|
918
943
|
) -> dict[str, Any] | None:
|
|
919
944
|
"""执行内部辅助逻辑。"""
|
|
920
945
|
candidates: list[dict[str, Any]] = []
|
|
921
946
|
for keyword in (email, nick_name):
|
|
922
|
-
member = self._search_member_once(context, uid=uid, keyword=keyword)
|
|
947
|
+
member = self._search_member_once(context, uid=uid, keyword=keyword, warnings=warnings)
|
|
923
948
|
if member is not None:
|
|
924
949
|
return member
|
|
925
950
|
if keyword:
|
|
926
|
-
candidates.extend(self._search_member_items(context, keyword=keyword))
|
|
951
|
+
candidates.extend(self._search_member_items(context, keyword=keyword, warnings=warnings))
|
|
927
952
|
if uid is not None and uid > 0:
|
|
928
953
|
for item in candidates:
|
|
929
954
|
if self._same_member(item, uid=uid):
|
|
930
955
|
return item
|
|
931
|
-
return self._search_member_once(context, uid=uid, keyword=None)
|
|
956
|
+
return self._search_member_once(context, uid=uid, keyword=None, warnings=warnings)
|
|
932
957
|
return None
|
|
933
958
|
|
|
934
959
|
def _search_member_once(
|
|
@@ -937,14 +962,21 @@ class AuthTools(ToolBase):
|
|
|
937
962
|
*,
|
|
938
963
|
uid: int | None,
|
|
939
964
|
keyword: str | None,
|
|
965
|
+
warnings: list[dict[str, Any]] | None = None,
|
|
940
966
|
) -> dict[str, Any] | None:
|
|
941
967
|
"""执行内部辅助逻辑。"""
|
|
942
|
-
for item in self._search_member_items(context, keyword=keyword):
|
|
968
|
+
for item in self._search_member_items(context, keyword=keyword, warnings=warnings):
|
|
943
969
|
if self._same_member(item, uid=uid):
|
|
944
970
|
return item
|
|
945
971
|
return None
|
|
946
972
|
|
|
947
|
-
def _search_member_items(
|
|
973
|
+
def _search_member_items(
|
|
974
|
+
self,
|
|
975
|
+
context: BackendRequestContext,
|
|
976
|
+
*,
|
|
977
|
+
keyword: str | None,
|
|
978
|
+
warnings: list[dict[str, Any]] | None = None,
|
|
979
|
+
) -> list[dict[str, Any]]:
|
|
948
980
|
"""执行内部辅助逻辑。"""
|
|
949
981
|
params: dict[str, Any] = {"pageNum": 1, "pageSize": 100, "containDisable": True}
|
|
950
982
|
normalized_keyword = str(keyword or "").strip()
|
|
@@ -952,11 +984,38 @@ class AuthTools(ToolBase):
|
|
|
952
984
|
params["keyword"] = normalized_keyword
|
|
953
985
|
try:
|
|
954
986
|
payload = self.backend.request("GET", context, "/contact", params=params)
|
|
955
|
-
except QingflowApiError:
|
|
987
|
+
except QingflowApiError as exc:
|
|
988
|
+
if not _is_optional_auth_lookup_error(exc):
|
|
989
|
+
raise
|
|
990
|
+
if warnings is not None and _is_contact_directory_permission_denied(exc):
|
|
991
|
+
self._append_unique_warning(warnings, self._contact_directory_permission_warning(exc))
|
|
956
992
|
return []
|
|
957
993
|
items = self._extract_items(payload)
|
|
958
994
|
return [item for item in items if isinstance(item, dict)]
|
|
959
995
|
|
|
996
|
+
def _append_unique_warning(self, warnings: list[dict[str, Any]], warning: dict[str, Any]) -> None:
|
|
997
|
+
"""执行内部辅助逻辑。"""
|
|
998
|
+
code = self._normalize_text(warning.get("code"))
|
|
999
|
+
if code is not None and any(item.get("code") == code for item in warnings):
|
|
1000
|
+
return
|
|
1001
|
+
warnings.append(warning)
|
|
1002
|
+
|
|
1003
|
+
def _contact_directory_permission_warning(self, error: QingflowApiError) -> dict[str, Any]:
|
|
1004
|
+
"""执行内部辅助逻辑。"""
|
|
1005
|
+
warning: dict[str, Any] = {
|
|
1006
|
+
"code": "CONTACT_DIRECTORY_PERMISSION_DENIED",
|
|
1007
|
+
"message": (
|
|
1008
|
+
"auth_whoami could not read current member departments and roles because "
|
|
1009
|
+
"the contact directory is not readable in this permission context; "
|
|
1010
|
+
"permission_level still comes from the workspace auth route."
|
|
1011
|
+
),
|
|
1012
|
+
"category": error.category,
|
|
1013
|
+
"backend_code": backend_code_int(error),
|
|
1014
|
+
"http_status": error.http_status,
|
|
1015
|
+
"request_id": error.request_id,
|
|
1016
|
+
}
|
|
1017
|
+
return {key: value for key, value in warning.items() if value is not None}
|
|
1018
|
+
|
|
960
1019
|
def _same_member(self, item: dict[str, Any], *, uid: int | None) -> bool:
|
|
961
1020
|
"""执行内部辅助逻辑。"""
|
|
962
1021
|
if uid is None or uid <= 0:
|
|
@@ -1105,7 +1164,9 @@ class AuthTools(ToolBase):
|
|
|
1105
1164
|
qf_version=qf_version,
|
|
1106
1165
|
qf_version_source=qf_version_source,
|
|
1107
1166
|
)
|
|
1108
|
-
except QingflowApiError:
|
|
1167
|
+
except QingflowApiError as exc:
|
|
1168
|
+
if is_auth_like_error(exc):
|
|
1169
|
+
raise
|
|
1109
1170
|
workspace = None
|
|
1110
1171
|
if isinstance(workspace, dict):
|
|
1111
1172
|
workspace_name = str(workspace.get("workspaceName") or workspace.get("wsName") or "").strip()
|
|
@@ -1119,7 +1180,9 @@ class AuthTools(ToolBase):
|
|
|
1119
1180
|
qf_version=qf_version,
|
|
1120
1181
|
qf_version_source=qf_version_source,
|
|
1121
1182
|
)
|
|
1122
|
-
except QingflowApiError:
|
|
1183
|
+
except QingflowApiError as exc:
|
|
1184
|
+
if is_auth_like_error(exc):
|
|
1185
|
+
raise
|
|
1123
1186
|
fallback = None
|
|
1124
1187
|
return fallback or workspace
|
|
1125
1188
|
|
|
@@ -1157,3 +1220,16 @@ class AuthTools(ToolBase):
|
|
|
1157
1220
|
None,
|
|
1158
1221
|
)
|
|
1159
1222
|
return found if isinstance(found, dict) else None
|
|
1223
|
+
|
|
1224
|
+
|
|
1225
|
+
def _is_contact_directory_permission_denied(error: QingflowApiError) -> bool:
|
|
1226
|
+
if is_auth_like_error(error):
|
|
1227
|
+
return False
|
|
1228
|
+
return backend_code_int(error) in {40002, 40027}
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
def _is_optional_auth_lookup_error(error: QingflowApiError) -> bool:
|
|
1232
|
+
if is_auth_like_error(error):
|
|
1233
|
+
return False
|
|
1234
|
+
backend_code = backend_code_int(error)
|
|
1235
|
+
return backend_code in {40002, 40027, 404} or error.http_status == 404
|