@qingflow-tech/qingflow-cli 1.1.61-beta.5 → 1.1.61

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 (85) hide show
  1. package/README.md +3 -3
  2. package/package.json +1 -1
  3. package/pyproject.toml +1 -2
  4. package/skills/qingflow-cli/SKILL.md +2 -104
  5. package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +2 -2
  6. package/skills/qingflow-cli/reference/builder/30-schema-fields.md +5 -22
  7. package/skills/qingflow-cli/reference/builder/70-portal.md +16 -37
  8. package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +3 -3
  9. package/skills/qingflow-cli/reference/examples/portal/portal_sections_all_types.example.json +7 -6
  10. package/src/qingflow_mcp/app_form_compat.py +0 -3
  11. package/src/qingflow_mcp/app_form_validator.py +20 -89
  12. package/src/qingflow_mcp/builder_facade/app_form.py +112 -629
  13. package/src/qingflow_mcp/builder_facade/models.py +13 -164
  14. package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +1 -15
  15. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +38 -112
  16. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +8 -31
  17. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +10 -6
  18. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +10 -33
  19. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +10 -6
  20. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +18 -11
  21. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +8 -4
  22. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +8 -4
  23. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +10 -6
  24. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +8 -4
  25. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +8 -4
  26. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +8 -4
  27. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +10 -6
  28. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +8 -4
  29. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +10 -6
  30. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +8 -4
  31. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +12 -8
  32. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +15 -73
  33. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +8 -4
  34. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +8 -4
  35. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +29 -90
  36. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +8 -4
  37. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/formula.json +0 -43
  38. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +19 -27
  39. package/src/qingflow_mcp/builder_facade/service.py +72 -1143
  40. package/src/qingflow_mcp/cli/commands/builder.py +5 -114
  41. package/src/qingflow_mcp/cli/main.py +1 -34
  42. package/src/qingflow_mcp/cloud_context.py +1 -16
  43. package/src/qingflow_mcp/public_surface.py +8 -69
  44. package/src/qingflow_mcp/response_trim.py +1 -2
  45. package/src/qingflow_mcp/server.py +6 -12
  46. package/src/qingflow_mcp/server_app_builder.py +6 -41
  47. package/src/qingflow_mcp/server_app_user.py +5 -9
  48. package/src/qingflow_mcp/server_cloud.py +7 -43
  49. package/src/qingflow_mcp/solution/compiler/icon_utils.py +0 -73
  50. package/src/qingflow_mcp/solution/executor.py +2 -5
  51. package/src/qingflow_mcp/tools/ai_builder_tools.py +12 -110
  52. package/src/qingflow_mcp/tools/portal_tools.py +54 -4
  53. package/src/qingflow_mcp/tools/solution_tools.py +0 -2
  54. package/skills/qingflow-cli/reference/examples/portal/portal_apply_v1alpha1.example.json +0 -100
  55. package/src/qingflow_mcp/builder_facade/portals_schema.py +0 -397
  56. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_content_classification.json +0 -146
  57. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_content_extraction.json +0 -146
  58. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_content_generation.json +0 -146
  59. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_document_parsing.json +0 -146
  60. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_image_generation.json +0 -146
  61. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_image_recognition.json +0 -146
  62. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_machine_translation.json +0 -146
  63. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_text_summary.json +0 -146
  64. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date_duration.json +0 -146
  65. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/embed_field.json +0 -146
  66. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/image_select.json +0 -146
  67. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/location.json +0 -146
  68. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ocr.json +0 -146
  69. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/rich_text.json +0 -146
  70. package/src/qingflow_mcp/builder_facade/schemas/portals/manifest.json +0 -59
  71. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/complete.json +0 -1397
  72. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/carousel.json +0 -193
  73. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/chart.json +0 -643
  74. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/favourite.json +0 -203
  75. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/filter.json +0 -268
  76. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/grid.json +0 -265
  77. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/link.json +0 -131
  78. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/task.json +0 -199
  79. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/text.json +0 -123
  80. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/view.json +0 -196
  81. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/summary.json +0 -800
  82. package/src/qingflow_mcp/tool_catalog.py +0 -387
  83. package/src/qingflow_mcp/tool_catalog_i18n.py +0 -185
  84. package/src/qingflow_mcp/tool_policy.py +0 -197
  85. package/src/qingflow_mcp/tools/portal_writer.py +0 -59
