@josephyan/qingflow-app-builder-mcp 1.1.25 → 1.1.26
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 +5 -6
- package/docs/local-agent-install.md +6 -57
- package/entry_point.py +1 -1
- package/npm/bin/qingflow-app-builder-mcp.mjs +33 -2
- package/npm/lib/runtime.mjs +101 -21
- package/npm/scripts/postinstall.mjs +10 -1
- package/package.json +2 -3
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +42 -104
- package/skills/qingflow-app-builder/references/build-complete-system.md +428 -0
- package/skills/qingflow-app-builder/references/build-single-app.md +530 -0
- package/skills/qingflow-app-builder/references/create-app.md +11 -30
- package/skills/qingflow-app-builder/references/environments.md +1 -1
- package/skills/qingflow-app-builder/references/gotchas.md +9 -31
- package/skills/qingflow-app-builder/references/match-rules.md +0 -15
- package/skills/qingflow-app-builder/references/solution-playbooks.md +0 -10
- package/skills/qingflow-app-builder/references/tool-selection.md +23 -38
- package/skills/qingflow-app-builder/references/update-flow.md +38 -22
- package/skills/qingflow-app-builder/references/update-schema.md +0 -14
- package/skills/qingflow-app-builder/references/update-views.md +14 -83
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +3 -5
- package/skills/qingflow-app-builder-code-integrations/references/code-block.md +1 -1
- package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +1 -1
- package/skills/qingflow-mcp-setup/SKILL.md +111 -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 +61 -0
- package/skills/qingflow-mcp-setup/references/generic-stdio.md +31 -0
- package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
- package/skills/qingflow-workflow-builder/SKILL.md +96 -0
- package/skills/qingflow-workflow-builder/manifest.yaml +8 -0
- package/skills/qingflow-workflow-builder/references/01-overview.md +45 -0
- package/skills/qingflow-workflow-builder/references/02-update-mode.md +53 -0
- package/skills/qingflow-workflow-builder/references/03-flow-patterns.md +57 -0
- package/skills/qingflow-workflow-builder/references/04-stage1-business-modeling.md +131 -0
- package/skills/qingflow-workflow-builder/references/05-stage2-members-roles.md +29 -0
- package/skills/qingflow-workflow-builder/references/06-stage3-build-spec.md +165 -0
- package/skills/qingflow-workflow-builder/references/07-stage4-validate-spec.md +33 -0
- package/skills/qingflow-workflow-builder/references/08-stage5-apply-verify.md +51 -0
- package/skills/qingflow-workflow-builder/references/09-stage6-summary.md +88 -0
- package/skills/qingflow-workflow-builder/references/10-node-config-reference.md +93 -0
- package/skills/qingflow-workflow-builder/references/11-troubleshooting.md +15 -0
- package/skills/qingflow-workflow-builder/scripts/diff_flow_spec.py +275 -0
- package/skills/qingflow-workflow-builder/scripts/validate_flow_spec.py +605 -0
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/__main__.py +2 -6
- package/src/qingflow_mcp/builder_facade/models.py +108 -485
- package/src/qingflow_mcp/builder_facade/service.py +1686 -6674
- package/src/qingflow_mcp/cli/commands/builder.py +364 -322
- package/src/qingflow_mcp/cli/commands/chart.py +1 -1
- package/src/qingflow_mcp/cli/commands/common.py +3 -12
- 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 +27 -101
- package/src/qingflow_mcp/cli/commands/task.py +47 -28
- package/src/qingflow_mcp/cli/commands/view.py +1 -1
- package/src/qingflow_mcp/cli/context.py +3 -0
- package/src/qingflow_mcp/cli/formatters.py +16 -784
- package/src/qingflow_mcp/cli/main.py +33 -117
- package/src/qingflow_mcp/errors.py +2 -43
- package/src/qingflow_mcp/public_surface.py +17 -26
- package/src/qingflow_mcp/response_trim.py +17 -81
- package/src/qingflow_mcp/server.py +12 -14
- package/src/qingflow_mcp/server_app_builder.py +36 -79
- package/src/qingflow_mcp/server_app_user.py +16 -22
- package/src/qingflow_mcp/session_store.py +7 -11
- package/src/qingflow_mcp/solution/compiler/__init__.py +1 -3
- package/src/qingflow_mcp/solution/executor.py +18 -245
- package/src/qingflow_mcp/tools/ai_builder_tools.py +1396 -3103
- package/src/qingflow_mcp/tools/app_tools.py +43 -184
- package/src/qingflow_mcp/tools/approval_tools.py +35 -197
- package/src/qingflow_mcp/tools/auth_tools.py +16 -92
- package/src/qingflow_mcp/tools/code_block_tools.py +40 -298
- package/src/qingflow_mcp/tools/custom_button_tools.py +10 -64
- package/src/qingflow_mcp/tools/directory_tools.py +72 -236
- package/src/qingflow_mcp/tools/export_tools.py +34 -244
- package/src/qingflow_mcp/tools/feedback_tools.py +0 -9
- package/src/qingflow_mcp/tools/file_tools.py +3 -9
- package/src/qingflow_mcp/tools/import_tools.py +49 -336
- package/src/qingflow_mcp/tools/navigation_tools.py +12 -91
- package/src/qingflow_mcp/tools/package_tools.py +6 -118
- package/src/qingflow_mcp/tools/portal_tools.py +3 -39
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +7 -116
- package/src/qingflow_mcp/tools/record_tools.py +360 -1145
- package/src/qingflow_mcp/tools/resource_read_tools.py +39 -188
- package/src/qingflow_mcp/tools/role_tools.py +9 -80
- package/src/qingflow_mcp/tools/solution_tools.py +45 -59
- package/src/qingflow_mcp/tools/task_context_tools.py +158 -662
- package/src/qingflow_mcp/tools/task_tools.py +29 -113
- package/src/qingflow_mcp/tools/view_tools.py +3 -106
- package/src/qingflow_mcp/tools/workflow_tools.py +4 -48
- package/src/qingflow_mcp/tools/workspace_tools.py +3 -71
- package/npm/bin/qingflow-skills.mjs +0 -5
- package/skills/qingflow-app-builder/references/complete-system-development-guide.md +0 -172
- package/skills/qingflow-app-builder/references/single-app-development-guide.md +0 -87
- package/skills/qingflow-app-builder/scripts/validate_system_build_summary.py +0 -124
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +0 -173
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import json
|
|
4
3
|
from typing import Any
|
|
5
4
|
|
|
6
5
|
from mcp.server.fastmcp import FastMCP
|
|
7
6
|
|
|
8
7
|
from ..config import DEFAULT_PROFILE
|
|
9
|
-
from ..errors import QingflowApiError,
|
|
8
|
+
from ..errors import QingflowApiError, raise_tool_error
|
|
10
9
|
from ..json_types import JSONObject
|
|
11
10
|
from ..list_type_labels import get_record_list_type_label
|
|
12
11
|
from .base import ToolBase, tool_cn_name
|
|
@@ -173,26 +172,15 @@ class ApprovalTools(ToolBase):
|
|
|
173
172
|
keyword: str | None = None,
|
|
174
173
|
) -> dict[str, Any]:
|
|
175
174
|
"""执行记录相关逻辑。"""
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
)
|
|
175
|
+
raw = self.record_comment_mention_candidates(
|
|
176
|
+
profile=profile,
|
|
177
|
+
app_key=app_key,
|
|
178
|
+
apply_id=record_id,
|
|
179
|
+
page_size=page_size,
|
|
180
|
+
page_num=page_num,
|
|
181
|
+
list_type=list_type,
|
|
182
|
+
keyword=keyword,
|
|
183
|
+
)
|
|
196
184
|
items = _approval_page_items(raw.get("page"))
|
|
197
185
|
return self._public_page_response(
|
|
198
186
|
raw,
|
|
@@ -204,7 +192,7 @@ class ApprovalTools(ToolBase):
|
|
|
204
192
|
"page_amount": _approval_page_amount(raw.get("page")),
|
|
205
193
|
"reported_total": _approval_page_total(raw.get("page")),
|
|
206
194
|
},
|
|
207
|
-
selection=
|
|
195
|
+
selection={"app_key": app_key, "record_id": record_id, "list_type": list_type, "keyword": keyword},
|
|
208
196
|
)
|
|
209
197
|
|
|
210
198
|
@tool_cn_name("任务通过")
|
|
@@ -254,24 +242,13 @@ class ApprovalTools(ToolBase):
|
|
|
254
242
|
@tool_cn_name("任务退回候选")
|
|
255
243
|
def task_rollback_candidates(self, *, profile: str, app_key: str, record_id: int, workflow_node_id: int) -> dict[str, Any]:
|
|
256
244
|
"""执行任务相关逻辑。"""
|
|
257
|
-
|
|
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
|
-
)
|
|
245
|
+
raw = self.record_rollback_candidates(profile=profile, app_key=app_key, apply_id=record_id, audit_node_id=workflow_node_id)
|
|
269
246
|
items = _approval_page_items(raw.get("result"))
|
|
270
247
|
return self._public_page_response(
|
|
271
248
|
raw,
|
|
272
249
|
items=items,
|
|
273
250
|
pagination={"returned_items": len(items)},
|
|
274
|
-
selection=
|
|
251
|
+
selection={"app_key": app_key, "record_id": record_id, "workflow_node_id": workflow_node_id},
|
|
275
252
|
)
|
|
276
253
|
|
|
277
254
|
@tool_cn_name("任务退回")
|
|
@@ -354,26 +331,15 @@ class ApprovalTools(ToolBase):
|
|
|
354
331
|
keyword: str | None = None,
|
|
355
332
|
) -> dict[str, Any]:
|
|
356
333
|
"""执行任务相关逻辑。"""
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
)
|
|
334
|
+
raw = self.record_transfer_candidates(
|
|
335
|
+
profile=profile,
|
|
336
|
+
app_key=app_key,
|
|
337
|
+
apply_id=record_id,
|
|
338
|
+
page_size=page_size,
|
|
339
|
+
page_num=page_num,
|
|
340
|
+
audit_node_id=workflow_node_id,
|
|
341
|
+
keyword=keyword,
|
|
342
|
+
)
|
|
377
343
|
original_items = _approval_page_items(raw.get("page"))
|
|
378
344
|
items = self._filter_self_transfer_candidates(profile=profile, items=original_items)
|
|
379
345
|
filtered_count = max(len(original_items) - len(items), 0)
|
|
@@ -387,7 +353,7 @@ class ApprovalTools(ToolBase):
|
|
|
387
353
|
"page_amount": _approval_page_amount(raw.get("page")),
|
|
388
354
|
"reported_total": max(_approval_page_total(raw.get("page")) - filtered_count, 0),
|
|
389
355
|
},
|
|
390
|
-
selection=
|
|
356
|
+
selection={"app_key": app_key, "record_id": record_id, "workflow_node_id": workflow_node_id, "keyword": keyword},
|
|
391
357
|
)
|
|
392
358
|
|
|
393
359
|
@tool_cn_name("新增评论")
|
|
@@ -797,18 +763,9 @@ class ApprovalTools(ToolBase):
|
|
|
797
763
|
node_id = self._extract_node_id(body)
|
|
798
764
|
body["nodeId"] = self._resolve_actionable_node_id(context, app_key, apply_id, node_id)
|
|
799
765
|
body["applyId"] = self._match_or_fill_int(body, field_name="applyId", expected_value=apply_id)
|
|
800
|
-
|
|
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
|
-
)
|
|
766
|
+
body["formId"] = self._resolve_form_id(profile, context, app_key, explicit_form_id=body.get("formId"))
|
|
810
767
|
if body.get("answers") is None:
|
|
811
|
-
body["answers"] = self.
|
|
768
|
+
body["answers"] = self._fetch_current_todo_answers(context, app_key, apply_id, body["nodeId"])
|
|
812
769
|
|
|
813
770
|
self._validate_approval_payload(body)
|
|
814
771
|
return body
|
|
@@ -825,27 +782,12 @@ class ApprovalTools(ToolBase):
|
|
|
825
782
|
raise_tool_error(QingflowApiError.config_error("payload.nodeId or payload.auditNodeId must be a positive integer"))
|
|
826
783
|
return node_id
|
|
827
784
|
|
|
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]
|
|
785
|
+
def _resolve_form_id(self, profile: str, context, app_key: str, *, explicit_form_id: Any | None) -> int: # type: ignore[no-untyped-def]
|
|
837
786
|
"""执行内部辅助逻辑。"""
|
|
838
|
-
detail_form_id = self._extract_form_id_from_current_detail(current_detail)
|
|
839
787
|
if explicit_form_id is not None:
|
|
840
788
|
if not isinstance(explicit_form_id, int) or explicit_form_id <= 0:
|
|
841
789
|
raise_tool_error(QingflowApiError.config_error("payload.formId must be a positive integer"))
|
|
842
|
-
|
|
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
|
|
790
|
+
form_id = self._get_form_id(profile, context, app_key)
|
|
849
791
|
if form_id != explicit_form_id:
|
|
850
792
|
raise_tool_error(
|
|
851
793
|
QingflowApiError.config_error(
|
|
@@ -853,9 +795,6 @@ class ApprovalTools(ToolBase):
|
|
|
853
795
|
)
|
|
854
796
|
)
|
|
855
797
|
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
|
|
859
798
|
return self._get_form_id(profile, context, app_key)
|
|
860
799
|
|
|
861
800
|
def _get_form_id(self, profile: str, context, app_key: str) -> int: # type: ignore[no-untyped-def]
|
|
@@ -892,18 +831,12 @@ class ApprovalTools(ToolBase):
|
|
|
892
831
|
|
|
893
832
|
def _resolve_actionable_node_id(self, context, app_key: str, apply_id: int, node_id: int) -> int: # type: ignore[no-untyped-def]
|
|
894
833
|
"""执行内部辅助逻辑。"""
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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
|
|
834
|
+
infos = self.backend.request(
|
|
835
|
+
"GET",
|
|
836
|
+
context,
|
|
837
|
+
f"/app/{app_key}/apply/{apply_id}/auditInfo",
|
|
838
|
+
params={"type": 1},
|
|
839
|
+
)
|
|
907
840
|
if not isinstance(infos, list) or not infos:
|
|
908
841
|
raise_tool_error(
|
|
909
842
|
QingflowApiError.config_error(
|
|
@@ -925,7 +858,7 @@ class ApprovalTools(ToolBase):
|
|
|
925
858
|
)
|
|
926
859
|
return node_id
|
|
927
860
|
|
|
928
|
-
def
|
|
861
|
+
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]
|
|
929
862
|
"""执行内部辅助逻辑。"""
|
|
930
863
|
detail = self.backend.request(
|
|
931
864
|
"GET",
|
|
@@ -933,21 +866,6 @@ class ApprovalTools(ToolBase):
|
|
|
933
866
|
f"/app/{app_key}/apply/{apply_id}",
|
|
934
867
|
params={"role": 3, "listType": 1, "auditNodeId": node_id},
|
|
935
868
|
)
|
|
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
|
-
"""执行内部辅助逻辑。"""
|
|
951
869
|
answers = detail.get("answers") if isinstance(detail, dict) else None
|
|
952
870
|
if not isinstance(answers, list):
|
|
953
871
|
raise_tool_error(
|
|
@@ -961,21 +879,6 @@ class ApprovalTools(ToolBase):
|
|
|
961
879
|
normalized_answers.append(dict(item))
|
|
962
880
|
return normalized_answers
|
|
963
881
|
|
|
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
|
-
|
|
979
882
|
def _validate_approval_payload(self, payload: dict[str, Any]) -> None:
|
|
980
883
|
"""执行内部辅助逻辑。"""
|
|
981
884
|
self._reject_unsupported_fields(payload)
|
|
@@ -1055,7 +958,7 @@ class ApprovalTools(ToolBase):
|
|
|
1055
958
|
if node_id is None:
|
|
1056
959
|
node_payload = dict(payload or {})
|
|
1057
960
|
node_id = self._extract_node_id(node_payload)
|
|
1058
|
-
delegated = TaskContextTools(self.sessions, self.backend).
|
|
961
|
+
delegated = TaskContextTools(self.sessions, self.backend).task_action_execute(
|
|
1059
962
|
profile=profile,
|
|
1060
963
|
app_key=app_key,
|
|
1061
964
|
record_id=record_id,
|
|
@@ -1092,48 +995,6 @@ class ApprovalTools(ToolBase):
|
|
|
1092
995
|
}
|
|
1093
996
|
return response
|
|
1094
997
|
|
|
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
|
-
|
|
1137
998
|
def _public_action_response(
|
|
1138
999
|
self,
|
|
1139
1000
|
raw: dict[str, Any],
|
|
@@ -1199,26 +1060,3 @@ def _approval_page_total(payload: Any) -> Any:
|
|
|
1199
1060
|
if isinstance(payload, dict):
|
|
1200
1061
|
return payload.get("total", payload.get("count"))
|
|
1201
1062
|
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,
|
|
17
|
+
from ..errors import QingflowApiError, raise_tool_error
|
|
18
18
|
from ..session_store import SessionStore
|
|
19
19
|
from .base import ToolBase, tool_cn_name
|
|
20
20
|
|
|
@@ -650,9 +650,7 @@ class AuthTools(ToolBase):
|
|
|
650
650
|
qf_version=qf_version,
|
|
651
651
|
qf_version_source=qf_version_source,
|
|
652
652
|
)
|
|
653
|
-
except QingflowApiError
|
|
654
|
-
if is_auth_like_error(exc):
|
|
655
|
-
raise
|
|
653
|
+
except QingflowApiError:
|
|
656
654
|
return None, None
|
|
657
655
|
|
|
658
656
|
def _fetch_first_workspace(
|
|
@@ -767,9 +765,7 @@ class AuthTools(ToolBase):
|
|
|
767
765
|
qf_version=session_profile.qf_version,
|
|
768
766
|
qf_version_source=session_profile.qf_version_source,
|
|
769
767
|
)
|
|
770
|
-
except QingflowApiError
|
|
771
|
-
if is_auth_like_error(exc):
|
|
772
|
-
raise
|
|
768
|
+
except QingflowApiError:
|
|
773
769
|
return None
|
|
774
770
|
|
|
775
771
|
ws_name = session_profile.selected_ws_name
|
|
@@ -827,18 +823,6 @@ class AuthTools(ToolBase):
|
|
|
827
823
|
)
|
|
828
824
|
payload = dict(default_payload)
|
|
829
825
|
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
|
-
)
|
|
842
826
|
|
|
843
827
|
context = BackendRequestContext(
|
|
844
828
|
base_url=backend_session.base_url,
|
|
@@ -847,17 +831,14 @@ class AuthTools(ToolBase):
|
|
|
847
831
|
qf_version=backend_session.qf_version,
|
|
848
832
|
qf_version_source=backend_session.qf_version_source,
|
|
849
833
|
)
|
|
850
|
-
member_lookup_warnings: list[dict[str, Any]] = []
|
|
851
834
|
member = self._lookup_current_member(
|
|
852
835
|
context=context,
|
|
853
836
|
uid=session_profile.uid,
|
|
854
837
|
email=session_profile.email,
|
|
855
838
|
nick_name=session_profile.nick_name,
|
|
856
|
-
warnings=member_lookup_warnings,
|
|
857
839
|
)
|
|
858
|
-
warnings.extend(member_lookup_warnings)
|
|
859
840
|
if member is None:
|
|
860
|
-
|
|
841
|
+
return payload, [
|
|
861
842
|
{
|
|
862
843
|
"code": "CURRENT_MEMBER_PROFILE_UNAVAILABLE",
|
|
863
844
|
"message": (
|
|
@@ -865,12 +846,11 @@ class AuthTools(ToolBase):
|
|
|
865
846
|
f"in workspace {ws_id}."
|
|
866
847
|
),
|
|
867
848
|
}
|
|
868
|
-
|
|
869
|
-
return payload, warnings
|
|
849
|
+
]
|
|
870
850
|
|
|
871
851
|
payload["departments"] = self._compact_departments(member)
|
|
872
852
|
payload["roles"] = self._compact_roles(member)
|
|
873
|
-
return payload,
|
|
853
|
+
return payload, []
|
|
874
854
|
|
|
875
855
|
def _workspace_permission_level(
|
|
876
856
|
self,
|
|
@@ -902,9 +882,7 @@ class AuthTools(ToolBase):
|
|
|
902
882
|
"""执行内部辅助逻辑。"""
|
|
903
883
|
try:
|
|
904
884
|
workspace = self.backend.request("GET", context, f"/user/workspace/{ws_id}")
|
|
905
|
-
except QingflowApiError
|
|
906
|
-
if not _is_optional_auth_lookup_error(exc):
|
|
907
|
-
raise
|
|
885
|
+
except QingflowApiError:
|
|
908
886
|
return None
|
|
909
887
|
if not isinstance(workspace, dict):
|
|
910
888
|
return None
|
|
@@ -919,9 +897,7 @@ class AuthTools(ToolBase):
|
|
|
919
897
|
"/user/workspaceList/pageQuery",
|
|
920
898
|
json_body={"pageNum": 1, "pageSize": 100, "authList": [0, 1, 2, 3]},
|
|
921
899
|
)
|
|
922
|
-
except QingflowApiError
|
|
923
|
-
if not _is_optional_auth_lookup_error(exc):
|
|
924
|
-
raise
|
|
900
|
+
except QingflowApiError:
|
|
925
901
|
return None
|
|
926
902
|
workspaces = payload.get("list") if isinstance(payload, dict) else []
|
|
927
903
|
if not isinstance(workspaces, list):
|
|
@@ -939,21 +915,20 @@ class AuthTools(ToolBase):
|
|
|
939
915
|
uid: int | None,
|
|
940
916
|
email: str | None,
|
|
941
917
|
nick_name: str | None,
|
|
942
|
-
warnings: list[dict[str, Any]] | None = None,
|
|
943
918
|
) -> dict[str, Any] | None:
|
|
944
919
|
"""执行内部辅助逻辑。"""
|
|
945
920
|
candidates: list[dict[str, Any]] = []
|
|
946
921
|
for keyword in (email, nick_name):
|
|
947
|
-
member = self._search_member_once(context, uid=uid, keyword=keyword
|
|
922
|
+
member = self._search_member_once(context, uid=uid, keyword=keyword)
|
|
948
923
|
if member is not None:
|
|
949
924
|
return member
|
|
950
925
|
if keyword:
|
|
951
|
-
candidates.extend(self._search_member_items(context, keyword=keyword
|
|
926
|
+
candidates.extend(self._search_member_items(context, keyword=keyword))
|
|
952
927
|
if uid is not None and uid > 0:
|
|
953
928
|
for item in candidates:
|
|
954
929
|
if self._same_member(item, uid=uid):
|
|
955
930
|
return item
|
|
956
|
-
return self._search_member_once(context, uid=uid, keyword=None
|
|
931
|
+
return self._search_member_once(context, uid=uid, keyword=None)
|
|
957
932
|
return None
|
|
958
933
|
|
|
959
934
|
def _search_member_once(
|
|
@@ -962,21 +937,14 @@ class AuthTools(ToolBase):
|
|
|
962
937
|
*,
|
|
963
938
|
uid: int | None,
|
|
964
939
|
keyword: str | None,
|
|
965
|
-
warnings: list[dict[str, Any]] | None = None,
|
|
966
940
|
) -> dict[str, Any] | None:
|
|
967
941
|
"""执行内部辅助逻辑。"""
|
|
968
|
-
for item in self._search_member_items(context, keyword=keyword
|
|
942
|
+
for item in self._search_member_items(context, keyword=keyword):
|
|
969
943
|
if self._same_member(item, uid=uid):
|
|
970
944
|
return item
|
|
971
945
|
return None
|
|
972
946
|
|
|
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]]:
|
|
947
|
+
def _search_member_items(self, context: BackendRequestContext, *, keyword: str | None) -> list[dict[str, Any]]:
|
|
980
948
|
"""执行内部辅助逻辑。"""
|
|
981
949
|
params: dict[str, Any] = {"pageNum": 1, "pageSize": 100, "containDisable": True}
|
|
982
950
|
normalized_keyword = str(keyword or "").strip()
|
|
@@ -984,38 +952,11 @@ class AuthTools(ToolBase):
|
|
|
984
952
|
params["keyword"] = normalized_keyword
|
|
985
953
|
try:
|
|
986
954
|
payload = self.backend.request("GET", context, "/contact", params=params)
|
|
987
|
-
except QingflowApiError
|
|
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))
|
|
955
|
+
except QingflowApiError:
|
|
992
956
|
return []
|
|
993
957
|
items = self._extract_items(payload)
|
|
994
958
|
return [item for item in items if isinstance(item, dict)]
|
|
995
959
|
|
|
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
|
-
|
|
1019
960
|
def _same_member(self, item: dict[str, Any], *, uid: int | None) -> bool:
|
|
1020
961
|
"""执行内部辅助逻辑。"""
|
|
1021
962
|
if uid is None or uid <= 0:
|
|
@@ -1164,9 +1105,7 @@ class AuthTools(ToolBase):
|
|
|
1164
1105
|
qf_version=qf_version,
|
|
1165
1106
|
qf_version_source=qf_version_source,
|
|
1166
1107
|
)
|
|
1167
|
-
except QingflowApiError
|
|
1168
|
-
if is_auth_like_error(exc):
|
|
1169
|
-
raise
|
|
1108
|
+
except QingflowApiError:
|
|
1170
1109
|
workspace = None
|
|
1171
1110
|
if isinstance(workspace, dict):
|
|
1172
1111
|
workspace_name = str(workspace.get("workspaceName") or workspace.get("wsName") or "").strip()
|
|
@@ -1180,9 +1119,7 @@ class AuthTools(ToolBase):
|
|
|
1180
1119
|
qf_version=qf_version,
|
|
1181
1120
|
qf_version_source=qf_version_source,
|
|
1182
1121
|
)
|
|
1183
|
-
except QingflowApiError
|
|
1184
|
-
if is_auth_like_error(exc):
|
|
1185
|
-
raise
|
|
1122
|
+
except QingflowApiError:
|
|
1186
1123
|
fallback = None
|
|
1187
1124
|
return fallback or workspace
|
|
1188
1125
|
|
|
@@ -1220,16 +1157,3 @@ class AuthTools(ToolBase):
|
|
|
1220
1157
|
None,
|
|
1221
1158
|
)
|
|
1222
1159
|
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
|