@josephyan/qingflow-cli 1.1.4 → 1.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/README.md +7 -3
  2. package/docs/local-agent-install.md +57 -6
  3. package/entry_point.py +1 -1
  4. package/npm/bin/qingflow-skills.mjs +5 -0
  5. package/npm/bin/qingflow.mjs +1 -34
  6. package/npm/lib/runtime.mjs +21 -101
  7. package/npm/scripts/postinstall.mjs +1 -10
  8. package/package.json +3 -2
  9. package/pyproject.toml +1 -1
  10. package/skills/qingflow-cli/SKILL.md +58 -44
  11. package/skills/qingflow-cli/manifest.yaml +1 -1
  12. package/skills/qingflow-cli/reference/00-INDEX.md +35 -0
  13. package/skills/qingflow-cli/reference/builder/10-build-single-app.md +38 -0
  14. package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +39 -0
  15. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md → builder/30-schema-fields.md} +52 -10
  16. package/skills/qingflow-cli/reference/builder/40-layout.md +52 -0
  17. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md → builder/50-views.md} +39 -15
  18. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_CHARTS_WORKFLOW.md → builder/60-charts.md} +36 -13
  19. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_PORTAL_WORKFLOW.md → builder/70-portal.md} +36 -13
  20. package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +41 -0
  21. package/skills/qingflow-cli/reference/builder/90-workflow.md +34 -0
  22. package/skills/qingflow-cli/reference/builder/99-publish-verify.md +46 -0
  23. package/skills/qingflow-cli/reference/builder/README.md +41 -0
  24. package/skills/qingflow-cli/reference/builder/code-integrations/README.md +130 -0
  25. package/skills/qingflow-cli/reference/builder/code-integrations/code-block.md +66 -0
  26. package/skills/qingflow-cli/reference/builder/code-integrations/q-linker.md +77 -0
  27. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md → builder/reference/app-delivery-sop.md} +26 -16
  28. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/README.md +293 -0
  29. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-complete-system.md +809 -0
  30. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-single-app.md +830 -0
  31. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/complete-system-development-guide.md +123 -0
  32. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/create-app.md +182 -0
  33. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/environments.md +63 -0
  34. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/flow-actors-and-permissions.md +142 -0
  35. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/gotchas.md +108 -0
  36. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/match-rules.md +114 -0
  37. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/public-surface-sync.md +75 -0
  38. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/single-app-development-guide.md +58 -0
  39. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/solution-playbooks.md +52 -0
  40. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/tool-selection.md +107 -0
  41. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-flow.md +7 -0
  42. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-layout.md +7 -0
  43. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-schema.md +7 -0
  44. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-views.md +7 -0
  45. package/skills/qingflow-cli/reference/builder/workflow/01-overview.md +45 -0
  46. package/skills/qingflow-cli/reference/builder/workflow/02-update-mode.md +53 -0
  47. package/skills/qingflow-cli/reference/builder/workflow/03-flow-patterns.md +57 -0
  48. package/skills/qingflow-cli/reference/builder/workflow/04-stage1-business-modeling.md +131 -0
  49. package/skills/qingflow-cli/reference/builder/workflow/05-stage2-members-roles.md +29 -0
  50. package/skills/qingflow-cli/reference/builder/workflow/06-stage3-build-spec.md +165 -0
  51. package/skills/qingflow-cli/reference/builder/workflow/07-stage4-validate-spec.md +33 -0
  52. package/skills/qingflow-cli/reference/builder/workflow/08-stage5-apply-verify.md +51 -0
  53. package/skills/qingflow-cli/reference/builder/workflow/09-stage6-summary.md +88 -0
  54. package/skills/qingflow-cli/reference/builder/workflow/10-node-config-reference.md +93 -0
  55. package/skills/qingflow-cli/reference/builder/workflow/11-troubleshooting.md +15 -0
  56. package/skills/qingflow-cli/reference/builder/workflow/README.md +88 -0
  57. package/skills/qingflow-cli/reference/builder/workflow/workflow-schema.json +1754 -0
  58. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ADMIN_CHEATSHEET.md → core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md} +3 -3
  59. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md → core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md} +6 -6
  60. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_EXPLORATION_REPORT.md → core/QINGFLOW_CLI_EXPLORATION_REPORT.md} +2 -2
  61. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_FIELD_DATA_TYPES.md → core/QINGFLOW_CLI_FIELD_DATA_TYPES.md} +11 -11
  62. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_MEMBER_CHEATSHEET.md → core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md} +4 -4
  63. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md → core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md} +4 -4
  64. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md} +3 -3
  65. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_DELETE_WORKFLOW.md +31 -0
  66. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md} +4 -4
  67. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md} +7 -7
  68. package/skills/qingflow-cli/reference/record/analysis/README.md +130 -0
  69. package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +91 -0
  70. package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +112 -0
  71. package/skills/qingflow-cli/reference/record/analysis/business-context.md +74 -0
  72. package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +69 -0
  73. package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +106 -0
  74. package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +172 -0
  75. package/skills/qingflow-cli/reference/record/analysis/report-format.md +76 -0
  76. package/skills/qingflow-cli/reference/record/insert/README.md +75 -0
  77. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md → task/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md} +5 -5
  78. package/skills/qingflow-cli/reference/task/ops/README.md +131 -0
  79. package/skills/qingflow-cli/reference/task/ops/environments.md +43 -0
  80. package/skills/qingflow-cli/reference/task/ops/workflow-usage.md +26 -0
  81. package/skills/qingflow-cli/scripts/validate_system_build_summary.py +124 -0
  82. package/skills/qingflow-cli/scripts/workflow/diff_flow_spec.py +275 -0
  83. package/skills/qingflow-cli/scripts/workflow/validate_flow_spec.py +605 -0
  84. package/skills/qingflow-mcp-setup/SKILL.md +115 -0
  85. package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
  86. package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
  87. package/skills/qingflow-mcp-setup/references/environments.md +62 -0
  88. package/skills/qingflow-mcp-setup/references/generic-stdio.md +32 -0
  89. package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
  90. package/src/qingflow_mcp/__init__.py +1 -1
  91. package/src/qingflow_mcp/__main__.py +6 -2
  92. package/src/qingflow_mcp/builder_facade/models.py +282 -102
  93. package/src/qingflow_mcp/builder_facade/service.py +4166 -929
  94. package/src/qingflow_mcp/cli/commands/builder.py +316 -298
  95. package/src/qingflow_mcp/cli/commands/chart.py +1 -1
  96. package/src/qingflow_mcp/cli/commands/common.py +12 -3
  97. package/src/qingflow_mcp/cli/commands/exports.py +2 -2
  98. package/src/qingflow_mcp/cli/commands/imports.py +3 -3
  99. package/src/qingflow_mcp/cli/commands/portal.py +2 -2
  100. package/src/qingflow_mcp/cli/commands/record.py +101 -27
  101. package/src/qingflow_mcp/cli/commands/task.py +28 -47
  102. package/src/qingflow_mcp/cli/commands/view.py +1 -1
  103. package/src/qingflow_mcp/cli/context.py +0 -3
  104. package/src/qingflow_mcp/cli/formatters.py +784 -16
  105. package/src/qingflow_mcp/cli/main.py +117 -33
  106. package/src/qingflow_mcp/errors.py +43 -2
  107. package/src/qingflow_mcp/public_surface.py +26 -17
  108. package/src/qingflow_mcp/response_trim.py +81 -17
  109. package/src/qingflow_mcp/server.py +14 -12
  110. package/src/qingflow_mcp/server_app_builder.py +65 -21
  111. package/src/qingflow_mcp/server_app_user.py +22 -16
  112. package/src/qingflow_mcp/session_store.py +11 -7
  113. package/src/qingflow_mcp/solution/compiler/__init__.py +3 -1
  114. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
  115. package/src/qingflow_mcp/solution/executor.py +245 -18
  116. package/src/qingflow_mcp/tools/ai_builder_tools.py +1780 -406
  117. package/src/qingflow_mcp/tools/app_tools.py +184 -43
  118. package/src/qingflow_mcp/tools/approval_tools.py +197 -35
  119. package/src/qingflow_mcp/tools/auth_tools.py +92 -16
  120. package/src/qingflow_mcp/tools/code_block_tools.py +298 -40
  121. package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
  122. package/src/qingflow_mcp/tools/directory_tools.py +236 -72
  123. package/src/qingflow_mcp/tools/export_tools.py +244 -34
  124. package/src/qingflow_mcp/tools/feedback_tools.py +9 -0
  125. package/src/qingflow_mcp/tools/file_tools.py +9 -3
  126. package/src/qingflow_mcp/tools/import_tools.py +336 -49
  127. package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
  128. package/src/qingflow_mcp/tools/package_tools.py +118 -6
  129. package/src/qingflow_mcp/tools/portal_tools.py +39 -3
  130. package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
  131. package/src/qingflow_mcp/tools/record_tools.py +1141 -356
  132. package/src/qingflow_mcp/tools/resource_read_tools.py +188 -39
  133. package/src/qingflow_mcp/tools/role_tools.py +80 -9
  134. package/src/qingflow_mcp/tools/solution_tools.py +59 -45
  135. package/src/qingflow_mcp/tools/task_context_tools.py +662 -158
  136. package/src/qingflow_mcp/tools/task_tools.py +113 -29
  137. package/src/qingflow_mcp/tools/view_tools.py +106 -3
  138. package/src/qingflow_mcp/tools/workflow_tools.py +48 -4
  139. package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
  140. /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_MATCH_RULES.md → builder/reference/match-rules.md} +0 -0
  141. /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_WORKSPACE_ICONS.md → builder/reference/workspace-icons.md} +0 -0
  142. /package/skills/qingflow-cli/reference/{charts_remove.example.json → examples/charts/charts_remove.example.json} +0 -0
  143. /package/skills/qingflow-cli/reference/{charts_reorder.example.json → examples/charts/charts_reorder.example.json} +0 -0
  144. /package/skills/qingflow-cli/reference/{charts_upsert_bar.example.json → examples/charts/charts_upsert_bar.example.json} +0 -0
  145. /package/skills/qingflow-cli/reference/{charts_upsert_dashboard_starter.example.json → examples/charts/charts_upsert_dashboard_starter.example.json} +0 -0
  146. /package/skills/qingflow-cli/reference/{charts_upsert_minimal.example.json → examples/charts/charts_upsert_minimal.example.json} +0 -0
  147. /package/skills/qingflow-cli/reference/{portal_sections_all_types.example.json → examples/portal/portal_sections_all_types.example.json} +0 -0
  148. /package/skills/qingflow-cli/reference/{portal_sections_five_types.example.json → examples/portal/portal_sections_five_types.example.json} +0 -0
  149. /package/skills/qingflow-cli/reference/{portal_sections_standard_workbench.example.json → examples/portal/portal_sections_standard_workbench.example.json} +0 -0
  150. /package/skills/qingflow-cli/reference/{_batch_schema_complex.json → examples/schema/_batch_schema_complex.json} +0 -0
  151. /package/skills/qingflow-cli/reference/{_batch_schema_scalar.json → examples/schema/_batch_schema_scalar.json} +0 -0
  152. /package/skills/qingflow-cli/reference/{schema_add_fields_minimal.example.json → examples/schema/schema_add_fields_minimal.example.json} +0 -0
  153. /package/skills/qingflow-cli/reference/{schema_apply_add_fields_all_types.json → examples/schema/schema_apply_add_fields_all_types.json} +0 -0
  154. /package/skills/qingflow-cli/reference/{views_upsert_table_minimal.example.json → examples/views/views_upsert_table_minimal.example.json} +0 -0
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from copy import deepcopy
4
4
 
