@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.
Files changed (98) hide show
  1. package/README.md +5 -6
  2. package/docs/local-agent-install.md +6 -57
  3. package/entry_point.py +1 -1
  4. package/npm/bin/qingflow-app-builder-mcp.mjs +33 -2
  5. package/npm/lib/runtime.mjs +101 -21
  6. package/npm/scripts/postinstall.mjs +10 -1
  7. package/package.json +2 -3
  8. package/pyproject.toml +1 -1
  9. package/skills/qingflow-app-builder/SKILL.md +42 -104
  10. package/skills/qingflow-app-builder/references/build-complete-system.md +428 -0
  11. package/skills/qingflow-app-builder/references/build-single-app.md +530 -0
  12. package/skills/qingflow-app-builder/references/create-app.md +11 -30
  13. package/skills/qingflow-app-builder/references/environments.md +1 -1
  14. package/skills/qingflow-app-builder/references/gotchas.md +9 -31
  15. package/skills/qingflow-app-builder/references/match-rules.md +0 -15
  16. package/skills/qingflow-app-builder/references/solution-playbooks.md +0 -10
  17. package/skills/qingflow-app-builder/references/tool-selection.md +23 -38
  18. package/skills/qingflow-app-builder/references/update-flow.md +38 -22
  19. package/skills/qingflow-app-builder/references/update-schema.md +0 -14
  20. package/skills/qingflow-app-builder/references/update-views.md +14 -83
  21. package/skills/qingflow-app-builder-code-integrations/SKILL.md +3 -5
  22. package/skills/qingflow-app-builder-code-integrations/references/code-block.md +1 -1
  23. package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +1 -1
  24. package/skills/qingflow-mcp-setup/SKILL.md +111 -0
  25. package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
  26. package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
  27. package/skills/qingflow-mcp-setup/references/environments.md +61 -0
  28. package/skills/qingflow-mcp-setup/references/generic-stdio.md +31 -0
  29. package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
  30. package/skills/qingflow-workflow-builder/SKILL.md +96 -0
  31. package/skills/qingflow-workflow-builder/manifest.yaml +8 -0
  32. package/skills/qingflow-workflow-builder/references/01-overview.md +45 -0
  33. package/skills/qingflow-workflow-builder/references/02-update-mode.md +53 -0
  34. package/skills/qingflow-workflow-builder/references/03-flow-patterns.md +57 -0
  35. package/skills/qingflow-workflow-builder/references/04-stage1-business-modeling.md +131 -0
  36. package/skills/qingflow-workflow-builder/references/05-stage2-members-roles.md +29 -0
  37. package/skills/qingflow-workflow-builder/references/06-stage3-build-spec.md +165 -0
  38. package/skills/qingflow-workflow-builder/references/07-stage4-validate-spec.md +33 -0
  39. package/skills/qingflow-workflow-builder/references/08-stage5-apply-verify.md +51 -0
  40. package/skills/qingflow-workflow-builder/references/09-stage6-summary.md +88 -0
  41. package/skills/qingflow-workflow-builder/references/10-node-config-reference.md +93 -0
  42. package/skills/qingflow-workflow-builder/references/11-troubleshooting.md +15 -0
  43. package/skills/qingflow-workflow-builder/scripts/diff_flow_spec.py +275 -0
  44. package/skills/qingflow-workflow-builder/scripts/validate_flow_spec.py +605 -0
  45. package/src/qingflow_mcp/__init__.py +1 -1
  46. package/src/qingflow_mcp/__main__.py +2 -6
  47. package/src/qingflow_mcp/builder_facade/models.py +108 -485
  48. package/src/qingflow_mcp/builder_facade/service.py +1686 -6674
  49. package/src/qingflow_mcp/cli/commands/builder.py +364 -322
  50. package/src/qingflow_mcp/cli/commands/chart.py +1 -1
  51. package/src/qingflow_mcp/cli/commands/common.py +3 -12
  52. package/src/qingflow_mcp/cli/commands/exports.py +2 -2
  53. package/src/qingflow_mcp/cli/commands/imports.py +3 -3
  54. package/src/qingflow_mcp/cli/commands/portal.py +2 -2
  55. package/src/qingflow_mcp/cli/commands/record.py +27 -101
  56. package/src/qingflow_mcp/cli/commands/task.py +47 -28
  57. package/src/qingflow_mcp/cli/commands/view.py +1 -1
  58. package/src/qingflow_mcp/cli/context.py +3 -0
  59. package/src/qingflow_mcp/cli/formatters.py +16 -784
  60. package/src/qingflow_mcp/cli/main.py +33 -117
  61. package/src/qingflow_mcp/errors.py +2 -43
  62. package/src/qingflow_mcp/public_surface.py +17 -26
  63. package/src/qingflow_mcp/response_trim.py +17 -81
  64. package/src/qingflow_mcp/server.py +12 -14
  65. package/src/qingflow_mcp/server_app_builder.py +36 -79
  66. package/src/qingflow_mcp/server_app_user.py +16 -22
  67. package/src/qingflow_mcp/session_store.py +7 -11
  68. package/src/qingflow_mcp/solution/compiler/__init__.py +1 -3
  69. package/src/qingflow_mcp/solution/executor.py +18 -245
  70. package/src/qingflow_mcp/tools/ai_builder_tools.py +1396 -3100
  71. package/src/qingflow_mcp/tools/app_tools.py +43 -184
  72. package/src/qingflow_mcp/tools/approval_tools.py +35 -197
  73. package/src/qingflow_mcp/tools/auth_tools.py +16 -92
  74. package/src/qingflow_mcp/tools/code_block_tools.py +40 -298
  75. package/src/qingflow_mcp/tools/custom_button_tools.py +10 -64
  76. package/src/qingflow_mcp/tools/directory_tools.py +72 -236
  77. package/src/qingflow_mcp/tools/export_tools.py +34 -244
  78. package/src/qingflow_mcp/tools/feedback_tools.py +0 -9
  79. package/src/qingflow_mcp/tools/file_tools.py +3 -9
  80. package/src/qingflow_mcp/tools/import_tools.py +49 -336
  81. package/src/qingflow_mcp/tools/navigation_tools.py +12 -91
  82. package/src/qingflow_mcp/tools/package_tools.py +6 -118
  83. package/src/qingflow_mcp/tools/portal_tools.py +3 -39
  84. package/src/qingflow_mcp/tools/qingbi_report_tools.py +7 -116
  85. package/src/qingflow_mcp/tools/record_tools.py +360 -1145
  86. package/src/qingflow_mcp/tools/resource_read_tools.py +39 -188
  87. package/src/qingflow_mcp/tools/role_tools.py +9 -80
  88. package/src/qingflow_mcp/tools/solution_tools.py +45 -59
  89. package/src/qingflow_mcp/tools/task_context_tools.py +158 -662
  90. package/src/qingflow_mcp/tools/task_tools.py +29 -113
  91. package/src/qingflow_mcp/tools/view_tools.py +3 -106
  92. package/src/qingflow_mcp/tools/workflow_tools.py +4 -48
  93. package/src/qingflow_mcp/tools/workspace_tools.py +3 -71
  94. package/npm/bin/qingflow-skills.mjs +0 -5
  95. package/skills/qingflow-app-builder/references/complete-system-development-guide.md +0 -172
  96. package/skills/qingflow-app-builder/references/single-app-development-guide.md +0 -87
  97. package/skills/qingflow-app-builder/scripts/validate_system_build_summary.py +0 -124
  98. 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, backend_code_int, is_auth_like_error, raise_tool_error
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
- 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
- )
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": effective_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
- 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})
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": effective_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, backend_code_int, is_auth_like_error, raise_tool_error
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. 为 builder 配置、审批处理或联系人确认提供目录侧数据来源。
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
- self.register_frontend_search(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
+ )
32
43
 
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.")
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(description="Page through internal workspace members through the contact directory route. This is a directory/admin-style lookup, not the record field candidate path.")
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(description="List internal departments. With keyword, uses frontend directory search; without keyword, browses the contact department tree and may require contact-directory permission.")
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(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.")
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(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.")
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(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.")
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 = True,
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
- 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
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
- 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
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
- 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
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
- 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
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
- 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
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": dept_bucket,
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
- try:
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=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
- 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
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 = True,
482
+ simple: bool,
574
483
  ) -> dict[str, Any]:
575
484
  """执行组织目录相关逻辑。"""
576
485
  def runner(session_profile, context):
577
486
  if simple:
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
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
- 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
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"] = list(raw.get("warnings") or [])
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)