@@ -1,197 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- import os
5
- from dataclasses import dataclass
6
- from typing import Any, Literal, Mapping, Sequence
7
- from urllib.parse import parse_qs
8
-
9
- from mcp.server.fastmcp import FastMCP
10
- from mcp.server.fastmcp.exceptions import ToolError
11
-
12
- from .cloud_context import current_http_tool_policy, current_tool_policy_header
13
- from .tool_catalog import TOOL_ID_PATTERN, require_registered_tool_id, tool_is_exempt
14
-
15
-
16
- TOOL_POLICY_ENV = "QINGFLOW_TOOL_POLICY"
17
- TOOL_POLICY_HEADER = "X-Qingflow-Tool-Policy"
18
- TOOL_POLICY_INVALID = "TOOL_POLICY_INVALID"
19
- TOOL_POLICY_DENIED = "TOOL_POLICY_DENIED"
20
-
21
-
22
- @dataclass(frozen=True, slots=True)
23
- class ToolPolicySnapshot:
24
- mode: Literal["unrestricted", "restricted", "invalid"]
25
- allowed_tools: tuple[str, ...] = ()
26
- error: str | None = None
27
-
28
- def violation_for(self, stable_id: str) -> "ToolPolicyViolation | None":
29
- # 基础恢复能力在非法策略下也必须可用。
30
- if tool_is_exempt(stable_id):
31
- return None
32
- if self.mode == "unrestricted":
33
- return None
34
- if self.mode == "invalid":
35
- return ToolPolicyViolation(TOOL_POLICY_INVALID, stable_id, self.error or "工具策略无效")
36
- if stable_id in self.allowed_tools:
37
- return None
38
- return ToolPolicyViolation(TOOL_POLICY_DENIED, stable_id, "当前策略未允许该工具")
39
-
40
-
41
- class ToolPolicyViolation(Exception):
42
- def __init__(self, code: str, tool_id_value: str, message: str) -> None:
43
- super().__init__(message)
44
- self.code = code
45
- self.tool_id = tool_id_value
46
- self.message = message
47
-
48
- def payload(self, *, camel_case: bool = False) -> dict[str, Any]:
49
- if camel_case:
50
- return {
51
- "status": "failed",
52
- "errorCode": self.code,
53
- "message": self.message,
54
- "toolId": self.tool_id,
55
- }
56
- return {
57
- "status": "failed",
58
- "error_code": self.code,
59
- "message": self.message,
60
- "toolId": self.tool_id,
61
- }
62
-
63
-
64
- def parse_tool_policy(value: str | Mapping[str, Any] | None) -> ToolPolicySnapshot:
65
- if value is None:
66
- return ToolPolicySnapshot("unrestricted")
67
- payload: Any = value
68
- if isinstance(value, str):
69
- if value == "":
70
- return ToolPolicySnapshot("unrestricted")
71
- try:
72
- payload = json.loads(value)
73
- except json.JSONDecodeError:
74
- return ToolPolicySnapshot("invalid", error="工具策略不是合法 JSON")
75
- if not isinstance(payload, Mapping):
76
- return ToolPolicySnapshot("invalid", error="工具策略必须是对象")
77
- if set(payload) != {"version", "allowed_tools"}:
78
- return ToolPolicySnapshot("invalid", error="工具策略字段必须为 version 和 allowed_tools")
79
- if type(payload.get("version")) is not int or payload.get("version") != 1: # noqa: E721
80
- return ToolPolicySnapshot("invalid", error="工具策略 version 必须为 1")
81
- allowed = payload.get("allowed_tools")
82
- if not isinstance(allowed, list):
83
- return ToolPolicySnapshot("invalid", error="allowed_tools 必须是数组")
84
- normalized: set[str] = set()
85
- for stable_id in allowed:
86
- if not isinstance(stable_id, str) or TOOL_ID_PATTERN.fullmatch(stable_id) is None:
87
- return ToolPolicySnapshot("invalid", error="allowed_tools 包含非法工具 ID")
88
- normalized.add(stable_id)
89
- return ToolPolicySnapshot("restricted", tuple(sorted(normalized)))
90
-
91
-
92
- def parse_http_tool_policy(header: str | None, query_string: bytes) -> ToolPolicySnapshot:
93
- header_policy = parse_tool_policy(header)
94
- # 保留空值并在 URL 解码后检查重复,tools= 与缺省不能合并为同一种状态。
95
- values = parse_qs(query_string.decode("latin1"), keep_blank_values=True).get("tools", [])
96
- if len(values) > 1:
97
- return ToolPolicySnapshot("invalid", error="tools 查询参数不能重复")
98
- url_policy = parse_tool_policy(
99
- {"version": 1, "allowed_tools": values[0].split(",") if values[0] else []}
100
- if values else None
101
- )
102
- if header_policy.mode == "invalid":
103
- return header_policy
104
- if url_policy.mode == "invalid":
105
- return ToolPolicySnapshot("invalid", error=f"tools 查询参数无效: {url_policy.error}")
106
- if header_policy.mode == "unrestricted":
107
- return url_policy
108
- if url_policy.mode == "unrestricted":
109
- return header_policy
110
- return ToolPolicySnapshot(
111
- "restricted", tuple(sorted(set(header_policy.allowed_tools).intersection(url_policy.allowed_tools)))
112
- )
113
-
114
-
115
- class PolicyAwareFastMCP(FastMCP):
116
- """让 MCP 列表与调用在 FastMCP handler 边界共用策略快照。"""
117
-
118
- def __init__(self, *args: Any, **kwargs: Any) -> None:
119
- self._tool_policy_bindings: dict[str, str] | None = None
120
- self._tool_policy_source: Literal["environment", "request"] = "environment"
121
- self._tool_policy_environment_snapshot = ToolPolicySnapshot("unrestricted")
122
- super().__init__(*args, **kwargs)
123
-
124
- def configure_tool_policy(
125
- self,
126
- bindings: Mapping[str, str],
127
- *,
128
- source: Literal["environment", "request"],
129
- ) -> None:
130
- registered = set(self._tool_manager._tools)
131
- bound = set(bindings)
132
- if registered != bound:
133
- missing = sorted(registered - bound)
134
- extra = sorted(bound - registered)
135
- raise ValueError(f"工具策略绑定不完整: missing={missing}, extra={extra}")
136
- for stable_id in bindings.values():
137
- # 登记校验只依赖提供方稳定 ID,不依赖 Wingent-X 的产品分类。
138
- require_registered_tool_id(stable_id)
139
- self._tool_policy_bindings = dict(bindings)
140
- self._tool_policy_source = source
141
- if source == "environment":
142
- self._tool_policy_environment_snapshot = parse_tool_policy(os.getenv(TOOL_POLICY_ENV))
143
-
144
- @property
145
- def tool_policy_bindings(self) -> dict[str, str]:
146
- return dict(self._tool_policy_bindings or {})
147
-
148
- async def list_tools(self): # type: ignore[no-untyped-def]
149
- tools = await super().list_tools()
150
- bindings = self._require_bindings()
151
- snapshot = self._current_snapshot()
152
- return [
153
- tool
154
- for tool in tools
155
- if snapshot.violation_for(bindings[tool.name]) is None
156
- ]
157
-
158
- async def call_tool(self, name: str, arguments: dict[str, Any]) -> Sequence[Any] | dict[str, Any]:
159
- bindings = self._require_bindings()
160
- stable_id = bindings.get(name)
161
- if stable_id is None:
162
- raise ToolError(json.dumps(
163
- {
164
- "status": "failed",
165
- "errorCode": TOOL_POLICY_INVALID,
166
- "message": "工具未登记稳定 ID",
167
- },
168
- ensure_ascii=False,
169
- ))
170
- violation = self._current_snapshot().violation_for(stable_id)
171
- if violation is not None:
172
- raise ToolError(json.dumps(violation.payload(camel_case=True), ensure_ascii=False))
173
- return await super().call_tool(name, arguments)
174
-
175
- def _require_bindings(self) -> dict[str, str]:
176
- if self._tool_policy_bindings is None:
177
- raise RuntimeError("MCP 工具策略绑定尚未配置")
178
- return self._tool_policy_bindings
179
-
180
- def _current_snapshot(self) -> ToolPolicySnapshot:
181
- if self._tool_policy_source == "request":
182
- snapshot = current_http_tool_policy.get()
183
- if snapshot is not None:
184
- return snapshot
185
- # 保留未经过 HTTP 入口的请求头上下文用法,绝不回退共享 env。
186
- return parse_tool_policy(current_tool_policy_header.get())
187
- return self._tool_policy_environment_snapshot
188
-
189
-
190
- def configure_tool_policy(
191
- server: PolicyAwareFastMCP,
192
- bindings: Mapping[str, str],
193
- *,
194
- source: Literal["environment", "request"],
195
- ) -> PolicyAwareFastMCP:
196
- server.configure_tool_policy(bindings, source=source)
197
- return server
@@ -1,59 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Any
4
-
5
- from ..errors import QingflowApiError, raise_tool_error
6
- from ..json_types import JSONObject, JSONValue
7
- from .base import ToolBase
8
-
9
-
10
- class PortalWriter(ToolBase):
11
- """门户写手——POST /dash、POST /dash/{dashKey}、POST /dash/{dashKey}/baseInfo 的唯一落点。
12
-
13
- 仅限 builder facade 与 solution executor 内部使用;
14
- 不注册 MCP 工具、不进 public_surface、不暴露 CLI。
15
- 公开层的门户创建/更新唯一入口是 ``builder portal apply`` / ``portal_apply``。
16
- """
17
-
18
- def create_portal(self, *, profile: str, payload: JSONObject) -> JSONObject:
19
- """创建门户(内部 writer;公开入口请走 builder portal apply)。"""
20
- body = self._require_dict(payload)
21
- return self._request(profile, "POST", "/dash", tool_name="创建门户", json_body=body)
22
-
23
- def update_portal(self, *, profile: str, dash_key: str, payload: JSONObject) -> JSONObject:
24
- """更新门户配置(内部 writer;公开入口请走 builder portal apply)。"""
25
- self._require_dash_key(dash_key)
26
- body = self._require_dict(payload)
27
- return self._request(profile, "POST", f"/dash/{dash_key}", tool_name="更新门户配置", json_body=body)
28
-
29
- def update_portal_base_info(self, *, profile: str, dash_key: str, payload: JSONObject) -> JSONObject:
30
- """更新门户基础信息(内部 writer;公开入口请走 builder portal apply)。"""
31
- self._require_dash_key(dash_key)
32
- body = self._require_dict(payload)
33
- return self._request(profile, "POST", f"/dash/{dash_key}/baseInfo", tool_name="更新门户基础信息", json_body=body)
34
-
35
- def _require_dash_key(self, dash_key: str) -> None:
36
- if not dash_key:
37
- raise_tool_error(QingflowApiError.config_error("dash_key is required"))
38
-
39
- def _request(
40
- self,
41
- profile: str,
42
- method: str,
43
- path: str,
44
- *,
45
- json_body: JSONValue = None,
46
- params: JSONObject | None = None,
47
- tool_name: str,
48
- **extra: Any,
49
- ) -> JSONObject:
50
- def runner(session_profile, context):
51
- result = self.backend.request(method, context, path, json_body=json_body, params=params)
52
- response: JSONObject = {"profile": profile, "ws_id": session_profile.selected_ws_id, "result": result}
53
- response.update(extra)
54
- return response
55
-
56
- return self._run(profile, runner, tool_name=tool_name)
57
-
58
-
59
- __all__ = ["PortalWriter", "QingflowApiError", "raise_tool_error"]