5
5
  from ..config import DEFAULT_PROFILE
6
- from ..errors import QingflowApiError, raise_tool_error
6
+ from ..errors import QingflowApiError, backend_code_int, is_auth_like_error, raise_tool_error
7
7
  from ..json_types import JSONObject
8
8
  from .base import ToolBase, tool_cn_name
9
9
 
@@ -31,12 +31,26 @@ class CustomButtonTools(ToolBase):
31
31
  self._require_app_key(app_key)
32
32
 
33
33
  def runner(session_profile, context):
34
- result = self.backend.request(
35
- "GET",
36
- context,
37
- f"/app/{app_key}/customButton",
38
- params={"beingDraft": being_draft},
39
- )
34
+ effective_being_draft = being_draft
35
+ fallback_error: QingflowApiError | None = None
36
+ try:
37
+ result = self.backend.request(
38
+ "GET",
39
+ context,
40
+ f"/app/{app_key}/customButton",
41
+ params={"beingDraft": being_draft},
42
+ )
43
+ except QingflowApiError as exc:
44
+ if not being_draft or not _is_optional_draft_button_read_error(exc):
45
+ raise
46
+ fallback_error = exc
47
+ effective_being_draft = False
48
+ result = self.backend.request(
49
+ "GET",
50
+ context,
51
+ f"/app/{app_key}/customButton",
52
+ params={"beingDraft": False},
53
+ )
40
54
  items = []
