@josephyan/qingflow-cli 0.2.0-beta.55
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 +30 -0
- package/docs/local-agent-install.md +235 -0
- package/entry_point.py +13 -0
- package/npm/bin/qingflow.mjs +5 -0
- package/npm/lib/runtime.mjs +204 -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 +5 -0
- package/src/qingflow_mcp/__main__.py +5 -0
- package/src/qingflow_mcp/backend_client.py +547 -0
- package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
- package/src/qingflow_mcp/builder_facade/models.py +985 -0
- package/src/qingflow_mcp/builder_facade/service.py +8243 -0
- package/src/qingflow_mcp/cli/__init__.py +1 -0
- package/src/qingflow_mcp/cli/commands/__init__.py +15 -0
- package/src/qingflow_mcp/cli/commands/app.py +40 -0
- package/src/qingflow_mcp/cli/commands/auth.py +78 -0
- package/src/qingflow_mcp/cli/commands/builder.py +184 -0
- package/src/qingflow_mcp/cli/commands/common.py +47 -0
- package/src/qingflow_mcp/cli/commands/imports.py +86 -0
- package/src/qingflow_mcp/cli/commands/record.py +202 -0
- package/src/qingflow_mcp/cli/commands/task.py +87 -0
- package/src/qingflow_mcp/cli/commands/workspace.py +33 -0
- package/src/qingflow_mcp/cli/context.py +48 -0
- package/src/qingflow_mcp/cli/formatters.py +269 -0
- package/src/qingflow_mcp/cli/json_io.py +50 -0
- package/src/qingflow_mcp/cli/main.py +147 -0
- package/src/qingflow_mcp/config.py +221 -0
- package/src/qingflow_mcp/errors.py +66 -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/server.py +211 -0
- package/src/qingflow_mcp/server_app_builder.py +387 -0
- package/src/qingflow_mcp/server_app_user.py +317 -0
- package/src/qingflow_mcp/session_store.py +289 -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 +466 -0
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +113 -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 +2339 -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 +853 -0
- package/src/qingflow_mcp/tools/__init__.py +1 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +2063 -0
- package/src/qingflow_mcp/tools/app_tools.py +850 -0
- package/src/qingflow_mcp/tools/approval_tools.py +833 -0
- package/src/qingflow_mcp/tools/auth_tools.py +697 -0
- package/src/qingflow_mcp/tools/base.py +81 -0
- package/src/qingflow_mcp/tools/code_block_tools.py +679 -0
- package/src/qingflow_mcp/tools/directory_tools.py +648 -0
- package/src/qingflow_mcp/tools/feedback_tools.py +230 -0
- package/src/qingflow_mcp/tools/file_tools.py +385 -0
- package/src/qingflow_mcp/tools/import_tools.py +1971 -0
- package/src/qingflow_mcp/tools/navigation_tools.py +177 -0
- package/src/qingflow_mcp/tools/package_tools.py +240 -0
- package/src/qingflow_mcp/tools/portal_tools.py +131 -0
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +269 -0
- package/src/qingflow_mcp/tools/record_tools.py +12739 -0
- package/src/qingflow_mcp/tools/role_tools.py +94 -0
- package/src/qingflow_mcp/tools/solution_tools.py +3887 -0
- package/src/qingflow_mcp/tools/task_context_tools.py +1423 -0
- package/src/qingflow_mcp/tools/task_tools.py +843 -0
- package/src/qingflow_mcp/tools/view_tools.py +280 -0
- package/src/qingflow_mcp/tools/workflow_tools.py +312 -0
- package/src/qingflow_mcp/tools/workspace_tools.py +219 -0
|
@@ -0,0 +1,648 @@
|
|
|
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
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
ALLOWED_DIRECTORY_SEARCH_SCOPES = {"MEMBER", "DEPT"}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DirectoryTools(ToolBase):
|
|
17
|
+
def register(self, mcp: FastMCP) -> None:
|
|
18
|
+
@mcp.tool()
|
|
19
|
+
def directory_search(
|
|
20
|
+
profile: str = DEFAULT_PROFILE,
|
|
21
|
+
query: str = "",
|
|
22
|
+
scopes: list[str] | None = None,
|
|
23
|
+
page_num: int = 1,
|
|
24
|
+
page_size: int = 20,
|
|
25
|
+
) -> dict[str, Any]:
|
|
26
|
+
return self.directory_search(
|
|
27
|
+
profile=profile,
|
|
28
|
+
query=query,
|
|
29
|
+
scopes=scopes,
|
|
30
|
+
page_num=page_num,
|
|
31
|
+
page_size=page_size,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
@mcp.tool()
|
|
35
|
+
def directory_list_internal_users(
|
|
36
|
+
profile: str = DEFAULT_PROFILE,
|
|
37
|
+
keyword: str | None = None,
|
|
38
|
+
department_id: int | None = None,
|
|
39
|
+
role_id: int | None = None,
|
|
40
|
+
page_num: int = 1,
|
|
41
|
+
page_size: int = 20,
|
|
42
|
+
include_disabled: bool = False,
|
|
43
|
+
) -> dict[str, Any]:
|
|
44
|
+
return self.directory_list_internal_users(
|
|
45
|
+
profile=profile,
|
|
46
|
+
keyword=keyword,
|
|
47
|
+
dept_id=department_id,
|
|
48
|
+
role_id=role_id,
|
|
49
|
+
page_num=page_num,
|
|
50
|
+
page_size=page_size,
|
|
51
|
+
contain_disable=include_disabled,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
@mcp.tool()
|
|
55
|
+
def directory_list_all_internal_users(
|
|
56
|
+
profile: str = DEFAULT_PROFILE,
|
|
57
|
+
keyword: str | None = None,
|
|
58
|
+
department_id: int | None = None,
|
|
59
|
+
role_id: int | None = None,
|
|
60
|
+
page_size: int = 200,
|
|
61
|
+
include_disabled: bool = False,
|
|
62
|
+
max_pages: int = 100,
|
|
63
|
+
) -> dict[str, Any]:
|
|
64
|
+
return self.directory_list_all_internal_users(
|
|
65
|
+
profile=profile,
|
|
66
|
+
keyword=keyword,
|
|
67
|
+
dept_id=department_id,
|
|
68
|
+
role_id=role_id,
|
|
69
|
+
page_size=page_size,
|
|
70
|
+
contain_disable=include_disabled,
|
|
71
|
+
max_pages=max_pages,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
@mcp.tool()
|
|
75
|
+
def directory_list_internal_departments(
|
|
76
|
+
profile: str = DEFAULT_PROFILE,
|
|
77
|
+
keyword: str = "",
|
|
78
|
+
page_num: int = 1,
|
|
79
|
+
page_size: int = 20,
|
|
80
|
+
) -> dict[str, Any]:
|
|
81
|
+
return self.directory_list_internal_departments(
|
|
82
|
+
profile=profile,
|
|
83
|
+
keyword=keyword,
|
|
84
|
+
page_num=page_num,
|
|
85
|
+
page_size=page_size,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
@mcp.tool()
|
|
89
|
+
def directory_list_all_departments(
|
|
90
|
+
profile: str = DEFAULT_PROFILE,
|
|
91
|
+
parent_department_id: int | None = None,
|
|
92
|
+
max_depth: int = 20,
|
|
93
|
+
max_items: int = 2000,
|
|
94
|
+
) -> dict[str, Any]:
|
|
95
|
+
return self.directory_list_all_departments(
|
|
96
|
+
profile=profile,
|
|
97
|
+
parent_dept_id=parent_department_id,
|
|
98
|
+
max_depth=max_depth,
|
|
99
|
+
max_items=max_items,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
@mcp.tool()
|
|
103
|
+
def directory_list_sub_departments(
|
|
104
|
+
profile: str = DEFAULT_PROFILE,
|
|
105
|
+
parent_department_id: int | None = None,
|
|
106
|
+
) -> dict[str, Any]:
|
|
107
|
+
return self.directory_list_sub_departments(profile=profile, parent_dept_id=parent_department_id)
|
|
108
|
+
|
|
109
|
+
@mcp.tool()
|
|
110
|
+
def directory_list_external_members(
|
|
111
|
+
profile: str = DEFAULT_PROFILE,
|
|
112
|
+
keyword: str | None = None,
|
|
113
|
+
page_num: int = 1,
|
|
114
|
+
page_size: int = 20,
|
|
115
|
+
simple: bool = False,
|
|
116
|
+
) -> dict[str, Any]:
|
|
117
|
+
return self.directory_list_external_members(
|
|
118
|
+
profile=profile,
|
|
119
|
+
keyword=keyword,
|
|
120
|
+
page_num=page_num,
|
|
121
|
+
page_size=page_size,
|
|
122
|
+
simple=simple,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def directory_search(
|
|
126
|
+
self,
|
|
127
|
+
*,
|
|
128
|
+
profile: str,
|
|
129
|
+
query: str,
|
|
130
|
+
scopes: list[str] | None,
|
|
131
|
+
page_num: int,
|
|
132
|
+
page_size: int,
|
|
133
|
+
) -> dict[str, Any]:
|
|
134
|
+
normalized_scopes = scopes or ["MEMBER", "DEPT"]
|
|
135
|
+
invalid_scopes = [scope for scope in normalized_scopes if scope not in ALLOWED_DIRECTORY_SEARCH_SCOPES]
|
|
136
|
+
if invalid_scopes:
|
|
137
|
+
raise_tool_error(QingflowApiError.not_supported(f"directory_search only supports internal scopes {sorted(ALLOWED_DIRECTORY_SEARCH_SCOPES)}; got {invalid_scopes}"))
|
|
138
|
+
if not query:
|
|
139
|
+
raise_tool_error(QingflowApiError.config_error("query is required"))
|
|
140
|
+
|
|
141
|
+
def runner(session_profile, context):
|
|
142
|
+
result = self.backend.request(
|
|
143
|
+
"POST",
|
|
144
|
+
context,
|
|
145
|
+
"/member/search",
|
|
146
|
+
json_body={
|
|
147
|
+
"dimensions": normalized_scopes,
|
|
148
|
+
"searchKey": query,
|
|
149
|
+
"pageNum": page_num,
|
|
150
|
+
"pageSize": page_size,
|
|
151
|
+
},
|
|
152
|
+
)
|
|
153
|
+
return {
|
|
154
|
+
"profile": profile,
|
|
155
|
+
"ws_id": session_profile.selected_ws_id,
|
|
156
|
+
"request_route": self._request_route_payload(context),
|
|
157
|
+
"result": result,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
raw = self._run(profile, runner)
|
|
161
|
+
items = [item for item in _directory_items(raw.get("result")) if isinstance(item, dict)]
|
|
162
|
+
return self._public_directory_response(
|
|
163
|
+
raw,
|
|
164
|
+
items=items,
|
|
165
|
+
pagination={
|
|
166
|
+
"page": page_num,
|
|
167
|
+
"page_size": page_size,
|
|
168
|
+
"returned_items": len(items),
|
|
169
|
+
"reported_total": _coerce_int(_payload_value(raw.get("result"), "total")),
|
|
170
|
+
"page_amount": _coerce_int(_payload_value(raw.get("result"), "pageAmount")),
|
|
171
|
+
},
|
|
172
|
+
selection={"query": query, "scopes": normalized_scopes},
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def directory_list_internal_users(
|
|
176
|
+
self,
|
|
177
|
+
*,
|
|
178
|
+
profile: str,
|
|
179
|
+
keyword: str | None,
|
|
180
|
+
dept_id: int | None,
|
|
181
|
+
role_id: int | None,
|
|
182
|
+
page_num: int,
|
|
183
|
+
page_size: int,
|
|
184
|
+
contain_disable: bool,
|
|
185
|
+
) -> dict[str, Any]:
|
|
186
|
+
def runner(session_profile, context):
|
|
187
|
+
params: dict[str, Any] = {
|
|
188
|
+
"pageNum": page_num,
|
|
189
|
+
"pageSize": page_size,
|
|
190
|
+
"containDisable": contain_disable,
|
|
191
|
+
}
|
|
192
|
+
if keyword:
|
|
193
|
+
params["keyword"] = keyword
|
|
194
|
+
if dept_id is not None:
|
|
195
|
+
params["deptId"] = dept_id
|
|
196
|
+
if role_id is not None:
|
|
197
|
+
params["roleId"] = role_id
|
|
198
|
+
result = self.backend.request("GET", context, "/contact", params=params)
|
|
199
|
+
return {
|
|
200
|
+
"profile": profile,
|
|
201
|
+
"ws_id": session_profile.selected_ws_id,
|
|
202
|
+
"request_route": self._request_route_payload(context),
|
|
203
|
+
"result": result,
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
raw = self._run(profile, runner)
|
|
207
|
+
items = [item for item in _directory_items(raw.get("result")) if isinstance(item, dict)]
|
|
208
|
+
return self._public_directory_response(
|
|
209
|
+
raw,
|
|
210
|
+
items=items,
|
|
211
|
+
pagination={
|
|
212
|
+
"page": page_num,
|
|
213
|
+
"page_size": page_size,
|
|
214
|
+
"returned_items": len(items),
|
|
215
|
+
"reported_total": _coerce_int(_payload_value(raw.get("result"), "total")),
|
|
216
|
+
"page_amount": _coerce_int(_payload_value(raw.get("result"), "pageAmount")),
|
|
217
|
+
},
|
|
218
|
+
selection={"keyword": keyword, "department_id": dept_id, "role_id": role_id, "include_disabled": contain_disable},
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def directory_list_all_internal_users(
|
|
222
|
+
self,
|
|
223
|
+
*,
|
|
224
|
+
profile: str,
|
|
225
|
+
keyword: str | None,
|
|
226
|
+
dept_id: int | None,
|
|
227
|
+
role_id: int | None,
|
|
228
|
+
page_size: int,
|
|
229
|
+
contain_disable: bool,
|
|
230
|
+
max_pages: int,
|
|
231
|
+
) -> dict[str, Any]:
|
|
232
|
+
if page_size <= 0:
|
|
233
|
+
raise_tool_error(QingflowApiError.config_error("page_size must be positive"))
|
|
234
|
+
if max_pages <= 0:
|
|
235
|
+
raise_tool_error(QingflowApiError.config_error("max_pages must be positive"))
|
|
236
|
+
|
|
237
|
+
def runner(session_profile, context):
|
|
238
|
+
current_page = 1
|
|
239
|
+
fetched_pages = 0
|
|
240
|
+
has_more = False
|
|
241
|
+
reported_total: int | None = None
|
|
242
|
+
seen_keys: set[str] = set()
|
|
243
|
+
items: list[dict[str, Any]] = []
|
|
244
|
+
while fetched_pages < max_pages:
|
|
245
|
+
params: dict[str, Any] = {
|
|
246
|
+
"pageNum": current_page,
|
|
247
|
+
"pageSize": page_size,
|
|
248
|
+
"containDisable": contain_disable,
|
|
249
|
+
}
|
|
250
|
+
if keyword:
|
|
251
|
+
params["keyword"] = keyword
|
|
252
|
+
if dept_id is not None:
|
|
253
|
+
params["deptId"] = dept_id
|
|
254
|
+
if role_id is not None:
|
|
255
|
+
params["roleId"] = role_id
|
|
256
|
+
result = self.backend.request("GET", context, "/contact", params=params)
|
|
257
|
+
page_items = _directory_items(result)
|
|
258
|
+
if reported_total is None:
|
|
259
|
+
reported_total = _coerce_int(_payload_value(result, "total"))
|
|
260
|
+
for item in page_items:
|
|
261
|
+
if not isinstance(item, dict):
|
|
262
|
+
continue
|
|
263
|
+
member_key = _directory_member_key(item)
|
|
264
|
+
if member_key in seen_keys:
|
|
265
|
+
continue
|
|
266
|
+
seen_keys.add(member_key)
|
|
267
|
+
items.append(dict(item))
|
|
268
|
+
fetched_pages += 1
|
|
269
|
+
has_more = _directory_has_more(result, current_page=current_page, page_size=page_size, returned_items=len(page_items))
|
|
270
|
+
if not has_more:
|
|
271
|
+
break
|
|
272
|
+
current_page += 1
|
|
273
|
+
return {
|
|
274
|
+
"profile": profile,
|
|
275
|
+
"ws_id": session_profile.selected_ws_id,
|
|
276
|
+
"request_route": self._request_route_payload(context),
|
|
277
|
+
"items": items,
|
|
278
|
+
"pagination": {
|
|
279
|
+
"page_size": page_size,
|
|
280
|
+
"fetched_pages": fetched_pages,
|
|
281
|
+
"returned_items": len(items),
|
|
282
|
+
"reported_total": reported_total,
|
|
283
|
+
"is_complete": not has_more,
|
|
284
|
+
"has_more": has_more,
|
|
285
|
+
"next_page_num": current_page + 1 if has_more else None,
|
|
286
|
+
"max_pages": max_pages,
|
|
287
|
+
},
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
raw = self._run(profile, runner)
|
|
291
|
+
return self._public_directory_response(
|
|
292
|
+
raw,
|
|
293
|
+
items=[item for item in raw.get("items", []) if isinstance(item, dict)],
|
|
294
|
+
pagination=raw.get("pagination", {}),
|
|
295
|
+
selection={"keyword": keyword, "department_id": dept_id, "role_id": role_id, "include_disabled": contain_disable},
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
def directory_list_internal_departments(
|
|
299
|
+
self,
|
|
300
|
+
*,
|
|
301
|
+
profile: str,
|
|
302
|
+
keyword: str,
|
|
303
|
+
page_num: int,
|
|
304
|
+
page_size: int,
|
|
305
|
+
) -> dict[str, Any]:
|
|
306
|
+
if page_num <= 0:
|
|
307
|
+
raise_tool_error(QingflowApiError.config_error("page_num must be positive"))
|
|
308
|
+
if page_size <= 0:
|
|
309
|
+
raise_tool_error(QingflowApiError.config_error("page_size must be positive"))
|
|
310
|
+
normalized_keyword = keyword.strip()
|
|
311
|
+
|
|
312
|
+
if not normalized_keyword:
|
|
313
|
+
def runner(session_profile, context):
|
|
314
|
+
fetch_limit = max((page_num + 1) * page_size + 1, page_size + 1)
|
|
315
|
+
items, truncated, deepest_depth = self._walk_department_tree(
|
|
316
|
+
context,
|
|
317
|
+
parent_dept_id=None,
|
|
318
|
+
max_depth=20,
|
|
319
|
+
max_items=fetch_limit,
|
|
320
|
+
)
|
|
321
|
+
start = (page_num - 1) * page_size
|
|
322
|
+
page_items = items[start : start + page_size]
|
|
323
|
+
reported_total = None if truncated else len(items)
|
|
324
|
+
page_amount = None if truncated else ((len(items) + page_size - 1) // page_size if items else 0)
|
|
325
|
+
if truncated and page_items:
|
|
326
|
+
page_amount = max(page_num + 1, (start + len(page_items) + page_size - 1) // page_size)
|
|
327
|
+
return {
|
|
328
|
+
"profile": profile,
|
|
329
|
+
"ws_id": session_profile.selected_ws_id,
|
|
330
|
+
"request_route": self._request_route_payload(context),
|
|
331
|
+
"items": page_items,
|
|
332
|
+
"pagination": {
|
|
333
|
+
"page": page_num,
|
|
334
|
+
"page_size": page_size,
|
|
335
|
+
"returned_items": len(page_items),
|
|
336
|
+
"reported_total": reported_total,
|
|
337
|
+
"page_amount": page_amount,
|
|
338
|
+
"depth_scanned": deepest_depth + 1 if page_items else 0,
|
|
339
|
+
},
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
raw = self._run(profile, runner)
|
|
343
|
+
items = [item for item in raw.get("items", []) if isinstance(item, dict)]
|
|
344
|
+
return self._public_directory_response(
|
|
345
|
+
raw,
|
|
346
|
+
items=items,
|
|
347
|
+
pagination=raw.get("pagination", {}),
|
|
348
|
+
selection={"keyword": None},
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
def runner(session_profile, context):
|
|
352
|
+
result = self.backend.request(
|
|
353
|
+
"GET",
|
|
354
|
+
context,
|
|
355
|
+
"/contact/deptByPage",
|
|
356
|
+
params={"keyword": normalized_keyword, "pageNum": page_num, "pageSize": page_size},
|
|
357
|
+
)
|
|
358
|
+
return {
|
|
359
|
+
"profile": profile,
|
|
360
|
+
"ws_id": session_profile.selected_ws_id,
|
|
361
|
+
"request_route": self._request_route_payload(context),
|
|
362
|
+
"page": result,
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
raw = self._run(profile, runner)
|
|
366
|
+
items = [item for item in _directory_items(raw.get("page")) if isinstance(item, dict)]
|
|
367
|
+
return self._public_directory_response(
|
|
368
|
+
raw,
|
|
369
|
+
items=items,
|
|
370
|
+
pagination={
|
|
371
|
+
"page": page_num,
|
|
372
|
+
"page_size": page_size,
|
|
373
|
+
"returned_items": len(items),
|
|
374
|
+
"reported_total": _coerce_int(_payload_value(raw.get("page"), "total")),
|
|
375
|
+
"page_amount": _coerce_int(_payload_value(raw.get("page"), "pageAmount")),
|
|
376
|
+
},
|
|
377
|
+
selection={"keyword": normalized_keyword},
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
def directory_list_all_departments(
|
|
381
|
+
self,
|
|
382
|
+
*,
|
|
383
|
+
profile: str,
|
|
384
|
+
parent_dept_id: int | None,
|
|
385
|
+
max_depth: int,
|
|
386
|
+
max_items: int,
|
|
387
|
+
) -> dict[str, Any]:
|
|
388
|
+
if max_depth < 0:
|
|
389
|
+
raise_tool_error(QingflowApiError.config_error("max_depth must be non-negative"))
|
|
390
|
+
if max_items <= 0:
|
|
391
|
+
raise_tool_error(QingflowApiError.config_error("max_items must be positive"))
|
|
392
|
+
|
|
393
|
+
def runner(session_profile, context):
|
|
394
|
+
items, truncated, deepest_depth = self._walk_department_tree(
|
|
395
|
+
context,
|
|
396
|
+
parent_dept_id=parent_dept_id,
|
|
397
|
+
max_depth=max_depth,
|
|
398
|
+
max_items=max_items,
|
|
399
|
+
)
|
|
400
|
+
if not items and parent_dept_id is None:
|
|
401
|
+
items, truncated, deepest_depth = self._walk_department_tree(
|
|
402
|
+
context,
|
|
403
|
+
parent_dept_id=0,
|
|
404
|
+
max_depth=max_depth,
|
|
405
|
+
max_items=max_items,
|
|
406
|
+
)
|
|
407
|
+
return {
|
|
408
|
+
"profile": profile,
|
|
409
|
+
"ws_id": session_profile.selected_ws_id,
|
|
410
|
+
"request_route": self._request_route_payload(context),
|
|
411
|
+
"items": items,
|
|
412
|
+
"pagination": {
|
|
413
|
+
"root_parent_department_id": parent_dept_id,
|
|
414
|
+
"returned_items": len(items),
|
|
415
|
+
"fetched_pages": deepest_depth + 1 if items else 0,
|
|
416
|
+
"is_complete": not truncated,
|
|
417
|
+
"has_more": truncated,
|
|
418
|
+
"max_items": max_items,
|
|
419
|
+
},
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
raw = self._run(profile, runner)
|
|
423
|
+
return self._public_directory_response(
|
|
424
|
+
raw,
|
|
425
|
+
items=[item for item in raw.get("items", []) if isinstance(item, dict)],
|
|
426
|
+
pagination=raw.get("pagination", {}),
|
|
427
|
+
selection={"parent_department_id": parent_dept_id, "max_depth": max_depth, "max_items": max_items},
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
def directory_list_sub_departments(self, *, profile: str, parent_dept_id: int | None) -> dict[str, Any]:
|
|
431
|
+
def runner(session_profile, context):
|
|
432
|
+
params: dict[str, Any] = {}
|
|
433
|
+
if parent_dept_id is not None:
|
|
434
|
+
params["parentDeptId"] = parent_dept_id
|
|
435
|
+
result = self.backend.request("GET", context, "/contact/subDeptList", params=params)
|
|
436
|
+
return {
|
|
437
|
+
"profile": profile,
|
|
438
|
+
"ws_id": session_profile.selected_ws_id,
|
|
439
|
+
"request_route": self._request_route_payload(context),
|
|
440
|
+
"items": result,
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
raw = self._run(profile, runner)
|
|
444
|
+
items = [item for item in raw.get("items", []) if isinstance(item, dict)]
|
|
445
|
+
return self._public_directory_response(
|
|
446
|
+
raw,
|
|
447
|
+
items=items,
|
|
448
|
+
pagination={"returned_items": len(items)},
|
|
449
|
+
selection={"parent_department_id": parent_dept_id},
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
def directory_list_external_members(
|
|
453
|
+
self,
|
|
454
|
+
*,
|
|
455
|
+
profile: str,
|
|
456
|
+
keyword: str | None,
|
|
457
|
+
page_num: int,
|
|
458
|
+
page_size: int,
|
|
459
|
+
simple: bool,
|
|
460
|
+
) -> dict[str, Any]:
|
|
461
|
+
def runner(session_profile, context):
|
|
462
|
+
if simple:
|
|
463
|
+
result = self.backend.request(
|
|
464
|
+
"POST",
|
|
465
|
+
context,
|
|
466
|
+
"/external/member/simple/pageList",
|
|
467
|
+
json_body={"keyword": keyword, "pageNum": page_num, "pageSize": page_size},
|
|
468
|
+
)
|
|
469
|
+
else:
|
|
470
|
+
params: dict[str, Any] = {"pageNum": page_num, "pageSize": page_size}
|
|
471
|
+
if keyword:
|
|
472
|
+
params["keyword"] = keyword
|
|
473
|
+
result = self.backend.request("GET", context, "/external/member/pageList", params=params)
|
|
474
|
+
return {
|
|
475
|
+
"profile": profile,
|
|
476
|
+
"ws_id": session_profile.selected_ws_id,
|
|
477
|
+
"request_route": self._request_route_payload(context),
|
|
478
|
+
"page": result,
|
|
479
|
+
"simple": simple,
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
raw = self._run(profile, runner)
|
|
483
|
+
items = [item for item in _directory_items(raw.get("page")) if isinstance(item, dict)]
|
|
484
|
+
return self._public_directory_response(
|
|
485
|
+
raw,
|
|
486
|
+
items=items,
|
|
487
|
+
pagination={
|
|
488
|
+
"page": page_num,
|
|
489
|
+
"page_size": page_size,
|
|
490
|
+
"returned_items": len(items),
|
|
491
|
+
"reported_total": _coerce_int(_payload_value(raw.get("page"), "total")),
|
|
492
|
+
"page_amount": _coerce_int(_payload_value(raw.get("page"), "pageAmount")),
|
|
493
|
+
},
|
|
494
|
+
selection={"keyword": keyword, "simple": simple},
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
def _request_route_payload(self, context) -> dict[str, Any]: # type: ignore[no-untyped-def]
|
|
498
|
+
describe_route = getattr(self.backend, "describe_route", None)
|
|
499
|
+
if callable(describe_route):
|
|
500
|
+
payload = describe_route(context)
|
|
501
|
+
if isinstance(payload, dict):
|
|
502
|
+
return payload
|
|
503
|
+
return {
|
|
504
|
+
"base_url": getattr(context, "base_url", None),
|
|
505
|
+
"qf_version": getattr(context, "qf_version", None),
|
|
506
|
+
"qf_version_source": getattr(context, "qf_version_source", None) or ("context" if getattr(context, "qf_version", None) else "unknown"),
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
def _public_directory_response(
|
|
510
|
+
self,
|
|
511
|
+
raw: dict[str, Any],
|
|
512
|
+
*,
|
|
513
|
+
items: list[dict[str, Any]],
|
|
514
|
+
pagination: dict[str, Any],
|
|
515
|
+
selection: dict[str, Any],
|
|
516
|
+
) -> dict[str, Any]:
|
|
517
|
+
response = dict(raw)
|
|
518
|
+
response["ok"] = bool(raw.get("ok", True))
|
|
519
|
+
response["warnings"] = []
|
|
520
|
+
response["output_profile"] = "normal"
|
|
521
|
+
response["data"] = {
|
|
522
|
+
"items": items,
|
|
523
|
+
"pagination": pagination,
|
|
524
|
+
"selection": selection,
|
|
525
|
+
}
|
|
526
|
+
return response
|
|
527
|
+
|
|
528
|
+
def _walk_department_tree(
|
|
529
|
+
self,
|
|
530
|
+
context, # type: ignore[no-untyped-def]
|
|
531
|
+
*,
|
|
532
|
+
parent_dept_id: int | None,
|
|
533
|
+
max_depth: int,
|
|
534
|
+
max_items: int,
|
|
535
|
+
) -> tuple[list[dict[str, Any]], bool, int]:
|
|
536
|
+
queue: deque[tuple[int | None, int]] = deque([(parent_dept_id, 0)])
|
|
537
|
+
seen_ids: set[int] = set()
|
|
538
|
+
requested_parents: set[int | None] = set()
|
|
539
|
+
items: list[dict[str, Any]] = []
|
|
540
|
+
truncated = False
|
|
541
|
+
deepest_depth = 0
|
|
542
|
+
while queue:
|
|
543
|
+
current_parent, depth = queue.popleft()
|
|
544
|
+
if current_parent in requested_parents:
|
|
545
|
+
continue
|
|
546
|
+
requested_parents.add(current_parent)
|
|
547
|
+
params: dict[str, Any] = {}
|
|
548
|
+
if current_parent is not None:
|
|
549
|
+
params["parentDeptId"] = current_parent
|
|
550
|
+
result = self.backend.request("GET", context, "/contact/subDeptList", params=params)
|
|
551
|
+
page_items = _directory_items(result)
|
|
552
|
+
for item in page_items:
|
|
553
|
+
if not isinstance(item, dict):
|
|
554
|
+
continue
|
|
555
|
+
dept_id = _department_id(item)
|
|
556
|
+
if dept_id is None or dept_id in seen_ids:
|
|
557
|
+
continue
|
|
558
|
+
seen_ids.add(dept_id)
|
|
559
|
+
entry = dict(item)
|
|
560
|
+
if current_parent is not None and "parentDeptId" not in entry:
|
|
561
|
+
entry["parentDeptId"] = current_parent
|
|
562
|
+
entry.setdefault("depth", depth)
|
|
563
|
+
items.append(entry)
|
|
564
|
+
deepest_depth = max(deepest_depth, depth)
|
|
565
|
+
if len(items) >= max_items:
|
|
566
|
+
truncated = True
|
|
567
|
+
return items, truncated, deepest_depth
|
|
568
|
+
if depth < max_depth:
|
|
569
|
+
queue.append((dept_id, depth + 1))
|
|
570
|
+
else:
|
|
571
|
+
truncated = True
|
|
572
|
+
return items, truncated, deepest_depth
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def _payload_value(payload: Any, key: str) -> Any:
|
|
576
|
+
if isinstance(payload, dict):
|
|
577
|
+
if key in payload:
|
|
578
|
+
return payload.get(key)
|
|
579
|
+
for container_key in ("data", "result", "page"):
|
|
580
|
+
nested = payload.get(container_key)
|
|
581
|
+
if isinstance(nested, dict) and key in nested:
|
|
582
|
+
return nested.get(key)
|
|
583
|
+
return None
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def _directory_items(payload: Any) -> list[Any]:
|
|
587
|
+
if isinstance(payload, list):
|
|
588
|
+
return payload
|
|
589
|
+
if not isinstance(payload, dict):
|
|
590
|
+
return []
|
|
591
|
+
for key in ("list", "items", "rows"):
|
|
592
|
+
value = payload.get(key)
|
|
593
|
+
if isinstance(value, list):
|
|
594
|
+
return value
|
|
595
|
+
for key in ("data", "result", "page"):
|
|
596
|
+
nested = payload.get(key)
|
|
597
|
+
if isinstance(nested, list):
|
|
598
|
+
return nested
|
|
599
|
+
if isinstance(nested, dict):
|
|
600
|
+
for nested_key in ("list", "items", "rows", "result"):
|
|
601
|
+
value = nested.get(nested_key)
|
|
602
|
+
if isinstance(value, list):
|
|
603
|
+
return value
|
|
604
|
+
return []
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def _directory_has_more(payload: Any, *, current_page: int, page_size: int, returned_items: int) -> bool:
|
|
608
|
+
page_amount = _coerce_int(_payload_value(payload, "pageAmount"))
|
|
609
|
+
if page_amount is not None:
|
|
610
|
+
return current_page < page_amount
|
|
611
|
+
total = _coerce_int(_payload_value(payload, "total"))
|
|
612
|
+
if total is not None:
|
|
613
|
+
return current_page * page_size < total
|
|
614
|
+
return returned_items >= page_size and returned_items > 0
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def _directory_member_key(item: dict[str, Any]) -> str:
|
|
618
|
+
for key in ("uid", "id", "userId"):
|
|
619
|
+
value = item.get(key)
|
|
620
|
+
if value is not None:
|
|
621
|
+
return f"{key}:{value}"
|
|
622
|
+
for key in ("email", "nickName", "name"):
|
|
623
|
+
value = item.get(key)
|
|
624
|
+
if isinstance(value, str) and value:
|
|
625
|
+
return f"{key}:{value}"
|
|
626
|
+
return repr(sorted(item.items()))
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _department_id(item: dict[str, Any]) -> int | None:
|
|
630
|
+
return _coerce_int(item.get("deptId", item.get("id")))
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _coerce_int(value: Any) -> int | None:
|
|
634
|
+
if isinstance(value, bool) or value is None:
|
|
635
|
+
return None
|
|
636
|
+
if isinstance(value, int):
|
|
637
|
+
return value
|
|
638
|
+
if isinstance(value, float):
|
|
639
|
+
return int(value)
|
|
640
|
+
if isinstance(value, str):
|
|
641
|
+
text = value.strip()
|
|
642
|
+
if not text:
|
|
643
|
+
return None
|
|
644
|
+
try:
|
|
645
|
+
return int(text)
|
|
646
|
+
except ValueError:
|
|
647
|
+
return None
|
|
648
|
+
return None
|