@josephyan/qingflow-app-builder-mcp 1.1.24 → 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 -3100
- 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
|
@@ -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,
|
|
6
|
+
from ..errors import QingflowApiError, raise_tool_error
|
|
7
7
|
from ..json_types import JSONObject
|
|
8
8
|
from .base import ToolBase, tool_cn_name
|
|
9
9
|
|
|
@@ -31,26 +31,12 @@ 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
|
-
|
|
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
|
-
)
|
|
34
|
+
result = self.backend.request(
|
|
35
|
+
"GET",
|
|
36
|
+
context,
|
|
37
|
+
f"/app/{app_key}/customButton",
|
|
38
|
+
params={"beingDraft": being_draft},
|
|
39
|
+
)
|
|
54
40
|
items = []
|
|
55
41
|
raw_items = result.get("result") if isinstance(result, dict) and isinstance(result.get("result"), list) else []
|
|
56
42
|
for item in raw_items:
|
|
@@ -61,23 +47,11 @@ class CustomButtonTools(ToolBase):
|
|
|
61
47
|
"profile": profile,
|
|
62
48
|
"ws_id": session_profile.selected_ws_id,
|
|
63
49
|
"app_key": app_key,
|
|
64
|
-
"being_draft":
|
|
65
|
-
"requested_being_draft": being_draft,
|
|
50
|
+
"being_draft": being_draft,
|
|
66
51
|
"items": result if include_raw else items,
|
|
67
52
|
"count": len(items),
|
|
68
53
|
"compact": not include_raw,
|
|
69
54
|
}
|
|
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}
|
|
81
55
|
if include_raw:
|
|
82
56
|
response["summary"] = items
|
|
83
57
|
return response
|
|
@@ -100,37 +74,16 @@ class CustomButtonTools(ToolBase):
|
|
|
100
74
|
|
|
101
75
|
def runner(session_profile, context):
|
|
102
76
|
params = {"beingDraft": being_draft}
|
|
103
|
-
|
|
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})
|
|
77
|
+
result = self.backend.request("GET", context, f"/app/{app_key}/customButton/{button_id}", params=params)
|
|
113
78
|
response = {
|
|
114
79
|
"profile": profile,
|
|
115
80
|
"ws_id": session_profile.selected_ws_id,
|
|
116
81
|
"app_key": app_key,
|
|
117
82
|
"button_id": button_id,
|
|
118
|
-
"being_draft":
|
|
119
|
-
"requested_being_draft": being_draft,
|
|
83
|
+
"being_draft": being_draft,
|
|
120
84
|
"result": result if include_raw else self._compact_button_detail(result if isinstance(result, dict) else {}),
|
|
121
85
|
"compact": not include_raw,
|
|
122
86
|
}
|
|
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}
|
|
134
87
|
if include_raw:
|
|
135
88
|
response["summary"] = self._compact_button_detail(result if isinstance(result, dict) else {})
|
|
136
89
|
return response
|
|
@@ -245,10 +198,3 @@ class CustomButtonTools(ToolBase):
|
|
|
245
198
|
else None,
|
|
246
199
|
"trigger_wings_config": deepcopy(item.get("triggerWingsConfig")) if isinstance(item.get("triggerWingsConfig"), dict) else None,
|
|
247
200
|
}
|
|
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,
|
|
9
|
+
from ..errors import QingflowApiError, raise_tool_error
|
|
10
10
|
from .base import ToolBase, tool_cn_name
|
|
11
11
|
|
|
12
12
|
|
|
@@ -20,17 +20,28 @@ class DirectoryTools(ToolBase):
|
|
|
20
20
|
主要职责:
|
|
21
21
|
1. 搜索用户、部门、外部联系人;
|
|
22
22
|
2. 列举内部用户与部门层级;
|
|
23
|
-
3.
|
|
24
|
-
|
|
25
|
-
注意:记录成员/部门字段候选应优先使用 record_member_candidates /
|
|
26
|
-
record_department_candidates,避免把普通记录读写误带入通讯录目录链路。
|
|
23
|
+
3. 为审批/字段候选提供目录侧数据来源。
|
|
27
24
|
"""
|
|
28
25
|
|
|
29
26
|
def register(self, mcp: FastMCP) -> None:
|
|
30
27
|
"""注册当前工具到 MCP 服务。"""
|
|
31
|
-
|
|
28
|
+
@mcp.tool()
|
|
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
|
+
)
|
|
32
43
|
|
|
33
|
-
@mcp.tool(
|
|
44
|
+
@mcp.tool()
|
|
34
45
|
def directory_list_internal_users(
|
|
35
46
|
profile: str = DEFAULT_PROFILE,
|
|
36
47
|
keyword: str | None = None,
|
|
@@ -50,7 +61,7 @@ class DirectoryTools(ToolBase):
|
|
|
50
61
|
contain_disable=include_disabled,
|
|
51
62
|
)
|
|
52
63
|
|
|
53
|
-
@mcp.tool(
|
|
64
|
+
@mcp.tool()
|
|
54
65
|
def directory_list_all_internal_users(
|
|
55
66
|
profile: str = DEFAULT_PROFILE,
|
|
56
67
|
keyword: str | None = None,
|
|
@@ -70,7 +81,7 @@ class DirectoryTools(ToolBase):
|
|
|
70
81
|
max_pages=max_pages,
|
|
71
82
|
)
|
|
72
83
|
|
|
73
|
-
@mcp.tool(
|
|
84
|
+
@mcp.tool()
|
|
74
85
|
def directory_list_internal_departments(
|
|
75
86
|
profile: str = DEFAULT_PROFILE,
|
|
76
87
|
keyword: str = "",
|
|
@@ -84,7 +95,7 @@ class DirectoryTools(ToolBase):
|
|
|
84
95
|
page_size=page_size,
|
|
85
96
|
)
|
|
86
97
|
|
|
87
|
-
@mcp.tool(
|
|
98
|
+
@mcp.tool()
|
|
88
99
|
def directory_list_all_departments(
|
|
89
100
|
profile: str = DEFAULT_PROFILE,
|
|
90
101
|
parent_department_id: int | None = None,
|
|
@@ -98,20 +109,20 @@ class DirectoryTools(ToolBase):
|
|
|
98
109
|
max_items=max_items,
|
|
99
110
|
)
|
|
100
111
|
|
|
101
|
-
@mcp.tool(
|
|
112
|
+
@mcp.tool()
|
|
102
113
|
def directory_list_sub_departments(
|
|
103
114
|
profile: str = DEFAULT_PROFILE,
|
|
104
115
|
parent_department_id: int | None = None,
|
|
105
116
|
) -> dict[str, Any]:
|
|
106
117
|
return self.directory_list_sub_departments(profile=profile, parent_dept_id=parent_department_id)
|
|
107
118
|
|
|
108
|
-
@mcp.tool(
|
|
119
|
+
@mcp.tool()
|
|
109
120
|
def directory_list_external_members(
|
|
110
121
|
profile: str = DEFAULT_PROFILE,
|
|
111
122
|
keyword: str | None = None,
|
|
112
123
|
page_num: int = 1,
|
|
113
124
|
page_size: int = 20,
|
|
114
|
-
simple: bool =
|
|
125
|
+
simple: bool = False,
|
|
115
126
|
) -> dict[str, Any]:
|
|
116
127
|
return self.directory_list_external_members(
|
|
117
128
|
profile=profile,
|
|
@@ -121,24 +132,6 @@ class DirectoryTools(ToolBase):
|
|
|
121
132
|
simple=simple,
|
|
122
133
|
)
|
|
123
134
|
|
|
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
|
-
|
|
142
135
|
@tool_cn_name("通讯录搜索")
|
|
143
136
|
def directory_search(
|
|
144
137
|
self,
|
|
@@ -158,28 +151,17 @@ class DirectoryTools(ToolBase):
|
|
|
158
151
|
raise_tool_error(QingflowApiError.config_error("query is required"))
|
|
159
152
|
|
|
160
153
|
def runner(session_profile, context):
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
154
|
+
result = self.backend.request(
|
|
155
|
+
"POST",
|
|
156
|
+
context,
|
|
157
|
+
"/member/search",
|
|
158
|
+
json_body={
|
|
159
|
+
"dimensions": normalized_scopes,
|
|
160
|
+
"searchKey": query,
|
|
161
|
+
"pageNum": page_num,
|
|
162
|
+
"pageSize": page_size,
|
|
163
|
+
},
|
|
164
|
+
)
|
|
183
165
|
return {
|
|
184
166
|
"profile": profile,
|
|
185
167
|
"ws_id": session_profile.selected_ws_id,
|
|
@@ -227,18 +209,7 @@ class DirectoryTools(ToolBase):
|
|
|
227
209
|
params["deptId"] = dept_id
|
|
228
210
|
if role_id is not None:
|
|
229
211
|
params["roleId"] = role_id
|
|
230
|
-
|
|
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
|
|
212
|
+
result = self.backend.request("GET", context, "/contact", params=params)
|
|
242
213
|
return {
|
|
243
214
|
"profile": profile,
|
|
244
215
|
"ws_id": session_profile.selected_ws_id,
|
|
@@ -298,18 +269,7 @@ class DirectoryTools(ToolBase):
|
|
|
298
269
|
params["deptId"] = dept_id
|
|
299
270
|
if role_id is not None:
|
|
300
271
|
params["roleId"] = role_id
|
|
301
|
-
|
|
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
|
|
272
|
+
result = self.backend.request("GET", context, "/contact", params=params)
|
|
313
273
|
page_items = _directory_items(result)
|
|
314
274
|
if reported_total is None:
|
|
315
275
|
reported_total = _coerce_int(_payload_value(result, "total"))
|
|
@@ -369,24 +329,13 @@ class DirectoryTools(ToolBase):
|
|
|
369
329
|
|
|
370
330
|
if not normalized_keyword:
|
|
371
331
|
def runner(session_profile, context):
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
|
332
|
+
fetch_limit = max((page_num + 1) * page_size + 1, page_size + 1)
|
|
333
|
+
items, truncated, deepest_depth = self._walk_department_tree(
|
|
334
|
+
context,
|
|
335
|
+
parent_dept_id=None,
|
|
336
|
+
max_depth=20,
|
|
337
|
+
max_items=fetch_limit,
|
|
338
|
+
)
|
|
390
339
|
start = (page_num - 1) * page_size
|
|
391
340
|
page_items = items[start : start + page_size]
|
|
392
341
|
reported_total = None if truncated else len(items)
|
|
@@ -418,35 +367,17 @@ class DirectoryTools(ToolBase):
|
|
|
418
367
|
)
|
|
419
368
|
|
|
420
369
|
def runner(session_profile, context):
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
|
370
|
+
result = self.backend.request(
|
|
371
|
+
"GET",
|
|
372
|
+
context,
|
|
373
|
+
"/contact/deptByPage",
|
|
374
|
+
params={"keyword": normalized_keyword, "pageNum": page_num, "pageSize": page_size},
|
|
375
|
+
)
|
|
444
376
|
return {
|
|
445
377
|
"profile": profile,
|
|
446
378
|
"ws_id": session_profile.selected_ws_id,
|
|
447
379
|
"request_route": self._request_route_payload(context),
|
|
448
|
-
"page":
|
|
449
|
-
"result": result,
|
|
380
|
+
"page": result,
|
|
450
381
|
}
|
|
451
382
|
|
|
452
383
|
raw = self._run(profile, runner)
|
|
@@ -480,30 +411,19 @@ class DirectoryTools(ToolBase):
|
|
|
480
411
|
raise_tool_error(QingflowApiError.config_error("max_items must be positive"))
|
|
481
412
|
|
|
482
413
|
def runner(session_profile, context):
|
|
483
|
-
|
|
414
|
+
items, truncated, deepest_depth = self._walk_department_tree(
|
|
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:
|
|
484
421
|
items, truncated, deepest_depth = self._walk_department_tree(
|
|
485
422
|
context,
|
|
486
|
-
parent_dept_id=
|
|
423
|
+
parent_dept_id=0,
|
|
487
424
|
max_depth=max_depth,
|
|
488
425
|
max_items=max_items,
|
|
489
426
|
)
|
|
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
|
|
507
427
|
return {
|
|
508
428
|
"profile": profile,
|
|
509
429
|
"ws_id": session_profile.selected_ws_id,
|
|
@@ -534,18 +454,7 @@ class DirectoryTools(ToolBase):
|
|
|
534
454
|
params: dict[str, Any] = {}
|
|
535
455
|
if parent_dept_id is not None:
|
|
536
456
|
params["parentDeptId"] = parent_dept_id
|
|
537
|
-
|
|
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
|
|
457
|
+
result = self.backend.request("GET", context, "/contact/subDeptList", params=params)
|
|
549
458
|
return {
|
|
550
459
|
"profile": profile,
|
|
551
460
|
"ws_id": session_profile.selected_ws_id,
|
|
@@ -570,46 +479,22 @@ class DirectoryTools(ToolBase):
|
|
|
570
479
|
keyword: str | None,
|
|
571
480
|
page_num: int,
|
|
572
481
|
page_size: int,
|
|
573
|
-
simple: bool
|
|
482
|
+
simple: bool,
|
|
574
483
|
) -> dict[str, Any]:
|
|
575
484
|
"""执行组织目录相关逻辑。"""
|
|
576
485
|
def runner(session_profile, context):
|
|
577
486
|
if simple:
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
|
487
|
+
result = self.backend.request(
|
|
488
|
+
"POST",
|
|
489
|
+
context,
|
|
490
|
+
"/external/member/simple/pageList",
|
|
491
|
+
json_body={"keyword": keyword, "pageNum": page_num, "pageSize": page_size},
|
|
492
|
+
)
|
|
595
493
|
else:
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
|
494
|
+
params: dict[str, Any] = {"pageNum": page_num, "pageSize": page_size}
|
|
495
|
+
if keyword:
|
|
496
|
+
params["keyword"] = keyword
|
|
497
|
+
result = self.backend.request("GET", context, "/external/member/pageList", params=params)
|
|
613
498
|
return {
|
|
614
499
|
"profile": profile,
|
|
615
500
|
"ws_id": session_profile.selected_ws_id,
|
|
@@ -657,7 +542,7 @@ class DirectoryTools(ToolBase):
|
|
|
657
542
|
"""执行内部辅助逻辑。"""
|
|
658
543
|
response = dict(raw)
|
|
659
544
|
response["ok"] = bool(raw.get("ok", True))
|
|
660
|
-
response["warnings"] =
|
|
545
|
+
response["warnings"] = []
|
|
661
546
|
response["output_profile"] = "normal"
|
|
662
547
|
response["data"] = {
|
|
663
548
|
"items": items,
|
|
@@ -666,49 +551,6 @@ class DirectoryTools(ToolBase):
|
|
|
666
551
|
}
|
|
667
552
|
return response
|
|
668
553
|
|
|
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
|
-
|
|
712
554
|
def _walk_department_tree(
|
|
713
555
|
self,
|
|
714
556
|
context, # type: ignore[no-untyped-def]
|
|
@@ -799,12 +641,6 @@ def _directory_has_more(payload: Any, *, current_page: int, page_size: int, retu
|
|
|
799
641
|
return returned_items >= page_size and returned_items > 0
|
|
800
642
|
|
|
801
643
|
|
|
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
|
-
|
|
808
644
|
def _directory_member_key(item: dict[str, Any]) -> str:
|
|
809
645
|
for key in ("uid", "id", "userId"):
|
|
810
646
|
value = item.get(key)
|