41
55
  raw_items = result.get("result") if isinstance(result, dict) and isinstance(result.get("result"), list) else []
42
56
  for item in raw_items:
@@ -47,11 +61,23 @@ class CustomButtonTools(ToolBase):
47
61
  "profile": profile,
48
62
  "ws_id": session_profile.selected_ws_id,
49
63
  "app_key": app_key,
50
- "being_draft": being_draft,
64
+ "being_draft": effective_being_draft,
65
+ "requested_being_draft": being_draft,
51
66
  "items": result if include_raw else items,
52
67
  "count": len(items),
53
68
  "compact": not include_raw,
54
69
  }
70
+ if fallback_error is not None:
71
+ response["warnings"] = [
72
+ {
73
+ "code": "CUSTOM_BUTTON_DRAFT_LIST_UNAVAILABLE",
74
+ "message": "draft custom button list is unavailable; returned published custom buttons instead",
75
+ "backend_code": fallback_error.backend_code,
76
+ "http_status": fallback_error.http_status,
77
+ "request_id": fallback_error.request_id,
78
+ }
79
+ ]
80
+ response["verification"] = {"draft_readable": False, "published_fallback_used": True}
55
81
  if include_raw:
56
82
  response["summary"] = items
57
83
  return response
@@ -74,16 +100,37 @@ class CustomButtonTools(ToolBase):
74
100
 
