@josephyan/qingflow-cli 1.1.3 → 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 +287 -25
- package/src/qingflow_mcp/builder_facade/service.py +4195 -856
- package/src/qingflow_mcp/cli/commands/builder.py +316 -247
- 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 +1782 -399
- 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
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from copy import deepcopy
|
|
4
4
|
|
|
5
5
|
from ..config import DEFAULT_PROFILE
|
|
6
|
-
from ..errors import QingflowApiError, raise_tool_error
|
|
6
|
+
from ..errors import QingflowApiError, backend_code_int, is_auth_like_error, raise_tool_error
|
|
7
7
|
from ..json_types import JSONObject
|
|
8
8
|
from .base import ToolBase, tool_cn_name
|
|
9
9
|
|
|
@@ -31,12 +31,26 @@ class CustomButtonTools(ToolBase):
|
|
|
31
31
|
self._require_app_key(app_key)
|
|
32
32
|
|
|
33
33
|
def runner(session_profile, context):
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
effective_being_draft = being_draft
|
|
35
|
+
fallback_error: QingflowApiError | None = None
|
|
36
|
+
try:
|
|
37
|
+
result = self.backend.request(
|
|
38
|
+
"GET",
|
|
39
|
+
context,
|
|
40
|
+
f"/app/{app_key}/customButton",
|
|
41
|
+
params={"beingDraft": being_draft},
|
|
42
|
+
)
|
|
43
|
+
except QingflowApiError as exc:
|
|
44
|
+
if not being_draft or not _is_optional_draft_button_read_error(exc):
|
|
45
|
+
raise
|
|
46
|
+
fallback_error = exc
|
|
47
|
+
effective_being_draft = False
|
|
48
|
+
result = self.backend.request(
|
|
49
|
+
"GET",
|
|
50
|
+
context,
|
|
51
|
+
f"/app/{app_key}/customButton",
|
|
52
|
+
params={"beingDraft": False},
|
|
53
|
+
)
|
|
40
54
|
items = []
|
|
41
55
|
raw_items = result.get("result") if isinstance(result, dict) and isinstance(result.get("result"), list) else []
|
|
42
56
|
for item in raw_items:
|
|
@@ -47,11 +61,23 @@ class CustomButtonTools(ToolBase):
|
|
|
47
61
|
"profile": profile,
|
|
48
62
|
"ws_id": session_profile.selected_ws_id,
|
|
49
63
|
"app_key": app_key,
|
|
50
|
-
"being_draft":
|
|
64
|
+
"being_draft": effective_being_draft,
|
|
65
|
+
"requested_being_draft": being_draft,
|
|
51
66
|
"items": result if include_raw else items,
|
|
52
67
|
"count": len(items),
|
|
53
68
|
"compact": not include_raw,
|
|
54
69
|
}
|
|
70
|
+
if fallback_error is not None:
|
|
71
|
+
response["warnings"] = [
|
|
72
|
+
{
|
|
73
|
+
"code": "CUSTOM_BUTTON_DRAFT_LIST_UNAVAILABLE",
|
|
74
|
+
"message": "draft custom button list is unavailable; returned published custom buttons instead",
|
|
75
|
+
"backend_code": fallback_error.backend_code,
|
|
76
|
+
"http_status": fallback_error.http_status,
|
|
77
|
+
"request_id": fallback_error.request_id,
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
response["verification"] = {"draft_readable": False, "published_fallback_used": True}
|
|
55
81
|
if include_raw:
|
|
56
82
|
response["summary"] = items
|
|
57
83
|
return response
|
|
@@ -74,16 +100,37 @@ class CustomButtonTools(ToolBase):
|
|
|
74
100
|
|
|
75
101
|
def runner(session_profile, context):
|
|
76
102
|
params = {"beingDraft": being_draft}
|
|
77
|
-
|
|
103
|
+
effective_being_draft = being_draft
|
|
104
|
+
fallback_error: QingflowApiError | None = None
|
|
105
|
+
try:
|
|
106
|
+
result = self.backend.request("GET", context, f"/app/{app_key}/customButton/{button_id}", params=params)
|
|
107
|
+
except QingflowApiError as exc:
|
|
108
|
+
if not being_draft or not _is_optional_draft_button_read_error(exc):
|
|
109
|
+
raise
|
|
110
|
+
fallback_error = exc
|
|
111
|
+
effective_being_draft = False
|
|
112
|
+
result = self.backend.request("GET", context, f"/app/{app_key}/customButton/{button_id}", params={"beingDraft": False})
|
|
78
113
|
response = {
|
|
79
114
|
"profile": profile,
|
|
80
115
|
"ws_id": session_profile.selected_ws_id,
|
|
81
116
|
"app_key": app_key,
|
|
82
117
|
"button_id": button_id,
|
|
83
|
-
"being_draft":
|
|
118
|
+
"being_draft": effective_being_draft,
|
|
119
|
+
"requested_being_draft": being_draft,
|
|
84
120
|
"result": result if include_raw else self._compact_button_detail(result if isinstance(result, dict) else {}),
|
|
85
121
|
"compact": not include_raw,
|
|
86
122
|
}
|
|
123
|
+
if fallback_error is not None:
|
|
124
|
+
response["warnings"] = [
|
|
125
|
+
{
|
|
126
|
+
"code": "CUSTOM_BUTTON_DRAFT_DETAIL_UNAVAILABLE",
|
|
127
|
+
"message": "draft custom button detail is unavailable; returned published custom button detail instead",
|
|
128
|
+
"backend_code": fallback_error.backend_code,
|
|
129
|
+
"http_status": fallback_error.http_status,
|
|
130
|
+
"request_id": fallback_error.request_id,
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
response["verification"] = {"draft_readable": False, "published_fallback_used": True}
|
|
87
134
|
if include_raw:
|
|
88
135
|
response["summary"] = self._compact_button_detail(result if isinstance(result, dict) else {})
|
|
89
136
|
return response
|
|
@@ -198,3 +245,10 @@ class CustomButtonTools(ToolBase):
|
|
|
198
245
|
else None,
|
|
199
246
|
"trigger_wings_config": deepcopy(item.get("triggerWingsConfig")) if isinstance(item.get("triggerWingsConfig"), dict) else None,
|
|
200
247
|
}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _is_optional_draft_button_read_error(error: QingflowApiError) -> bool:
|
|
251
|
+
if is_auth_like_error(error):
|
|
252
|
+
return False
|
|
253
|
+
backend_code = backend_code_int(error)
|
|
254
|
+
return backend_code in {40002, 40027, 404} or error.http_status == 404
|
|
@@ -6,7 +6,7 @@ from typing import Any
|
|
|
6
6
|
from mcp.server.fastmcp import FastMCP
|
|
7
7
|
|
|
8
8
|
from ..config import DEFAULT_PROFILE
|
|
9
|
-
from ..errors import QingflowApiError, raise_tool_error
|
|
9
|
+
from ..errors import QingflowApiError, backend_code_int, is_auth_like_error, raise_tool_error
|
|
10
10
|
from .base import ToolBase, tool_cn_name
|
|
11
11
|
|
|
12
12
|
|
|
@@ -20,28 +20,17 @@ class DirectoryTools(ToolBase):
|
|
|
20
20
|
主要职责:
|
|
21
21
|
1. 搜索用户、部门、外部联系人;
|
|
22
22
|
2. 列举内部用户与部门层级;
|
|
23
|
-
3.
|
|
23
|
+
3. 为 builder 配置、审批处理或联系人确认提供目录侧数据来源。
|
|
24
|
+
|
|
25
|
+
注意:记录成员/部门字段候选应优先使用 record_member_candidates /
|
|
26
|
+
record_department_candidates,避免把普通记录读写误带入通讯录目录链路。
|
|
24
27
|
"""
|
|
25
28
|
|
|
26
29
|
def register(self, mcp: FastMCP) -> None:
|
|
27
30
|
"""注册当前工具到 MCP 服务。"""
|
|
28
|
-
|
|
29
|
-
def directory_search(
|
|
30
|
-
profile: str = DEFAULT_PROFILE,
|
|
31
|
-
query: str = "",
|
|
32
|
-
scopes: list[str] | None = None,
|
|
33
|
-
page_num: int = 1,
|
|
34
|
-
page_size: int = 20,
|
|
35
|
-
) -> dict[str, Any]:
|
|
36
|
-
return self.directory_search(
|
|
37
|
-
profile=profile,
|
|
38
|
-
query=query,
|
|
39
|
-
scopes=scopes,
|
|
40
|
-
page_num=page_num,
|
|
41
|
-
page_size=page_size,
|
|
42
|
-
)
|
|
31
|
+
self.register_frontend_search(mcp)
|
|
43
32
|
|
|
44
|
-
@mcp.tool()
|
|
33
|
+
@mcp.tool(description="List internal workspace members through the contact directory route. This is a directory/admin-style lookup, not the record field candidate path.")
|
|
45
34
|
def directory_list_internal_users(
|
|
46
35
|
profile: str = DEFAULT_PROFILE,
|
|
47
36
|
keyword: str | None = None,
|
|
@@ -61,7 +50,7 @@ class DirectoryTools(ToolBase):
|
|
|
61
50
|
contain_disable=include_disabled,
|
|
62
51
|
)
|
|
63
52
|
|
|
64
|
-
@mcp.tool()
|
|
53
|
+
@mcp.tool(description="Page through internal workspace members through the contact directory route. This is a directory/admin-style lookup, not the record field candidate path.")
|
|
65
54
|
def directory_list_all_internal_users(
|
|
66
55
|
profile: str = DEFAULT_PROFILE,
|
|
67
56
|
keyword: str | None = None,
|
|
@@ -81,7 +70,7 @@ class DirectoryTools(ToolBase):
|
|
|
81
70
|
max_pages=max_pages,
|
|
82
71
|
)
|
|
83
72
|
|
|
84
|
-
@mcp.tool()
|
|
73
|
+
@mcp.tool(description="List internal departments. With keyword, uses frontend directory search; without keyword, browses the contact department tree and may require contact-directory permission.")
|
|
85
74
|
def directory_list_internal_departments(
|
|
86
75
|
profile: str = DEFAULT_PROFILE,
|
|
87
76
|
keyword: str = "",
|
|
@@ -95,7 +84,7 @@ class DirectoryTools(ToolBase):
|
|
|
95
84
|
page_size=page_size,
|
|
96
85
|
)
|
|
97
86
|
|
|
98
|
-
@mcp.tool()
|
|
87
|
+
@mcp.tool(description="Walk the internal department tree through the contact directory route. This may require contact-directory permission and should not be used for record department field candidates.")
|
|
99
88
|
def directory_list_all_departments(
|
|
100
89
|
profile: str = DEFAULT_PROFILE,
|
|
101
90
|
parent_department_id: int | None = None,
|
|
@@ -109,20 +98,20 @@ class DirectoryTools(ToolBase):
|
|
|
109
98
|
max_items=max_items,
|
|
110
99
|
)
|
|
111
100
|
|
|
112
|
-
@mcp.tool()
|
|
101
|
+
@mcp.tool(description="List child departments through the contact directory route. This may require contact-directory permission and should not be used for record department field candidates.")
|
|
113
102
|
def directory_list_sub_departments(
|
|
114
103
|
profile: str = DEFAULT_PROFILE,
|
|
115
104
|
parent_department_id: int | None = None,
|
|
116
105
|
) -> dict[str, Any]:
|
|
117
106
|
return self.directory_list_sub_departments(profile=profile, parent_dept_id=parent_department_id)
|
|
118
107
|
|
|
119
|
-
@mcp.tool()
|
|
108
|
+
@mcp.tool(description="Search external contacts. Defaults to the frontend simple external-member route; set simple=false only for full external-contact management data, which may require contact permission.")
|
|
120
109
|
def directory_list_external_members(
|
|
121
110
|
profile: str = DEFAULT_PROFILE,
|
|
122
111
|
keyword: str | None = None,
|
|
123
112
|
page_num: int = 1,
|
|
124
113
|
page_size: int = 20,
|
|
125
|
-
simple: bool =
|
|
114
|
+
simple: bool = True,
|
|
126
115
|
) -> dict[str, Any]:
|
|
127
116
|
return self.directory_list_external_members(
|
|
128
117
|
profile=profile,
|
|
@@ -132,6 +121,24 @@ class DirectoryTools(ToolBase):
|
|
|
132
121
|
simple=simple,
|
|
133
122
|
)
|
|
134
123
|
|
|
124
|
+
def register_frontend_search(self, mcp: FastMCP) -> None:
|
|
125
|
+
"""Register only the member-visible frontend search route."""
|
|
126
|
+
@mcp.tool(description="Search internal workspace members/departments by keyword via the frontend directory search route. Do not use for record member/department field candidates; use record_member_candidates or record_department_candidates instead.")
|
|
127
|
+
def directory_search(
|
|
128
|
+
profile: str = DEFAULT_PROFILE,
|
|
129
|
+
query: str = "",
|
|
130
|
+
scopes: list[str] | None = None,
|
|
131
|
+
page_num: int = 1,
|
|
132
|
+
page_size: int = 20,
|
|
133
|
+
) -> dict[str, Any]:
|
|
134
|
+
return self.directory_search(
|
|
135
|
+
profile=profile,
|
|
136
|
+
query=query,
|
|
137
|
+
scopes=scopes,
|
|
138
|
+
page_num=page_num,
|
|
139
|
+
page_size=page_size,
|
|
140
|
+
)
|
|
141
|
+
|
|
135
142
|
@tool_cn_name("通讯录搜索")
|
|
136
143
|
def directory_search(
|
|
137
144
|
self,
|
|
@@ -151,17 +158,28 @@ class DirectoryTools(ToolBase):
|
|
|
151
158
|
raise_tool_error(QingflowApiError.config_error("query is required"))
|
|
152
159
|
|
|
153
160
|
def runner(session_profile, context):
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
161
|
+
try:
|
|
162
|
+
result = self.backend.request(
|
|
163
|
+
"POST",
|
|
164
|
+
context,
|
|
165
|
+
"/member/search",
|
|
166
|
+
json_body={
|
|
167
|
+
"dimensions": normalized_scopes,
|
|
168
|
+
"searchKey": query,
|
|
169
|
+
"pageNum": page_num,
|
|
170
|
+
"pageSize": page_size,
|
|
171
|
+
},
|
|
172
|
+
)
|
|
173
|
+
except QingflowApiError as exc:
|
|
174
|
+
if _is_directory_permission_denied(exc):
|
|
175
|
+
return self._directory_permission_denied_payload(
|
|
176
|
+
profile=profile,
|
|
177
|
+
session_profile=session_profile,
|
|
178
|
+
context=context,
|
|
179
|
+
error=exc,
|
|
180
|
+
operation="directory_search",
|
|
181
|
+
)
|
|
182
|
+
raise
|
|
165
183
|
return {
|
|
166
184
|
"profile": profile,
|
|
167
185
|
"ws_id": session_profile.selected_ws_id,
|
|
@@ -209,7 +227,18 @@ class DirectoryTools(ToolBase):
|
|
|
209
227
|
params["deptId"] = dept_id
|
|
210
228
|
if role_id is not None:
|
|
211
229
|
params["roleId"] = role_id
|
|
212
|
-
|
|
230
|
+
try:
|
|
231
|
+
result = self.backend.request("GET", context, "/contact", params=params)
|
|
232
|
+
except QingflowApiError as exc:
|
|
233
|
+
if _is_directory_permission_denied(exc):
|
|
234
|
+
return self._directory_permission_denied_payload(
|
|
235
|
+
profile=profile,
|
|
236
|
+
session_profile=session_profile,
|
|
237
|
+
context=context,
|
|
238
|
+
error=exc,
|
|
239
|
+
operation="directory_list_internal_users",
|
|
240
|
+
)
|
|
241
|
+
raise
|
|
213
242
|
return {
|
|
214
243
|
"profile": profile,
|
|
215
244
|
"ws_id": session_profile.selected_ws_id,
|
|
@@ -269,7 +298,18 @@ class DirectoryTools(ToolBase):
|
|
|
269
298
|
params["deptId"] = dept_id
|
|
270
299
|
if role_id is not None:
|
|
271
300
|
params["roleId"] = role_id
|
|
272
|
-
|
|
301
|
+
try:
|
|
302
|
+
result = self.backend.request("GET", context, "/contact", params=params)
|
|
303
|
+
except QingflowApiError as exc:
|
|
304
|
+
if _is_directory_permission_denied(exc):
|
|
305
|
+
return self._directory_permission_denied_payload(
|
|
306
|
+
profile=profile,
|
|
307
|
+
session_profile=session_profile,
|
|
308
|
+
context=context,
|
|
309
|
+
error=exc,
|
|
310
|
+
operation="directory_list_all_internal_users",
|
|
311
|
+
)
|
|
312
|
+
raise
|
|
273
313
|
page_items = _directory_items(result)
|
|
274
314
|
if reported_total is None:
|
|
275
315
|
reported_total = _coerce_int(_payload_value(result, "total"))
|
|
@@ -329,13 +369,24 @@ class DirectoryTools(ToolBase):
|
|
|
329
369
|
|
|
330
370
|
if not normalized_keyword:
|
|
331
371
|
def runner(session_profile, context):
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
372
|
+
try:
|
|
373
|
+
fetch_limit = max((page_num + 1) * page_size + 1, page_size + 1)
|
|
374
|
+
items, truncated, deepest_depth = self._walk_department_tree(
|
|
375
|
+
context,
|
|
376
|
+
parent_dept_id=None,
|
|
377
|
+
max_depth=20,
|
|
378
|
+
max_items=fetch_limit,
|
|
379
|
+
)
|
|
380
|
+
except QingflowApiError as exc:
|
|
381
|
+
if _is_directory_permission_denied(exc):
|
|
382
|
+
return self._directory_permission_denied_payload(
|
|
383
|
+
profile=profile,
|
|
384
|
+
session_profile=session_profile,
|
|
385
|
+
context=context,
|
|
386
|
+
error=exc,
|
|
387
|
+
operation="directory_list_internal_departments",
|
|
388
|
+
)
|
|
389
|
+
raise
|
|
339
390
|
start = (page_num - 1) * page_size
|
|
340
391
|
page_items = items[start : start + page_size]
|
|
341
392
|
reported_total = None if truncated else len(items)
|
|
@@ -367,17 +418,35 @@ class DirectoryTools(ToolBase):
|
|
|
367
418
|
)
|
|
368
419
|
|
|
369
420
|
def runner(session_profile, context):
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
421
|
+
try:
|
|
422
|
+
result = self.backend.request(
|
|
423
|
+
"POST",
|
|
424
|
+
context,
|
|
425
|
+
"/member/search",
|
|
426
|
+
json_body={
|
|
427
|
+
"dimensions": ["DEPT"],
|
|
428
|
+
"searchKey": normalized_keyword,
|
|
429
|
+
"pageNum": page_num,
|
|
430
|
+
"pageSize": page_size,
|
|
431
|
+
},
|
|
432
|
+
)
|
|
433
|
+
except QingflowApiError as exc:
|
|
434
|
+
if _is_directory_permission_denied(exc):
|
|
435
|
+
return self._directory_permission_denied_payload(
|
|
436
|
+
profile=profile,
|
|
437
|
+
session_profile=session_profile,
|
|
438
|
+
context=context,
|
|
439
|
+
error=exc,
|
|
440
|
+
operation="directory_list_internal_departments",
|
|
441
|
+
)
|
|
442
|
+
raise
|
|
443
|
+
dept_bucket = _payload_value(result, "dept") or result
|
|
376
444
|
return {
|
|
377
445
|
"profile": profile,
|
|
378
446
|
"ws_id": session_profile.selected_ws_id,
|
|
379
447
|
"request_route": self._request_route_payload(context),
|
|
380
|
-
"page":
|
|
448
|
+
"page": dept_bucket,
|
|
449
|
+
"result": result,
|
|
381
450
|
}
|
|
382
451
|
|
|
383
452
|
raw = self._run(profile, runner)
|
|
@@ -411,19 +480,30 @@ class DirectoryTools(ToolBase):
|
|
|
411
480
|
raise_tool_error(QingflowApiError.config_error("max_items must be positive"))
|
|
412
481
|
|
|
413
482
|
def runner(session_profile, context):
|
|
414
|
-
|
|
415
|
-
context,
|
|
416
|
-
parent_dept_id=parent_dept_id,
|
|
417
|
-
max_depth=max_depth,
|
|
418
|
-
max_items=max_items,
|
|
419
|
-
)
|
|
420
|
-
if not items and parent_dept_id is None:
|
|
483
|
+
try:
|
|
421
484
|
items, truncated, deepest_depth = self._walk_department_tree(
|
|
422
485
|
context,
|
|
423
|
-
parent_dept_id=
|
|
486
|
+
parent_dept_id=parent_dept_id,
|
|
424
487
|
max_depth=max_depth,
|
|
425
488
|
max_items=max_items,
|
|
426
489
|
)
|
|
490
|
+
if not items and parent_dept_id is None:
|
|
491
|
+
items, truncated, deepest_depth = self._walk_department_tree(
|
|
492
|
+
context,
|
|
493
|
+
parent_dept_id=0,
|
|
494
|
+
max_depth=max_depth,
|
|
495
|
+
max_items=max_items,
|
|
496
|
+
)
|
|
497
|
+
except QingflowApiError as exc:
|
|
498
|
+
if _is_directory_permission_denied(exc):
|
|
499
|
+
return self._directory_permission_denied_payload(
|
|
500
|
+
profile=profile,
|
|
501
|
+
session_profile=session_profile,
|
|
502
|
+
context=context,
|
|
503
|
+
error=exc,
|
|
504
|
+
operation="directory_list_all_departments",
|
|
505
|
+
)
|
|
506
|
+
raise
|
|
427
507
|
return {
|
|
428
508
|
"profile": profile,
|
|
429
509
|
"ws_id": session_profile.selected_ws_id,
|
|
@@ -454,7 +534,18 @@ class DirectoryTools(ToolBase):
|
|
|
454
534
|
params: dict[str, Any] = {}
|
|
455
535
|
if parent_dept_id is not None:
|
|
456
536
|
params["parentDeptId"] = parent_dept_id
|
|
457
|
-
|
|
537
|
+
try:
|
|
538
|
+
result = self.backend.request("GET", context, "/contact/subDeptList", params=params)
|
|
539
|
+
except QingflowApiError as exc:
|
|
540
|
+
if _is_directory_permission_denied(exc):
|
|
541
|
+
return self._directory_permission_denied_payload(
|
|
542
|
+
profile=profile,
|
|
543
|
+
session_profile=session_profile,
|
|
544
|
+
context=context,
|
|
545
|
+
error=exc,
|
|
546
|
+
operation="directory_list_sub_departments",
|
|
547
|
+
)
|
|
548
|
+
raise
|
|
458
549
|
return {
|
|
459
550
|
"profile": profile,
|
|
460
551
|
"ws_id": session_profile.selected_ws_id,
|
|
@@ -479,22 +570,46 @@ class DirectoryTools(ToolBase):
|
|
|
479
570
|
keyword: str | None,
|
|
480
571
|
page_num: int,
|
|
481
572
|
page_size: int,
|
|
482
|
-
simple: bool,
|
|
573
|
+
simple: bool = True,
|
|
483
574
|
) -> dict[str, Any]:
|
|
484
575
|
"""执行组织目录相关逻辑。"""
|
|
485
576
|
def runner(session_profile, context):
|
|
486
577
|
if simple:
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
578
|
+
try:
|
|
579
|
+
result = self.backend.request(
|
|
580
|
+
"POST",
|
|
581
|
+
context,
|
|
582
|
+
"/external/member/simple/pageList",
|
|
583
|
+
json_body={"keyword": keyword, "pageNum": page_num, "pageSize": page_size},
|
|
584
|
+
)
|
|
585
|
+
except QingflowApiError as exc:
|
|
586
|
+
if _is_directory_permission_denied(exc):
|
|
587
|
+
return self._directory_permission_denied_payload(
|
|
588
|
+
profile=profile,
|
|
589
|
+
session_profile=session_profile,
|
|
590
|
+
context=context,
|
|
591
|
+
error=exc,
|
|
592
|
+
operation="directory_list_external_members_simple",
|
|
593
|
+
) | {"simple": simple}
|
|
594
|
+
raise
|
|
493
595
|
else:
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
596
|
+
try:
|
|
597
|
+
result = self.backend.request(
|
|
598
|
+
"POST",
|
|
599
|
+
context,
|
|
600
|
+
"/external/member/pageList",
|
|
601
|
+
json_body={"keyword": keyword, "pageNum": page_num, "pageSize": page_size},
|
|
602
|
+
)
|
|
603
|
+
except QingflowApiError as exc:
|
|
604
|
+
if _is_directory_permission_denied(exc):
|
|
605
|
+
return self._directory_permission_denied_payload(
|
|
606
|
+
profile=profile,
|
|
607
|
+
session_profile=session_profile,
|
|
608
|
+
context=context,
|
|
609
|
+
error=exc,
|
|
610
|
+
operation="directory_list_external_members_full",
|
|
611
|
+
) | {"simple": simple}
|
|
612
|
+
raise
|
|
498
613
|
return {
|
|
499
614
|
"profile": profile,
|
|
500
615
|
"ws_id": session_profile.selected_ws_id,
|
|
@@ -542,7 +657,7 @@ class DirectoryTools(ToolBase):
|
|
|
542
657
|
"""执行内部辅助逻辑。"""
|
|
543
658
|
response = dict(raw)
|
|
544
659
|
response["ok"] = bool(raw.get("ok", True))
|
|
545
|
-
response["warnings"] = []
|
|
660
|
+
response["warnings"] = list(raw.get("warnings") or [])
|
|
546
661
|
response["output_profile"] = "normal"
|
|
547
662
|
response["data"] = {
|
|
548
663
|
"items": items,
|
|
@@ -551,6 +666,49 @@ class DirectoryTools(ToolBase):
|
|
|
551
666
|
}
|
|
552
667
|
return response
|
|
553
668
|
|
|
669
|
+
def _directory_permission_denied_payload(
|
|
670
|
+
self,
|
|
671
|
+
*,
|
|
672
|
+
profile: str,
|
|
673
|
+
session_profile, # type: ignore[no-untyped-def]
|
|
674
|
+
context, # type: ignore[no-untyped-def]
|
|
675
|
+
error: QingflowApiError,
|
|
676
|
+
operation: str,
|
|
677
|
+
) -> dict[str, Any]:
|
|
678
|
+
candidate_hint = (
|
|
679
|
+
"For record member/department fields, use record_member_candidates or "
|
|
680
|
+
"record_department_candidates."
|
|
681
|
+
if operation == "directory_search"
|
|
682
|
+
else (
|
|
683
|
+
"For record member/department fields, use record_member_candidates, "
|
|
684
|
+
"record_department_candidates, or directory_search with a keyword instead."
|
|
685
|
+
)
|
|
686
|
+
)
|
|
687
|
+
return {
|
|
688
|
+
"profile": profile,
|
|
689
|
+
"ws_id": session_profile.selected_ws_id,
|
|
690
|
+
"ok": False,
|
|
691
|
+
"status": "failed",
|
|
692
|
+
"error_code": "CONTACT_DIRECTORY_PERMISSION_DENIED",
|
|
693
|
+
"message": (
|
|
694
|
+
"Contact-directory management data is not readable for the current user. "
|
|
695
|
+
f"{candidate_hint}"
|
|
696
|
+
),
|
|
697
|
+
"backend_code": error.backend_code,
|
|
698
|
+
"request_id": error.request_id,
|
|
699
|
+
"http_status": error.http_status,
|
|
700
|
+
"request_route": self._request_route_payload(context),
|
|
701
|
+
"items": [],
|
|
702
|
+
"pagination": {"returned_items": 0},
|
|
703
|
+
"warnings": [
|
|
704
|
+
{
|
|
705
|
+
"code": "CONTACT_DIRECTORY_PERMISSION_DENIED",
|
|
706
|
+
"message": "This is a contact-directory permission boundary, not proof that record field candidates are unavailable.",
|
|
707
|
+
"operation": operation,
|
|
708
|
+
}
|
|
709
|
+
],
|
|
710
|
+
}
|
|
711
|
+
|
|
554
712
|
def _walk_department_tree(
|
|
555
713
|
self,
|
|
556
714
|
context, # type: ignore[no-untyped-def]
|
|
@@ -641,6 +799,12 @@ def _directory_has_more(payload: Any, *, current_page: int, page_size: int, retu
|
|
|
641
799
|
return returned_items >= page_size and returned_items > 0
|
|
642
800
|
|
|
643
801
|
|
|
802
|
+
def _is_directory_permission_denied(error: QingflowApiError) -> bool:
|
|
803
|
+
if is_auth_like_error(error):
|
|
804
|
+
return False
|
|
805
|
+
return backend_code_int(error) in {40002, 40027}
|
|
806
|
+
|
|
807
|
+
|
|
644
808
|
def _directory_member_key(item: dict[str, Any]) -> str:
|
|
645
809
|
for key in ("uid", "id", "userId"):
|
|
646
810
|
value = item.get(key)
|