@josephyan/qingflow-cli 0.2.0-beta.1000
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 +31 -0
- package/docs/local-agent-install.md +309 -0
- package/entry_point.py +13 -0
- package/npm/bin/qingflow.mjs +5 -0
- package/npm/lib/runtime.mjs +346 -0
- package/npm/scripts/postinstall.mjs +16 -0
- package/package.json +34 -0
- package/pyproject.toml +67 -0
- package/qingflow +15 -0
- package/src/qingflow_mcp/__init__.py +37 -0
- package/src/qingflow_mcp/__main__.py +5 -0
- package/src/qingflow_mcp/backend_client.py +649 -0
- package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
- package/src/qingflow_mcp/builder_facade/models.py +1846 -0
- package/src/qingflow_mcp/builder_facade/service.py +16502 -0
- package/src/qingflow_mcp/cli/__init__.py +1 -0
- package/src/qingflow_mcp/cli/commands/__init__.py +18 -0
- package/src/qingflow_mcp/cli/commands/app.py +40 -0
- package/src/qingflow_mcp/cli/commands/auth.py +112 -0
- package/src/qingflow_mcp/cli/commands/builder.py +539 -0
- package/src/qingflow_mcp/cli/commands/chart.py +18 -0
- package/src/qingflow_mcp/cli/commands/common.py +62 -0
- package/src/qingflow_mcp/cli/commands/imports.py +96 -0
- package/src/qingflow_mcp/cli/commands/portal.py +25 -0
- package/src/qingflow_mcp/cli/commands/record.py +331 -0
- package/src/qingflow_mcp/cli/commands/repo.py +80 -0
- package/src/qingflow_mcp/cli/commands/task.py +141 -0
- package/src/qingflow_mcp/cli/commands/view.py +18 -0
- package/src/qingflow_mcp/cli/commands/workspace.py +110 -0
- package/src/qingflow_mcp/cli/context.py +60 -0
- package/src/qingflow_mcp/cli/formatters.py +573 -0
- package/src/qingflow_mcp/cli/json_io.py +50 -0
- package/src/qingflow_mcp/cli/main.py +186 -0
- package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
- package/src/qingflow_mcp/cli/terminal_ui.py +173 -0
- package/src/qingflow_mcp/config.py +407 -0
- package/src/qingflow_mcp/errors.py +66 -0
- package/src/qingflow_mcp/id_utils.py +49 -0
- package/src/qingflow_mcp/import_store.py +121 -0
- package/src/qingflow_mcp/json_types.py +18 -0
- package/src/qingflow_mcp/list_type_labels.py +76 -0
- package/src/qingflow_mcp/public_surface.py +243 -0
- package/src/qingflow_mcp/repository_store.py +71 -0
- package/src/qingflow_mcp/response_trim.py +841 -0
- package/src/qingflow_mcp/server.py +216 -0
- package/src/qingflow_mcp/server_app_builder.py +543 -0
- package/src/qingflow_mcp/server_app_user.py +386 -0
- package/src/qingflow_mcp/session_store.py +369 -0
- package/src/qingflow_mcp/solution/__init__.py +6 -0
- package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
- package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
- package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +495 -0
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +187 -0
- package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
- package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
- package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
- package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
- package/src/qingflow_mcp/solution/design_session.py +222 -0
- package/src/qingflow_mcp/solution/design_store.py +100 -0
- package/src/qingflow_mcp/solution/executor.py +2398 -0
- package/src/qingflow_mcp/solution/normalizer.py +23 -0
- package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
- package/src/qingflow_mcp/solution/run_store.py +244 -0
- package/src/qingflow_mcp/solution/spec_models.py +855 -0
- package/src/qingflow_mcp/tools/__init__.py +1 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +3449 -0
- package/src/qingflow_mcp/tools/app_tools.py +926 -0
- package/src/qingflow_mcp/tools/approval_tools.py +1062 -0
- package/src/qingflow_mcp/tools/auth_tools.py +1133 -0
- package/src/qingflow_mcp/tools/base.py +281 -0
- package/src/qingflow_mcp/tools/code_block_tools.py +777 -0
- package/src/qingflow_mcp/tools/custom_button_tools.py +202 -0
- package/src/qingflow_mcp/tools/directory_tools.py +675 -0
- package/src/qingflow_mcp/tools/feedback_tools.py +238 -0
- package/src/qingflow_mcp/tools/file_tools.py +409 -0
- package/src/qingflow_mcp/tools/import_tools.py +2223 -0
- package/src/qingflow_mcp/tools/navigation_tools.py +210 -0
- package/src/qingflow_mcp/tools/package_tools.py +326 -0
- package/src/qingflow_mcp/tools/portal_tools.py +158 -0
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +374 -0
- package/src/qingflow_mcp/tools/record_tools.py +14291 -0
- package/src/qingflow_mcp/tools/repository_dev_tools.py +552 -0
- package/src/qingflow_mcp/tools/resource_read_tools.py +503 -0
- package/src/qingflow_mcp/tools/role_tools.py +112 -0
- package/src/qingflow_mcp/tools/solution_tools.py +4054 -0
- package/src/qingflow_mcp/tools/task_context_tools.py +2986 -0
- package/src/qingflow_mcp/tools/task_tools.py +889 -0
- package/src/qingflow_mcp/tools/view_tools.py +335 -0
- package/src/qingflow_mcp/tools/workflow_tools.py +376 -0
- package/src/qingflow_mcp/tools/workspace_tools.py +266 -0
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import deque
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from mcp.server.fastmcp import FastMCP
|
|
7
|
+
|
|
8
|
+
from ..config import DEFAULT_PROFILE
|
|
9
|
+
from ..errors import QingflowApiError, raise_tool_error
|
|
10
|
+
from .base import ToolBase, tool_cn_name
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
ALLOWED_DIRECTORY_SEARCH_SCOPES = {"MEMBER", "DEPT"}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DirectoryTools(ToolBase):
|
|
17
|
+
"""组织架构工具(中文名:通讯录与组织目录)。
|
|
18
|
+
|
|
19
|
+
类型:组织目录工具。
|
|
20
|
+
主要职责:
|
|
21
|
+
1. 搜索用户、部门、外部联系人;
|
|
22
|
+
2. 列举内部用户与部门层级;
|
|
23
|
+
3. 为审批/字段候选提供目录侧数据来源。
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def register(self, mcp: FastMCP) -> None:
|
|
27
|
+
"""注册当前工具到 MCP 服务。"""
|
|
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
|
+
)
|
|
43
|
+
|
|
44
|
+
@mcp.tool()
|
|
45
|
+
def directory_list_internal_users(
|
|
46
|
+
profile: str = DEFAULT_PROFILE,
|
|
47
|
+
keyword: str | None = None,
|
|
48
|
+
department_id: int | None = None,
|
|
49
|
+
role_id: int | None = None,
|
|
50
|
+
page_num: int = 1,
|
|
51
|
+
page_size: int = 20,
|
|
52
|
+
include_disabled: bool = False,
|
|
53
|
+
) -> dict[str, Any]:
|
|
54
|
+
return self.directory_list_internal_users(
|
|
55
|
+
profile=profile,
|
|
56
|
+
keyword=keyword,
|
|
57
|
+
dept_id=department_id,
|
|
58
|
+
role_id=role_id,
|
|
59
|
+
page_num=page_num,
|
|
60
|
+
page_size=page_size,
|
|
61
|
+
contain_disable=include_disabled,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
@mcp.tool()
|
|
65
|
+
def directory_list_all_internal_users(
|
|
66
|
+
profile: str = DEFAULT_PROFILE,
|
|
67
|
+
keyword: str | None = None,
|
|
68
|
+
department_id: int | None = None,
|
|
69
|
+
role_id: int | None = None,
|
|
70
|
+
page_size: int = 200,
|
|
71
|
+
include_disabled: bool = False,
|
|
72
|
+
max_pages: int = 100,
|
|
73
|
+
) -> dict[str, Any]:
|
|
74
|
+
return self.directory_list_all_internal_users(
|
|
75
|
+
profile=profile,
|
|
76
|
+
keyword=keyword,
|
|
77
|
+
dept_id=department_id,
|
|
78
|
+
role_id=role_id,
|
|
79
|
+
page_size=page_size,
|
|
80
|
+
contain_disable=include_disabled,
|
|
81
|
+
max_pages=max_pages,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
@mcp.tool()
|
|
85
|
+
def directory_list_internal_departments(
|
|
86
|
+
profile: str = DEFAULT_PROFILE,
|
|
87
|
+
keyword: str = "",
|
|
88
|
+
page_num: int = 1,
|
|
89
|
+
page_size: int = 20,
|
|
90
|
+
) -> dict[str, Any]:
|
|
91
|
+
return self.directory_list_internal_departments(
|
|
92
|
+
profile=profile,
|
|
93
|
+
keyword=keyword,
|
|
94
|
+
page_num=page_num,
|
|
95
|
+
page_size=page_size,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
@mcp.tool()
|
|
99
|
+
def directory_list_all_departments(
|
|
100
|
+
profile: str = DEFAULT_PROFILE,
|
|
101
|
+
parent_department_id: int | None = None,
|
|
102
|
+
max_depth: int = 20,
|
|
103
|
+
max_items: int = 2000,
|
|
104
|
+
) -> dict[str, Any]:
|
|
105
|
+
return self.directory_list_all_departments(
|
|
106
|
+
profile=profile,
|
|
107
|
+
parent_dept_id=parent_department_id,
|
|
108
|
+
max_depth=max_depth,
|
|
109
|
+
max_items=max_items,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
@mcp.tool()
|
|
113
|
+
def directory_list_sub_departments(
|
|
114
|
+
profile: str = DEFAULT_PROFILE,
|
|
115
|
+
parent_department_id: int | None = None,
|
|
116
|
+
) -> dict[str, Any]:
|
|
117
|
+
return self.directory_list_sub_departments(profile=profile, parent_dept_id=parent_department_id)
|
|
118
|
+
|
|
119
|
+
@mcp.tool()
|
|
120
|
+
def directory_list_external_members(
|
|
121
|
+
profile: str = DEFAULT_PROFILE,
|
|
122
|
+
keyword: str | None = None,
|
|
123
|
+
page_num: int = 1,
|
|
124
|
+
page_size: int = 20,
|
|
125
|
+
simple: bool = False,
|
|
126
|
+
) -> dict[str, Any]:
|
|
127
|
+
return self.directory_list_external_members(
|
|
128
|
+
profile=profile,
|
|
129
|
+
keyword=keyword,
|
|
130
|
+
page_num=page_num,
|
|
131
|
+
page_size=page_size,
|
|
132
|
+
simple=simple,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
@tool_cn_name("通讯录搜索")
|
|
136
|
+
def directory_search(
|
|
137
|
+
self,
|
|
138
|
+
*,
|
|
139
|
+
profile: str,
|
|
140
|
+
query: str,
|
|
141
|
+
scopes: list[str] | None,
|
|
142
|
+
page_num: int,
|
|
143
|
+
page_size: int,
|
|
144
|
+
) -> dict[str, Any]:
|
|
145
|
+
"""执行组织目录相关逻辑。"""
|
|
146
|
+
normalized_scopes = scopes or ["MEMBER", "DEPT"]
|
|
147
|
+
invalid_scopes = [scope for scope in normalized_scopes if scope not in ALLOWED_DIRECTORY_SEARCH_SCOPES]
|
|
148
|
+
if invalid_scopes:
|
|
149
|
+
raise_tool_error(QingflowApiError.not_supported(f"directory_search only supports internal scopes {sorted(ALLOWED_DIRECTORY_SEARCH_SCOPES)}; got {invalid_scopes}"))
|
|
150
|
+
if not query:
|
|
151
|
+
raise_tool_error(QingflowApiError.config_error("query is required"))
|
|
152
|
+
|
|
153
|
+
def runner(session_profile, context):
|
|
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
|
+
)
|
|
165
|
+
return {
|
|
166
|
+
"profile": profile,
|
|
167
|
+
"ws_id": session_profile.selected_ws_id,
|
|
168
|
+
"request_route": self._request_route_payload(context),
|
|
169
|
+
"result": result,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
raw = self._run(profile, runner)
|
|
173
|
+
items = [item for item in _directory_items(raw.get("result")) if isinstance(item, dict)]
|
|
174
|
+
return self._public_directory_response(
|
|
175
|
+
raw,
|
|
176
|
+
items=items,
|
|
177
|
+
pagination={
|
|
178
|
+
"page": page_num,
|
|
179
|
+
"page_size": page_size,
|
|
180
|
+
"returned_items": len(items),
|
|
181
|
+
"reported_total": _coerce_int(_payload_value(raw.get("result"), "total")),
|
|
182
|
+
"page_amount": _coerce_int(_payload_value(raw.get("result"), "pageAmount")),
|
|
183
|
+
},
|
|
184
|
+
selection={"query": query, "scopes": normalized_scopes},
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
@tool_cn_name("内部成员列表")
|
|
188
|
+
def directory_list_internal_users(
|
|
189
|
+
self,
|
|
190
|
+
*,
|
|
191
|
+
profile: str,
|
|
192
|
+
keyword: str | None,
|
|
193
|
+
dept_id: int | None,
|
|
194
|
+
role_id: int | None,
|
|
195
|
+
page_num: int,
|
|
196
|
+
page_size: int,
|
|
197
|
+
contain_disable: bool,
|
|
198
|
+
) -> dict[str, Any]:
|
|
199
|
+
"""执行组织目录相关逻辑。"""
|
|
200
|
+
def runner(session_profile, context):
|
|
201
|
+
params: dict[str, Any] = {
|
|
202
|
+
"pageNum": page_num,
|
|
203
|
+
"pageSize": page_size,
|
|
204
|
+
"containDisable": contain_disable,
|
|
205
|
+
}
|
|
206
|
+
if keyword:
|
|
207
|
+
params["keyword"] = keyword
|
|
208
|
+
if dept_id is not None:
|
|
209
|
+
params["deptId"] = dept_id
|
|
210
|
+
if role_id is not None:
|
|
211
|
+
params["roleId"] = role_id
|
|
212
|
+
result = self.backend.request("GET", context, "/contact", params=params)
|
|
213
|
+
return {
|
|
214
|
+
"profile": profile,
|
|
215
|
+
"ws_id": session_profile.selected_ws_id,
|
|
216
|
+
"request_route": self._request_route_payload(context),
|
|
217
|
+
"result": result,
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
raw = self._run(profile, runner)
|
|
221
|
+
items = [item for item in _directory_items(raw.get("result")) if isinstance(item, dict)]
|
|
222
|
+
return self._public_directory_response(
|
|
223
|
+
raw,
|
|
224
|
+
items=items,
|
|
225
|
+
pagination={
|
|
226
|
+
"page": page_num,
|
|
227
|
+
"page_size": page_size,
|
|
228
|
+
"returned_items": len(items),
|
|
229
|
+
"reported_total": _coerce_int(_payload_value(raw.get("result"), "total")),
|
|
230
|
+
"page_amount": _coerce_int(_payload_value(raw.get("result"), "pageAmount")),
|
|
231
|
+
},
|
|
232
|
+
selection={"keyword": keyword, "department_id": dept_id, "role_id": role_id, "include_disabled": contain_disable},
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
@tool_cn_name("内部成员全量")
|
|
236
|
+
def directory_list_all_internal_users(
|
|
237
|
+
self,
|
|
238
|
+
*,
|
|
239
|
+
profile: str,
|
|
240
|
+
keyword: str | None,
|
|
241
|
+
dept_id: int | None,
|
|
242
|
+
role_id: int | None,
|
|
243
|
+
page_size: int,
|
|
244
|
+
contain_disable: bool,
|
|
245
|
+
max_pages: int,
|
|
246
|
+
) -> dict[str, Any]:
|
|
247
|
+
"""执行组织目录相关逻辑。"""
|
|
248
|
+
if page_size <= 0:
|
|
249
|
+
raise_tool_error(QingflowApiError.config_error("page_size must be positive"))
|
|
250
|
+
if max_pages <= 0:
|
|
251
|
+
raise_tool_error(QingflowApiError.config_error("max_pages must be positive"))
|
|
252
|
+
|
|
253
|
+
def runner(session_profile, context):
|
|
254
|
+
current_page = 1
|
|
255
|
+
fetched_pages = 0
|
|
256
|
+
has_more = False
|
|
257
|
+
reported_total: int | None = None
|
|
258
|
+
seen_keys: set[str] = set()
|
|
259
|
+
items: list[dict[str, Any]] = []
|
|
260
|
+
while fetched_pages < max_pages:
|
|
261
|
+
params: dict[str, Any] = {
|
|
262
|
+
"pageNum": current_page,
|
|
263
|
+
"pageSize": page_size,
|
|
264
|
+
"containDisable": contain_disable,
|
|
265
|
+
}
|
|
266
|
+
if keyword:
|
|
267
|
+
params["keyword"] = keyword
|
|
268
|
+
if dept_id is not None:
|
|
269
|
+
params["deptId"] = dept_id
|
|
270
|
+
if role_id is not None:
|
|
271
|
+
params["roleId"] = role_id
|
|
272
|
+
result = self.backend.request("GET", context, "/contact", params=params)
|
|
273
|
+
page_items = _directory_items(result)
|
|
274
|
+
if reported_total is None:
|
|
275
|
+
reported_total = _coerce_int(_payload_value(result, "total"))
|
|
276
|
+
for item in page_items:
|
|
277
|
+
if not isinstance(item, dict):
|
|
278
|
+
continue
|
|
279
|
+
member_key = _directory_member_key(item)
|
|
280
|
+
if member_key in seen_keys:
|
|
281
|
+
continue
|
|
282
|
+
seen_keys.add(member_key)
|
|
283
|
+
items.append(dict(item))
|
|
284
|
+
fetched_pages += 1
|
|
285
|
+
has_more = _directory_has_more(result, current_page=current_page, page_size=page_size, returned_items=len(page_items))
|
|
286
|
+
if not has_more:
|
|
287
|
+
break
|
|
288
|
+
current_page += 1
|
|
289
|
+
return {
|
|
290
|
+
"profile": profile,
|
|
291
|
+
"ws_id": session_profile.selected_ws_id,
|
|
292
|
+
"request_route": self._request_route_payload(context),
|
|
293
|
+
"items": items,
|
|
294
|
+
"pagination": {
|
|
295
|
+
"page_size": page_size,
|
|
296
|
+
"fetched_pages": fetched_pages,
|
|
297
|
+
"returned_items": len(items),
|
|
298
|
+
"reported_total": reported_total,
|
|
299
|
+
"is_complete": not has_more,
|
|
300
|
+
"has_more": has_more,
|
|
301
|
+
"next_page_num": current_page + 1 if has_more else None,
|
|
302
|
+
"max_pages": max_pages,
|
|
303
|
+
},
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
raw = self._run(profile, runner)
|
|
307
|
+
return self._public_directory_response(
|
|
308
|
+
raw,
|
|
309
|
+
items=[item for item in raw.get("items", []) if isinstance(item, dict)],
|
|
310
|
+
pagination=raw.get("pagination", {}),
|
|
311
|
+
selection={"keyword": keyword, "department_id": dept_id, "role_id": role_id, "include_disabled": contain_disable},
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
@tool_cn_name("内部部门列表")
|
|
315
|
+
def directory_list_internal_departments(
|
|
316
|
+
self,
|
|
317
|
+
*,
|
|
318
|
+
profile: str,
|
|
319
|
+
keyword: str,
|
|
320
|
+
page_num: int,
|
|
321
|
+
page_size: int,
|
|
322
|
+
) -> dict[str, Any]:
|
|
323
|
+
"""执行组织目录相关逻辑。"""
|
|
324
|
+
if page_num <= 0:
|
|
325
|
+
raise_tool_error(QingflowApiError.config_error("page_num must be positive"))
|
|
326
|
+
if page_size <= 0:
|
|
327
|
+
raise_tool_error(QingflowApiError.config_error("page_size must be positive"))
|
|
328
|
+
normalized_keyword = keyword.strip()
|
|
329
|
+
|
|
330
|
+
if not normalized_keyword:
|
|
331
|
+
def runner(session_profile, context):
|
|
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
|
+
)
|
|
339
|
+
start = (page_num - 1) * page_size
|
|
340
|
+
page_items = items[start : start + page_size]
|
|
341
|
+
reported_total = None if truncated else len(items)
|
|
342
|
+
page_amount = None if truncated else ((len(items) + page_size - 1) // page_size if items else 0)
|
|
343
|
+
if truncated and page_items:
|
|
344
|
+
page_amount = max(page_num + 1, (start + len(page_items) + page_size - 1) // page_size)
|
|
345
|
+
return {
|
|
346
|
+
"profile": profile,
|
|
347
|
+
"ws_id": session_profile.selected_ws_id,
|
|
348
|
+
"request_route": self._request_route_payload(context),
|
|
349
|
+
"items": page_items,
|
|
350
|
+
"pagination": {
|
|
351
|
+
"page": page_num,
|
|
352
|
+
"page_size": page_size,
|
|
353
|
+
"returned_items": len(page_items),
|
|
354
|
+
"reported_total": reported_total,
|
|
355
|
+
"page_amount": page_amount,
|
|
356
|
+
"depth_scanned": deepest_depth + 1 if page_items else 0,
|
|
357
|
+
},
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
raw = self._run(profile, runner)
|
|
361
|
+
items = [item for item in raw.get("items", []) if isinstance(item, dict)]
|
|
362
|
+
return self._public_directory_response(
|
|
363
|
+
raw,
|
|
364
|
+
items=items,
|
|
365
|
+
pagination=raw.get("pagination", {}),
|
|
366
|
+
selection={"keyword": None},
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
def runner(session_profile, context):
|
|
370
|
+
result = self.backend.request(
|
|
371
|
+
"GET",
|
|
372
|
+
context,
|
|
373
|
+
"/contact/deptByPage",
|
|
374
|
+
params={"keyword": normalized_keyword, "pageNum": page_num, "pageSize": page_size},
|
|
375
|
+
)
|
|
376
|
+
return {
|
|
377
|
+
"profile": profile,
|
|
378
|
+
"ws_id": session_profile.selected_ws_id,
|
|
379
|
+
"request_route": self._request_route_payload(context),
|
|
380
|
+
"page": result,
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
raw = self._run(profile, runner)
|
|
384
|
+
items = [item for item in _directory_items(raw.get("page")) if isinstance(item, dict)]
|
|
385
|
+
return self._public_directory_response(
|
|
386
|
+
raw,
|
|
387
|
+
items=items,
|
|
388
|
+
pagination={
|
|
389
|
+
"page": page_num,
|
|
390
|
+
"page_size": page_size,
|
|
391
|
+
"returned_items": len(items),
|
|
392
|
+
"reported_total": _coerce_int(_payload_value(raw.get("page"), "total")),
|
|
393
|
+
"page_amount": _coerce_int(_payload_value(raw.get("page"), "pageAmount")),
|
|
394
|
+
},
|
|
395
|
+
selection={"keyword": normalized_keyword},
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
@tool_cn_name("部门树全量")
|
|
399
|
+
def directory_list_all_departments(
|
|
400
|
+
self,
|
|
401
|
+
*,
|
|
402
|
+
profile: str,
|
|
403
|
+
parent_dept_id: int | None,
|
|
404
|
+
max_depth: int,
|
|
405
|
+
max_items: int,
|
|
406
|
+
) -> dict[str, Any]:
|
|
407
|
+
"""执行组织目录相关逻辑。"""
|
|
408
|
+
if max_depth < 0:
|
|
409
|
+
raise_tool_error(QingflowApiError.config_error("max_depth must be non-negative"))
|
|
410
|
+
if max_items <= 0:
|
|
411
|
+
raise_tool_error(QingflowApiError.config_error("max_items must be positive"))
|
|
412
|
+
|
|
413
|
+
def runner(session_profile, context):
|
|
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:
|
|
421
|
+
items, truncated, deepest_depth = self._walk_department_tree(
|
|
422
|
+
context,
|
|
423
|
+
parent_dept_id=0,
|
|
424
|
+
max_depth=max_depth,
|
|
425
|
+
max_items=max_items,
|
|
426
|
+
)
|
|
427
|
+
return {
|
|
428
|
+
"profile": profile,
|
|
429
|
+
"ws_id": session_profile.selected_ws_id,
|
|
430
|
+
"request_route": self._request_route_payload(context),
|
|
431
|
+
"items": items,
|
|
432
|
+
"pagination": {
|
|
433
|
+
"root_parent_department_id": parent_dept_id,
|
|
434
|
+
"returned_items": len(items),
|
|
435
|
+
"fetched_pages": deepest_depth + 1 if items else 0,
|
|
436
|
+
"is_complete": not truncated,
|
|
437
|
+
"has_more": truncated,
|
|
438
|
+
"max_items": max_items,
|
|
439
|
+
},
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
raw = self._run(profile, runner)
|
|
443
|
+
return self._public_directory_response(
|
|
444
|
+
raw,
|
|
445
|
+
items=[item for item in raw.get("items", []) if isinstance(item, dict)],
|
|
446
|
+
pagination=raw.get("pagination", {}),
|
|
447
|
+
selection={"parent_department_id": parent_dept_id, "max_depth": max_depth, "max_items": max_items},
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
@tool_cn_name("子部门列表")
|
|
451
|
+
def directory_list_sub_departments(self, *, profile: str, parent_dept_id: int | None) -> dict[str, Any]:
|
|
452
|
+
"""执行组织目录相关逻辑。"""
|
|
453
|
+
def runner(session_profile, context):
|
|
454
|
+
params: dict[str, Any] = {}
|
|
455
|
+
if parent_dept_id is not None:
|
|
456
|
+
params["parentDeptId"] = parent_dept_id
|
|
457
|
+
result = self.backend.request("GET", context, "/contact/subDeptList", params=params)
|
|
458
|
+
return {
|
|
459
|
+
"profile": profile,
|
|
460
|
+
"ws_id": session_profile.selected_ws_id,
|
|
461
|
+
"request_route": self._request_route_payload(context),
|
|
462
|
+
"items": result,
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
raw = self._run(profile, runner)
|
|
466
|
+
items = [item for item in raw.get("items", []) if isinstance(item, dict)]
|
|
467
|
+
return self._public_directory_response(
|
|
468
|
+
raw,
|
|
469
|
+
items=items,
|
|
470
|
+
pagination={"returned_items": len(items)},
|
|
471
|
+
selection={"parent_department_id": parent_dept_id},
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
@tool_cn_name("外部联系人列表")
|
|
475
|
+
def directory_list_external_members(
|
|
476
|
+
self,
|
|
477
|
+
*,
|
|
478
|
+
profile: str,
|
|
479
|
+
keyword: str | None,
|
|
480
|
+
page_num: int,
|
|
481
|
+
page_size: int,
|
|
482
|
+
simple: bool,
|
|
483
|
+
) -> dict[str, Any]:
|
|
484
|
+
"""执行组织目录相关逻辑。"""
|
|
485
|
+
def runner(session_profile, context):
|
|
486
|
+
if simple:
|
|
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
|
+
)
|
|
493
|
+
else:
|
|
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)
|
|
498
|
+
return {
|
|
499
|
+
"profile": profile,
|
|
500
|
+
"ws_id": session_profile.selected_ws_id,
|
|
501
|
+
"request_route": self._request_route_payload(context),
|
|
502
|
+
"page": result,
|
|
503
|
+
"simple": simple,
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
raw = self._run(profile, runner)
|
|
507
|
+
items = [item for item in _directory_items(raw.get("page")) if isinstance(item, dict)]
|
|
508
|
+
return self._public_directory_response(
|
|
509
|
+
raw,
|
|
510
|
+
items=items,
|
|
511
|
+
pagination={
|
|
512
|
+
"page": page_num,
|
|
513
|
+
"page_size": page_size,
|
|
514
|
+
"returned_items": len(items),
|
|
515
|
+
"reported_total": _coerce_int(_payload_value(raw.get("page"), "total")),
|
|
516
|
+
"page_amount": _coerce_int(_payload_value(raw.get("page"), "pageAmount")),
|
|
517
|
+
},
|
|
518
|
+
selection={"keyword": keyword, "simple": simple},
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
def _request_route_payload(self, context) -> dict[str, Any]: # type: ignore[no-untyped-def]
|
|
522
|
+
"""执行内部辅助逻辑。"""
|
|
523
|
+
describe_route = getattr(self.backend, "describe_route", None)
|
|
524
|
+
if callable(describe_route):
|
|
525
|
+
payload = describe_route(context)
|
|
526
|
+
if isinstance(payload, dict):
|
|
527
|
+
return payload
|
|
528
|
+
return {
|
|
529
|
+
"base_url": getattr(context, "base_url", None),
|
|
530
|
+
"qf_version": getattr(context, "qf_version", None),
|
|
531
|
+
"qf_version_source": getattr(context, "qf_version_source", None) or ("context" if getattr(context, "qf_version", None) else "unknown"),
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
def _public_directory_response(
|
|
535
|
+
self,
|
|
536
|
+
raw: dict[str, Any],
|
|
537
|
+
*,
|
|
538
|
+
items: list[dict[str, Any]],
|
|
539
|
+
pagination: dict[str, Any],
|
|
540
|
+
selection: dict[str, Any],
|
|
541
|
+
) -> dict[str, Any]:
|
|
542
|
+
"""执行内部辅助逻辑。"""
|
|
543
|
+
response = dict(raw)
|
|
544
|
+
response["ok"] = bool(raw.get("ok", True))
|
|
545
|
+
response["warnings"] = []
|
|
546
|
+
response["output_profile"] = "normal"
|
|
547
|
+
response["data"] = {
|
|
548
|
+
"items": items,
|
|
549
|
+
"pagination": pagination,
|
|
550
|
+
"selection": selection,
|
|
551
|
+
}
|
|
552
|
+
return response
|
|
553
|
+
|
|
554
|
+
def _walk_department_tree(
|
|
555
|
+
self,
|
|
556
|
+
context, # type: ignore[no-untyped-def]
|
|
557
|
+
*,
|
|
558
|
+
parent_dept_id: int | None,
|
|
559
|
+
max_depth: int,
|
|
560
|
+
max_items: int,
|
|
561
|
+
) -> tuple[list[dict[str, Any]], bool, int]:
|
|
562
|
+
"""执行内部辅助逻辑。"""
|
|
563
|
+
queue: deque[tuple[int | None, int]] = deque([(parent_dept_id, 0)])
|
|
564
|
+
seen_ids: set[int] = set()
|
|
565
|
+
requested_parents: set[int | None] = set()
|
|
566
|
+
items: list[dict[str, Any]] = []
|
|
567
|
+
truncated = False
|
|
568
|
+
deepest_depth = 0
|
|
569
|
+
while queue:
|
|
570
|
+
current_parent, depth = queue.popleft()
|
|
571
|
+
if current_parent in requested_parents:
|
|
572
|
+
continue
|
|
573
|
+
requested_parents.add(current_parent)
|
|
574
|
+
params: dict[str, Any] = {}
|
|
575
|
+
if current_parent is not None:
|
|
576
|
+
params["parentDeptId"] = current_parent
|
|
577
|
+
result = self.backend.request("GET", context, "/contact/subDeptList", params=params)
|
|
578
|
+
page_items = _directory_items(result)
|
|
579
|
+
for item in page_items:
|
|
580
|
+
if not isinstance(item, dict):
|
|
581
|
+
continue
|
|
582
|
+
dept_id = _department_id(item)
|
|
583
|
+
if dept_id is None or dept_id in seen_ids:
|
|
584
|
+
continue
|
|
585
|
+
seen_ids.add(dept_id)
|
|
586
|
+
entry = dict(item)
|
|
587
|
+
if current_parent is not None and "parentDeptId" not in entry:
|
|
588
|
+
entry["parentDeptId"] = current_parent
|
|
589
|
+
entry.setdefault("depth", depth)
|
|
590
|
+
items.append(entry)
|
|
591
|
+
deepest_depth = max(deepest_depth, depth)
|
|
592
|
+
if len(items) >= max_items:
|
|
593
|
+
truncated = True
|
|
594
|
+
return items, truncated, deepest_depth
|
|
595
|
+
if depth < max_depth:
|
|
596
|
+
queue.append((dept_id, depth + 1))
|
|
597
|
+
else:
|
|
598
|
+
truncated = True
|
|
599
|
+
return items, truncated, deepest_depth
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _payload_value(payload: Any, key: str) -> Any:
|
|
603
|
+
if isinstance(payload, dict):
|
|
604
|
+
if key in payload:
|
|
605
|
+
return payload.get(key)
|
|
606
|
+
for container_key in ("data", "result", "page"):
|
|
607
|
+
nested = payload.get(container_key)
|
|
608
|
+
if isinstance(nested, dict) and key in nested:
|
|
609
|
+
return nested.get(key)
|
|
610
|
+
return None
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _directory_items(payload: Any) -> list[Any]:
|
|
614
|
+
if isinstance(payload, list):
|
|
615
|
+
return payload
|
|
616
|
+
if not isinstance(payload, dict):
|
|
617
|
+
return []
|
|
618
|
+
for key in ("list", "items", "rows"):
|
|
619
|
+
value = payload.get(key)
|
|
620
|
+
if isinstance(value, list):
|
|
621
|
+
return value
|
|
622
|
+
for key in ("data", "result", "page"):
|
|
623
|
+
nested = payload.get(key)
|
|
624
|
+
if isinstance(nested, list):
|
|
625
|
+
return nested
|
|
626
|
+
if isinstance(nested, dict):
|
|
627
|
+
for nested_key in ("list", "items", "rows", "result"):
|
|
628
|
+
value = nested.get(nested_key)
|
|
629
|
+
if isinstance(value, list):
|
|
630
|
+
return value
|
|
631
|
+
return []
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def _directory_has_more(payload: Any, *, current_page: int, page_size: int, returned_items: int) -> bool:
|
|
635
|
+
page_amount = _coerce_int(_payload_value(payload, "pageAmount"))
|
|
636
|
+
if page_amount is not None:
|
|
637
|
+
return current_page < page_amount
|
|
638
|
+
total = _coerce_int(_payload_value(payload, "total"))
|
|
639
|
+
if total is not None:
|
|
640
|
+
return current_page * page_size < total
|
|
641
|
+
return returned_items >= page_size and returned_items > 0
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def _directory_member_key(item: dict[str, Any]) -> str:
|
|
645
|
+
for key in ("uid", "id", "userId"):
|
|
646
|
+
value = item.get(key)
|
|
647
|
+
if value is not None:
|
|
648
|
+
return f"{key}:{value}"
|
|
649
|
+
for key in ("email", "nickName", "name"):
|
|
650
|
+
value = item.get(key)
|
|
651
|
+
if isinstance(value, str) and value:
|
|
652
|
+
return f"{key}:{value}"
|
|
653
|
+
return repr(sorted(item.items()))
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def _department_id(item: dict[str, Any]) -> int | None:
|
|
657
|
+
return _coerce_int(item.get("deptId", item.get("id")))
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def _coerce_int(value: Any) -> int | None:
|
|
661
|
+
if isinstance(value, bool) or value is None:
|
|
662
|
+
return None
|
|
663
|
+
if isinstance(value, int):
|
|
664
|
+
return value
|
|
665
|
+
if isinstance(value, float):
|
|
666
|
+
return int(value)
|
|
667
|
+
if isinstance(value, str):
|
|
668
|
+
text = value.strip()
|
|
669
|
+
if not text:
|
|
670
|
+
return None
|
|
671
|
+
try:
|
|
672
|
+
return int(text)
|
|
673
|
+
except ValueError:
|
|
674
|
+
return None
|
|
675
|
+
return None
|