75
101
  def runner(session_profile, context):
76
102
  params = {"beingDraft": being_draft}
77
- result = self.backend.request("GET", context, f"/app/{app_key}/customButton/{button_id}", params=params)
103
+ effective_being_draft = being_draft
104
+ fallback_error: QingflowApiError | None = None
105
+ try:
106
+ result = self.backend.request("GET", context, f"/app/{app_key}/customButton/{button_id}", params=params)
107
+ except QingflowApiError as exc:
108
+ if not being_draft or not _is_optional_draft_button_read_error(exc):
109
+ raise
110
+ fallback_error = exc
111
+ effective_being_draft = False
112
+ result = self.backend.request("GET", context, f"/app/{app_key}/customButton/{button_id}", params={"beingDraft": False})
78
113
  response = {
79
114
  "profile": profile,
80
115
  "ws_id": session_profile.selected_ws_id,
81
116
  "app_key": app_key,
82
117
  "button_id": button_id,
83
- "being_draft": being_draft,
118
+ "being_draft": effective_being_draft,
119
+ "requested_being_draft": being_draft,
84
120
  "result": result if include_raw else self._compact_button_detail(result if isinstance(result, dict) else {}),
85
121
  "compact": not include_raw,
86
122
  }
123
+ if fallback_error is not None:
124
+ response["warnings"] = [
125
+ {
126
+ "code": "CUSTOM_BUTTON_DRAFT_DETAIL_UNAVAILABLE",
127
+ "message": "draft custom button detail is unavailable; returned published custom button detail instead",
128
+ "backend_code": fallback_error.backend_code,
129
+ "http_status": fallback_error.http_status,
130
+ "request_id": fallback_error.request_id,
131
+ }
132
+ ]
133
+ response["verification"] = {"draft_readable": False, "published_fallback_used": True}
87
134
  if include_raw:
88
135
  response["summary"] = self._compact_button_detail(result if isinstance(result, dict) else {})
89
136
  return response
@@ -198,3 +245,10 @@ class CustomButtonTools(ToolBase):
198
245
  else None,
199
246
  "trigger_wings_config": deepcopy(item.get("triggerWingsConfig")) if isinstance(item.get("triggerWingsConfig"), dict) else None,
200
247
  }
248
+
249
+
250
+ def _is_optional_draft_button_read_error(error: QingflowApiError) -> bool:
251
+ if is_auth_like_error(error):
252
+ return False
253
+ backend_code = backend_code_int(error)
254
+ return backend_code in {40002, 40027, 404} or error.http_status == 404
@@ -6,7 +6,7 @@ from typing import Any
6
6
  from mcp.server.fastmcp import FastMCP
7
7
 
8
8
  from ..config import DEFAULT_PROFILE
9
- from ..errors import QingflowApiError, raise_tool_error
9
+ from ..errors import QingflowApiError, backend_code_int, is_auth_like_error, raise_tool_error
10
10
  from .base import ToolBase, tool_cn_name
11
11
 
12
12
 
@@ -20,28 +20,17 @@ class DirectoryTools(ToolBase):
20
20
  主要职责:
21
21
  1. 搜索用户、部门、外部联系人;
22
22
  2. 列举内部用户与部门层级;
23
- 3. 为审批/字段候选提供目录侧数据来源。
23
+ 3. 为 builder 配置、审批处理或联系人确认提供目录侧数据来源。
24
+
25
+ 注意:记录成员/部门字段候选应优先使用 record_member_candidates /
26
+ record_department_candidates,避免把普通记录读写误带入通讯录目录链路。
24
27
  """
25
28
 
26
29
  def register(self, mcp: FastMCP) -> None:
27
30
  """注册当前工具到 MCP 服务。"""
28
- @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
- )
31
+ self.register_frontend_search(mcp)
43
32
 
44
- @mcp.tool()
33
+ @mcp.tool(description="List internal workspace members through the contact directory route. This is a directory/admin-style lookup, not the record field candidate path.")
45
34
  def directory_list_internal_users(
46
35
  profile: str = DEFAULT_PROFILE,
47
36
  keyword: str | None = None,
@@ -61,7 +50,7 @@ class DirectoryTools(ToolBase):
61
50
  contain_disable=include_disabled,
62
51
  )
63
52
 
64
- @mcp.tool()
53
+ @mcp.tool(description="Page through internal workspace members through the contact directory route. This is a directory/admin-style lookup, not the record field candidate path.")
65
54
  def directory_list_all_internal_users(
66
55
  profile: str = DEFAULT_PROFILE,
67
56
  keyword: str | None = None,
@@ -81,7 +70,7 @@ class DirectoryTools(ToolBase):
81
70
  max_pages=max_pages,
82
71
  )
83
72
 
84
- @mcp.tool()
73
+ @mcp.tool(description="List internal departments. With keyword, uses frontend directory search; without keyword, browses the contact department tree and may require contact-directory permission.")
85
74
  def directory_list_internal_departments(
86
75
  profile: str = DEFAULT_PROFILE,
87
76
  keyword: str = "",
@@ -95,7 +84,7 @@ class DirectoryTools(ToolBase):
95
84
  page_size=page_size,
96
85
  )
97
86
 
98
- @mcp.tool()
87
+ @mcp.tool(description="Walk the internal department tree through the contact directory route. This may require contact-directory permission and should not be used for record department field candidates.")
99
88
  def directory_list_all_departments(
100
89
  profile: str = DEFAULT_PROFILE,
101
90
  parent_department_id: int | None = None,
@@ -109,20 +98,20 @@ class DirectoryTools(ToolBase):
109
98
  max_items=max_items,
110
99
  )
111
100
 
112
- @mcp.tool()
101
+ @mcp.tool(description="List child departments through the contact directory route. This may require contact-directory permission and should not be used for record department field candidates.")
113
102
  def directory_list_sub_departments(
114
103
  profile: str = DEFAULT_PROFILE,
115
104
  parent_department_id: int | None = None,
116
105
  ) -> dict[str, Any]:
117
106
  return self.directory_list_sub_departments(profile=profile, parent_dept_id=parent_department_id)
118
107
 
119
- @mcp.tool()
108
+ @mcp.tool(description="Search external contacts. Defaults to the frontend simple external-member route; set simple=false only for full external-contact management data, which may require contact permission.")
120
109
  def directory_list_external_members(
121
110
  profile: str = DEFAULT_PROFILE,
122
111
  keyword: str | None = None,
123
112
  page_num: int = 1,
124
113
  page_size: int = 20,
125
- simple: bool = False,
114
+ simple: bool = True,
126
115
  ) -> dict[str, Any]:
127
116
  return self.directory_list_external_members(
128
117
  profile=profile,
@@ -132,6 +121,24 @@ class DirectoryTools(ToolBase):
132
121
  simple=simple,
133
122
  )
134
123
 
124
+ def register_frontend_search(self, mcp: FastMCP) -> None:
125
+ """Register only the member-visible frontend search route."""
126
+ @mcp.tool(description="Search internal workspace members/departments by keyword via the frontend directory search route. Do not use for record member/department field candidates; use record_member_candidates or record_department_candidates instead.")
127
+ def directory_search(
128
+ profile: str = DEFAULT_PROFILE,
129
+ query: str = "",
130
+ scopes: list[str] | None = None,
131
+ page_num: int = 1,
132
+ page_size: int = 20,
133
+ ) -> dict[str, Any]:
134
+ return self.directory_search(
135
+ profile=profile,
136
+ query=query,
137
+ scopes=scopes,
138
+ page_num=page_num,
139
+ page_size=page_size,
140
+ )
141
+
135
142
  @tool_cn_name("通讯录搜索")
136
143
  def directory_search(
137
144
  self,
@@ -151,17 +158,28 @@ class DirectoryTools(ToolBase):
151
158
  raise_tool_error(QingflowApiError.config_error("query is required"))
152
159
 
153
160
  def runner(session_profile, context):
154
- 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
- )
161
+ try:
162
+ result = self.backend.request(
163
+ "POST",
164
+ context,
165
+ "/member/search",
166
+ json_body={
167
+ "dimensions": normalized_scopes,
168
+ "searchKey": query,
169
+ "pageNum": page_num,
170
+ "pageSize": page_size,
171
+ },
172
+ )
173
+ except QingflowApiError as exc:
174
+ if _is_directory_permission_denied(exc):
175
+ return self._directory_permission_denied_payload(
176
+ profile=profile,
177
+ session_profile=session_profile,
178
+ context=context,
179
+ error=exc,
180
+ operation="directory_search",
181
+ )
182
+ raise
165
183
  return {
166
184
  "profile": profile,
167
185
  "ws_id": session_profile.selected_ws_id,
@@ -209,7 +227,18 @@ class DirectoryTools(ToolBase):
209
227
  params["deptId"] = dept_id
210
228
  if role_id is not None:
211
229
  params["roleId"] = role_id
212
- result = self.backend.request("GET", context, "/contact", params=params)
230
+ try:
231
+ result = self.backend.request("GET", context, "/contact", params=params)
232
+ except QingflowApiError as exc:
233
+ if _is_directory_permission_denied(exc):
234
+ return self._directory_permission_denied_payload(
235
+ profile=profile,
236
+ session_profile=session_profile,
237
+ context=context,
238
+ error=exc,
239
+ operation="directory_list_internal_users",
240
+ )
241
+ raise
213
242
  return {
214
243
  "profile": profile,
215
244
  "ws_id": session_profile.selected_ws_id,
@@ -269,7 +298,18 @@ class DirectoryTools(ToolBase):
269
298
  params["deptId"] = dept_id
270
299
  if role_id is not None:
271
300
  params["roleId"] = role_id
272
- result = self.backend.request("GET", context, "/contact", params=params)
301
+ try:
302
+ result = self.backend.request("GET", context, "/contact", params=params)
303
+ except QingflowApiError as exc:
304
+ if _is_directory_permission_denied(exc):
305
+ return self._directory_permission_denied_payload(
306
+ profile=profile,
307
+ session_profile=session_profile,
308
+ context=context,
309
+ error=exc,
310
+ operation="directory_list_all_internal_users",
311
+ )
312
+ raise
273
313
  page_items = _directory_items(result)
274
314
  if reported_total is None:
275
315
  reported_total = _coerce_int(_payload_value(result, "total"))
@@ -329,13 +369,24 @@ class DirectoryTools(ToolBase):
329
369
 
330
370
  if not normalized_keyword:
331
371
  def runner(session_profile, context):
332
- 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
- )
372
+ try:
373
+ fetch_limit = max((page_num + 1) * page_size + 1, page_size + 1)
374
+ items, truncated, deepest_depth = self._walk_department_tree(
375
+ context,
376
+ parent_dept_id=None,
377
+ max_depth=20,
378
+ max_items=fetch_limit,
379
+ )
380
+ except QingflowApiError as exc:
381
+ if _is_directory_permission_denied(exc):
382
+ return self._directory_permission_denied_payload(
383
+ profile=profile,
384
+ session_profile=session_profile,
385
+ context=context,
386
+ error=exc,
387
+ operation="directory_list_internal_departments",
388
+ )
389
+ raise
339
390
  start = (page_num - 1) * page_size
340
391
  page_items = items[start : start + page_size]
341
392
  reported_total = None if truncated else len(items)
@@ -367,17 +418,35 @@ class DirectoryTools(ToolBase):
367
418
  )
368
419
 
369
420
  def runner(session_profile, context):
370
- result = self.backend.request(
371
- "GET",
372
- context,
373
- "/contact/deptByPage",
374
- params={"keyword": normalized_keyword, "pageNum": page_num, "pageSize": page_size},
375
- )
421
+ try:
422
+ result = self.backend.request(
423
+ "POST",
424
+ context,
425
+ "/member/search",
426
+ json_body={
427
+ "dimensions": ["DEPT"],
428
+ "searchKey": normalized_keyword,
429
+ "pageNum": page_num,
430
+ "pageSize": page_size,
431
+ },
432
+ )
433
+ except QingflowApiError as exc:
434
+ if _is_directory_permission_denied(exc):
435
+ return self._directory_permission_denied_payload(
436
+ profile=profile,
437
+ session_profile=session_profile,
438
+ context=context,
439
+ error=exc,
440
+ operation="directory_list_internal_departments",
441
+ )
442
+ raise
443
+ dept_bucket = _payload_value(result, "dept") or result
376
444
  return {
377
445
  "profile": profile,
378
446
  "ws_id": session_profile.selected_ws_id,
379
447
  "request_route": self._request_route_payload(context),
380
- "page": result,
448
+ "page": dept_bucket,
449
+ "result": result,
381
450
  }
382
451
 
383
452
  raw = self._run(profile, runner)
@@ -411,19 +480,30 @@ class DirectoryTools(ToolBase):
411
480
  raise_tool_error(QingflowApiError.config_error("max_items must be positive"))
412
481
 
413
482
  def runner(session_profile, context):
414
- 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:
483
+ try:
421
484
  items, truncated, deepest_depth = self._walk_department_tree(
422
485
  context,
423
- parent_dept_id=0,
486
+ parent_dept_id=parent_dept_id,
424
487
  max_depth=max_depth,
425
488
  max_items=max_items,
426
489
  )
490
+ if not items and parent_dept_id is None:
491
+ items, truncated, deepest_depth = self._walk_department_tree(
492
+ context,
493
+ parent_dept_id=0,
494
+ max_depth=max_depth,
495
+ max_items=max_items,
496
+ )
497
+ except QingflowApiError as exc:
498
+ if _is_directory_permission_denied(exc):
499
+ return self._directory_permission_denied_payload(
500
+ profile=profile,
501
+ session_profile=session_profile,
502
+ context=context,
503
+ error=exc,
504
+ operation="directory_list_all_departments",
505
+ )
506
+ raise
427
507
  return {
428
508
  "profile": profile,
429
509
  "ws_id": session_profile.selected_ws_id,
@@ -454,7 +534,18 @@ class DirectoryTools(ToolBase):
454
534
  params: dict[str, Any] = {}
455
535
  if parent_dept_id is not None:
456
536
  params["parentDeptId"] = parent_dept_id
457
- result = self.backend.request("GET", context, "/contact/subDeptList", params=params)
537
+ try:
538
+ result = self.backend.request("GET", context, "/contact/subDeptList", params=params)
539
+ except QingflowApiError as exc:
540
+ if _is_directory_permission_denied(exc):
541
+ return self._directory_permission_denied_payload(
542
+ profile=profile,
543
+ session_profile=session_profile,
544
+ context=context,
545
+ error=exc,
546
+ operation="directory_list_sub_departments",
547
+ )
548
+ raise
458
549
  return {
459
550
  "profile": profile,
460
551
  "ws_id": session_profile.selected_ws_id,
@@ -479,22 +570,46 @@ class DirectoryTools(ToolBase):
479
570
  keyword: str | None,
480
571
  page_num: int,
481
572
  page_size: int,
482
- simple: bool,
573
+ simple: bool = True,
483
574
  ) -> dict[str, Any]:
484
575
  """执行组织目录相关逻辑。"""
485
576
  def runner(session_profile, context):
486
577
  if simple:
487
- 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
- )
578
+ try:
579
+ result = self.backend.request(
580
+ "POST",
581
+ context,
582
+ "/external/member/simple/pageList",
583
+ json_body={"keyword": keyword, "pageNum": page_num, "pageSize": page_size},
584
+ )
585
+ except QingflowApiError as exc:
586
+ if _is_directory_permission_denied(exc):
587
+ return self._directory_permission_denied_payload(
588
+ profile=profile,
589
+ session_profile=session_profile,
590
+ context=context,
591
+ error=exc,
592
+ operation="directory_list_external_members_simple",
593
+ ) | {"simple": simple}
594
+ raise
493
595
  else:
494
- 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)
596
+ try:
597
+ result = self.backend.request(
598
+ "POST",
599
+ context,
600
+ "/external/member/pageList",
601
+ json_body={"keyword": keyword, "pageNum": page_num, "pageSize": page_size},
602
+ )
603
+ except QingflowApiError as exc:
604
+ if _is_directory_permission_denied(exc):
605
+ return self._directory_permission_denied_payload(
606
+ profile=profile,
607
+ session_profile=session_profile,
608
+ context=context,
609
+ error=exc,
610
+ operation="directory_list_external_members_full",
611
+ ) | {"simple": simple}
612
+ raise
498
613
  return {
499
614
  "profile": profile,
500
615
  "ws_id": session_profile.selected_ws_id,
@@ -542,7 +657,7 @@ class DirectoryTools(ToolBase):
542
657
  """执行内部辅助逻辑。"""
543
658
  response = dict(raw)
544
659
  response["ok"] = bool(raw.get("ok", True))
545
- response["warnings"] = []
660
+ response["warnings"] = list(raw.get("warnings") or [])
546
661
  response["output_profile"] = "normal"
547
662
  response["data"] = {
548
663
  "items": items,
@@ -551,6 +666,49 @@ class DirectoryTools(ToolBase):
551
666
  }
552
667
  return response
553
668
 
669
+ def _directory_permission_denied_payload(
670
+ self,
671
+ *,
672
+ profile: str,
673
+ session_profile, # type: ignore[no-untyped-def]
674
+ context, # type: ignore[no-untyped-def]
675
+ error: QingflowApiError,
676
+ operation: str,
677
+ ) -> dict[str, Any]:
678
+ candidate_hint = (
679
+ "For record member/department fields, use record_member_candidates or "
680
+ "record_department_candidates."
681
+ if operation == "directory_search"
682
+ else (
683
+ "For record member/department fields, use record_member_candidates, "
684
+ "record_department_candidates, or directory_search with a keyword instead."
685
+ )
686
+ )
687
+ return {
688
+ "profile": profile,
689
+ "ws_id": session_profile.selected_ws_id,
690
+ "ok": False,
691
+ "status": "failed",
692
+ "error_code": "CONTACT_DIRECTORY_PERMISSION_DENIED",
693
+ "message": (
694
+ "Contact-directory management data is not readable for the current user. "
695
+ f"{candidate_hint}"
696
+ ),
697
+ "backend_code": error.backend_code,
698
+ "request_id": error.request_id,
699
+ "http_status": error.http_status,
700
+ "request_route": self._request_route_payload(context),
701
+ "items": [],
702
+ "pagination": {"returned_items": 0},
703
+ "warnings": [
704
+ {
705
+ "code": "CONTACT_DIRECTORY_PERMISSION_DENIED",
706
+ "message": "This is a contact-directory permission boundary, not proof that record field candidates are unavailable.",
707
+ "operation": operation,
708
+ }
709
+ ],
710
+ }
711
+
554
712
  def _walk_department_tree(
555
713
  self,
556
714
  context, # type: ignore[no-untyped-def]
@@ -641,6 +799,12 @@ def _directory_has_more(payload: Any, *, current_page: int, page_size: int, retu
641
799
  return returned_items >= page_size and returned_items > 0
642
800
 
643
801
 
802
+ def _is_directory_permission_denied(error: QingflowApiError) -> bool:
803
+ if is_auth_like_error(error):
804
+ return False
805
+ return backend_code_int(error) in {40002, 40027}
806
+
807
+
644
808
  def _directory_member_key(item: dict[str, Any]) -> str:
645
809
  for key in ("uid", "id", "userId"):
646
810
  value = item.get(key)