@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.
- package/README.md +3 -3
- package/package.json +1 -1
- package/pyproject.toml +1 -2
- package/skills/qingflow-cli/SKILL.md +2 -104
- package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +2 -2
- package/skills/qingflow-cli/reference/builder/30-schema-fields.md +5 -22
- package/skills/qingflow-cli/reference/builder/70-portal.md +16 -37
- package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +3 -3
- package/skills/qingflow-cli/reference/examples/portal/portal_sections_all_types.example.json +7 -6
- package/src/qingflow_mcp/app_form_compat.py +0 -3
- package/src/qingflow_mcp/app_form_validator.py +20 -89
- package/src/qingflow_mcp/builder_facade/app_form.py +112 -629
- package/src/qingflow_mcp/builder_facade/models.py +13 -164
- package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +1 -15
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +38 -112
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +8 -31
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +10 -6
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +10 -33
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +10 -6
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +18 -11
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +10 -6
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +10 -6
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +10 -6
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +12 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +15 -73
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +29 -90
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/formula.json +0 -43
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +19 -27
- package/src/qingflow_mcp/builder_facade/service.py +72 -1143
- package/src/qingflow_mcp/cli/commands/builder.py +5 -114
- package/src/qingflow_mcp/cli/main.py +1 -34
- package/src/qingflow_mcp/cloud_context.py +1 -16
- package/src/qingflow_mcp/public_surface.py +8 -69
- package/src/qingflow_mcp/response_trim.py +1 -2
- package/src/qingflow_mcp/server.py +6 -12
- package/src/qingflow_mcp/server_app_builder.py +6 -41
- package/src/qingflow_mcp/server_app_user.py +5 -9
- package/src/qingflow_mcp/server_cloud.py +7 -43
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +0 -73
- package/src/qingflow_mcp/solution/executor.py +2 -5
- package/src/qingflow_mcp/tools/ai_builder_tools.py +12 -110
- package/src/qingflow_mcp/tools/portal_tools.py +54 -4
- package/src/qingflow_mcp/tools/solution_tools.py +0 -2
- package/skills/qingflow-cli/reference/examples/portal/portal_apply_v1alpha1.example.json +0 -100
- package/src/qingflow_mcp/builder_facade/portals_schema.py +0 -397
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_content_classification.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_content_extraction.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_content_generation.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_document_parsing.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_image_generation.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_image_recognition.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_machine_translation.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_text_summary.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date_duration.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/embed_field.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/image_select.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/location.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ocr.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/rich_text.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/portals/manifest.json +0 -59
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/complete.json +0 -1397
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/carousel.json +0 -193
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/chart.json +0 -643
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/favourite.json +0 -203
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/filter.json +0 -268
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/grid.json +0 -265
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/link.json +0 -131
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/task.json +0 -199
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/text.json +0 -123
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/view.json +0 -196
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/summary.json +0 -800
- package/src/qingflow_mcp/tool_catalog.py +0 -387
- package/src/qingflow_mcp/tool_catalog_i18n.py +0 -185
- package/src/qingflow_mcp/tool_policy.py +0 -197
- package/src/qingflow_mcp/tools/portal_writer.py +0 -59
|
@@ -1,397 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
from copy import deepcopy
|
|
6
|
-
from dataclasses import dataclass
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from typing import Any
|
|
9
|
-
|
|
10
|
-
from jsonschema import Draft202012Validator, FormatChecker
|
|
11
|
-
from referencing import Registry, Resource
|
|
12
|
-
|
|
13
|
-
from .app_form_schema import compact_schema_errors, schema_error_message
|
|
14
|
-
|
|
15
|
-
JSONObject = dict[str, Any]
|
|
16
|
-
PORTALS_SCHEMA_ROOT = Path(__file__).with_name("schemas") / "portals"
|
|
17
|
-
PORTALS_SUPPORTED_SCHEMA_KINDS = (
|
|
18
|
-
"summary",
|
|
19
|
-
"complete",
|
|
20
|
-
"chart",
|
|
21
|
-
"view",
|
|
22
|
-
"grid",
|
|
23
|
-
"filter",
|
|
24
|
-
"text",
|
|
25
|
-
"link",
|
|
26
|
-
"carousel",
|
|
27
|
-
"task",
|
|
28
|
-
"favourite",
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class PortalsContractError(ValueError):
|
|
33
|
-
"""Raised when a portals declaration violates the versioned schema contract."""
|
|
34
|
-
|
|
35
|
-
def __init__(self, error_code: str, message: str, *, path: str | None = None, details: JSONObject | None = None) -> None:
|
|
36
|
-
super().__init__(message)
|
|
37
|
-
self.error_code = error_code
|
|
38
|
-
self.message = message
|
|
39
|
-
self.path = path
|
|
40
|
-
self.details = details or {}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def compact_none_values(value: Any) -> Any:
|
|
44
|
-
"""Recursively drop None values so pydantic dumps match the JSON Schema contract."""
|
|
45
|
-
if isinstance(value, dict):
|
|
46
|
-
return {key: compact_none_values(item) for key, item in value.items() if item is not None}
|
|
47
|
-
if isinstance(value, list):
|
|
48
|
-
return [compact_none_values(item) for item in value]
|
|
49
|
-
return value
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
@dataclass(frozen=True, slots=True)
|
|
53
|
-
class PortalsSchemaContext:
|
|
54
|
-
"""Resolved schema context for one portals schema version."""
|
|
55
|
-
|
|
56
|
-
schema_version: str
|
|
57
|
-
api_version: str
|
|
58
|
-
schemas: dict[str, JSONObject]
|
|
59
|
-
schema_files: dict[str, str]
|
|
60
|
-
ref_registry: Registry
|
|
61
|
-
supported_schema_kinds: tuple[str, ...]
|
|
62
|
-
contract_source: str
|
|
63
|
-
contract_version: str
|
|
64
|
-
|
|
65
|
-
@property
|
|
66
|
-
def summary_schema(self) -> JSONObject:
|
|
67
|
-
return self.schemas["summary"]
|
|
68
|
-
|
|
69
|
-
@property
|
|
70
|
-
def complete_schema(self) -> JSONObject:
|
|
71
|
-
return self.schemas["complete"]
|
|
72
|
-
|
|
73
|
-
def schema_for_kind(self, schema_kind: str) -> JSONObject:
|
|
74
|
-
requested = str(schema_kind or "").strip()
|
|
75
|
-
schema = self.schemas.get(requested)
|
|
76
|
-
if schema is not None:
|
|
77
|
-
return schema
|
|
78
|
-
raise PortalsContractError(
|
|
79
|
-
"UNSUPPORTED_SCHEMA_KIND",
|
|
80
|
-
f"unsupported portals schema kind: {requested}",
|
|
81
|
-
details={"supportedKinds": list(self.supported_schema_kinds)},
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
def _validate(self, payload: JSONObject, *, schema_kind: str) -> None:
|
|
85
|
-
schema = self.schema_for_kind(schema_kind)
|
|
86
|
-
validator = Draft202012Validator(schema, format_checker=FormatChecker(), registry=self.ref_registry)
|
|
87
|
-
errors = sorted(validator.iter_errors(payload), key=lambda item: (list(item.absolute_path), item.message))
|
|
88
|
-
if not errors:
|
|
89
|
-
return
|
|
90
|
-
compact_errors = compact_schema_errors(errors, schema)
|
|
91
|
-
error = compact_errors[0] if compact_errors else errors[0]
|
|
92
|
-
pointer = "".join(f"/{str(part).replace('~', '~0').replace('/', '~1')}" for part in error.absolute_path) or "/"
|
|
93
|
-
raise PortalsContractError(
|
|
94
|
-
"INVALID_SPEC",
|
|
95
|
-
schema_error_message(error),
|
|
96
|
-
path=pointer,
|
|
97
|
-
details={
|
|
98
|
-
"validator": error.validator,
|
|
99
|
-
"schemaPath": "/".join(str(part) for part in error.absolute_schema_path),
|
|
100
|
-
"schemaKind": schema_kind,
|
|
101
|
-
},
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
def validate_upsert(self, payload: JSONObject, *, schema_kind: str = "complete") -> None:
|
|
105
|
-
"""Validate a single (alias-normalized) portal declaration."""
|
|
106
|
-
self._validate(payload, schema_kind=schema_kind)
|
|
107
|
-
|
|
108
|
-
def validate_declaration(self, payload: JSONObject, *, schema_kind: str = "complete") -> None:
|
|
109
|
-
"""Validate one portal declaration used by ``portal_apply``."""
|
|
110
|
-
if not isinstance(payload, dict):
|
|
111
|
-
raise PortalsContractError("INVALID_SPEC", "portals declaration must be a JSON object", path="/")
|
|
112
|
-
self._validate(payload, schema_kind=schema_kind)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
class PortalsSchemaRegistry:
|
|
116
|
-
"""Registry for versioned portals declaration schemas (mirrors ViewsSchemaRegistry)."""
|
|
117
|
-
|
|
118
|
-
def __init__(self, root: Path = PORTALS_SCHEMA_ROOT, *, enabled_versions: tuple[str, ...] | None = None) -> None:
|
|
119
|
-
self.root = root
|
|
120
|
-
self.manifest = self._read_json(root / "manifest.json")
|
|
121
|
-
versions = self.manifest.get("versions")
|
|
122
|
-
if not isinstance(versions, dict) or not versions:
|
|
123
|
-
raise RuntimeError("portals schema manifest must define versions")
|
|
124
|
-
env_versions = self._configured_versions()
|
|
125
|
-
defaults = tuple(
|
|
126
|
-
str(version)
|
|
127
|
-
for version, metadata in versions.items()
|
|
128
|
-
if isinstance(metadata, dict) and bool(metadata.get("enabledByDefault"))
|
|
129
|
-
)
|
|
130
|
-
configured = tuple(enabled_versions) if enabled_versions is not None else (env_versions or defaults)
|
|
131
|
-
self.stored_versions = tuple(str(version) for version in versions)
|
|
132
|
-
self.latest_version = str(self.manifest.get("latestVersion") or "")
|
|
133
|
-
self._stored_api_to_version: dict[str, str] = {}
|
|
134
|
-
for version, metadata in versions.items():
|
|
135
|
-
if not isinstance(metadata, dict):
|
|
136
|
-
raise RuntimeError(f"portals schema metadata must be an object: {version}")
|
|
137
|
-
api_version = str(metadata.get("apiVersion") or "")
|
|
138
|
-
if not api_version:
|
|
139
|
-
raise RuntimeError(f"portals schema version has no apiVersion: {version}")
|
|
140
|
-
if api_version in self._stored_api_to_version:
|
|
141
|
-
raise RuntimeError(f"duplicate portals apiVersion: {api_version}")
|
|
142
|
-
self._stored_api_to_version[api_version] = str(version)
|
|
143
|
-
self.enabled_versions = configured
|
|
144
|
-
if self.latest_version not in versions:
|
|
145
|
-
raise RuntimeError("portals latestVersion is not stored")
|
|
146
|
-
if self.latest_version not in self.enabled_versions:
|
|
147
|
-
raise RuntimeError("portals latestVersion must remain enabled")
|
|
148
|
-
self._contexts: dict[str, PortalsSchemaContext] = {}
|
|
149
|
-
self._api_to_version: dict[str, str] = {}
|
|
150
|
-
for version in self.enabled_versions:
|
|
151
|
-
metadata = versions.get(version)
|
|
152
|
-
if not isinstance(metadata, dict):
|
|
153
|
-
raise RuntimeError(f"enabled portals schema version is not stored: {version}")
|
|
154
|
-
api_version = str(metadata.get("apiVersion") or "")
|
|
155
|
-
schema_files = metadata.get("schemaFiles")
|
|
156
|
-
if not isinstance(schema_files, dict):
|
|
157
|
-
schema_files = {kind: f"{version}/{kind}.json" for kind in ("summary", "complete")}
|
|
158
|
-
supported_schema_kinds = tuple(str(item) for item in metadata.get("schemaKinds") or schema_files)
|
|
159
|
-
if not {"summary", "complete"}.issubset(supported_schema_kinds):
|
|
160
|
-
raise RuntimeError(f"portals schema version must support summary and complete kinds: {version}")
|
|
161
|
-
schemas: dict[str, JSONObject] = {}
|
|
162
|
-
paths: dict[str, Path] = {}
|
|
163
|
-
for name in supported_schema_kinds:
|
|
164
|
-
relative_path = schema_files.get(name)
|
|
165
|
-
if not isinstance(relative_path, str) or not relative_path:
|
|
166
|
-
raise RuntimeError(f"portals schema kind has no schema file: {version}/{name}")
|
|
167
|
-
path = root / relative_path
|
|
168
|
-
schema = self._read_json(path)
|
|
169
|
-
schemas[name] = schema
|
|
170
|
-
paths[name] = path
|
|
171
|
-
ref_registry = Registry()
|
|
172
|
-
for name, schema in schemas.items():
|
|
173
|
-
schema_id = str(schema.get("$id") or "")
|
|
174
|
-
if version not in schema_id:
|
|
175
|
-
raise RuntimeError(f"portals {name} schema $id mismatch for {version}: {schema_id}")
|
|
176
|
-
Draft202012Validator.check_schema(schema)
|
|
177
|
-
resource = Resource.from_contents(schema)
|
|
178
|
-
if schema_id:
|
|
179
|
-
ref_registry = ref_registry.with_resource(schema_id, resource)
|
|
180
|
-
ref_registry = ref_registry.with_resource(paths[name].resolve().as_uri(), resource)
|
|
181
|
-
for name, schema in schemas.items():
|
|
182
|
-
self._validate_local_refs(schema, paths[name], schemas, paths)
|
|
183
|
-
context = PortalsSchemaContext(
|
|
184
|
-
version,
|
|
185
|
-
api_version,
|
|
186
|
-
schemas,
|
|
187
|
-
{name: str(schema_files[name]) for name in supported_schema_kinds},
|
|
188
|
-
ref_registry,
|
|
189
|
-
supported_schema_kinds,
|
|
190
|
-
str(metadata.get("contractSource") or "portal_apply"),
|
|
191
|
-
str(metadata.get("contractVersion") or version),
|
|
192
|
-
)
|
|
193
|
-
self._contexts[version] = context
|
|
194
|
-
self._api_to_version[api_version] = version
|
|
195
|
-
|
|
196
|
-
def schema(
|
|
197
|
-
self,
|
|
198
|
-
*,
|
|
199
|
-
schema_version: str | None = None,
|
|
200
|
-
schema_kind: str | None = None,
|
|
201
|
-
) -> JSONObject:
|
|
202
|
-
context = self.resolve_schema_version(schema_version)
|
|
203
|
-
requested_kind = str(schema_kind or "summary").strip()
|
|
204
|
-
if requested_kind not in context.supported_schema_kinds:
|
|
205
|
-
raise PortalsContractError(
|
|
206
|
-
"UNSUPPORTED_SCHEMA_KIND",
|
|
207
|
-
f"unsupported portals schema kind: {requested_kind}",
|
|
208
|
-
details={"supportedKinds": list(context.supported_schema_kinds)},
|
|
209
|
-
)
|
|
210
|
-
schema, dependencies = self._bundle_schema(
|
|
211
|
-
context.schema_for_kind(requested_kind),
|
|
212
|
-
schema_path=(self.root / context.schema_files[requested_kind]),
|
|
213
|
-
schemas=context.schemas,
|
|
214
|
-
schema_files=context.schema_files,
|
|
215
|
-
)
|
|
216
|
-
return {
|
|
217
|
-
"schemaVersion": context.schema_version,
|
|
218
|
-
"apiVersion": context.api_version,
|
|
219
|
-
"latestVersion": self.latest_version,
|
|
220
|
-
"storedVersions": list(self.stored_versions),
|
|
221
|
-
"enabledVersions": list(self.enabled_versions),
|
|
222
|
-
"supportedSchemaKinds": list(context.supported_schema_kinds),
|
|
223
|
-
"schemaKind": requested_kind,
|
|
224
|
-
"schemaFile": context.schema_files[requested_kind],
|
|
225
|
-
"schemaBundled": bool(dependencies),
|
|
226
|
-
"schemaDependencies": dependencies,
|
|
227
|
-
"schema": schema,
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
@classmethod
|
|
231
|
-
def _bundle_schema(
|
|
232
|
-
cls,
|
|
233
|
-
schema: JSONObject,
|
|
234
|
-
*,
|
|
235
|
-
schema_path: Path,
|
|
236
|
-
schemas: dict[str, JSONObject],
|
|
237
|
-
schema_files: dict[str, str],
|
|
238
|
-
) -> tuple[JSONObject, list[str]]:
|
|
239
|
-
"""Inline reachable cross-file definitions for standalone consumers."""
|
|
240
|
-
bundled = deepcopy(schema)
|
|
241
|
-
definitions = bundled.get("$defs")
|
|
242
|
-
if not isinstance(definitions, dict):
|
|
243
|
-
definitions = {}
|
|
244
|
-
dependencies: set[str] = set()
|
|
245
|
-
# schema_path is <root>/<version>/components/<file> or <root>/<version>/<file>.
|
|
246
|
-
# Find the manifest root from the configured path instead of relying on
|
|
247
|
-
# a fixed components depth.
|
|
248
|
-
cls_root = schema_path.parent
|
|
249
|
-
for candidate in schema_path.parents:
|
|
250
|
-
if (candidate / "manifest.json").is_file():
|
|
251
|
-
cls_root = candidate
|
|
252
|
-
break
|
|
253
|
-
paths = {name: cls_root / relative for name, relative in schema_files.items()}
|
|
254
|
-
|
|
255
|
-
def resolve_pointer(document: JSONObject, fragment: str, *, ref: str) -> Any:
|
|
256
|
-
target: Any = document
|
|
257
|
-
for raw_part in fragment[1:].split("/") if fragment.startswith("/") else ():
|
|
258
|
-
part = raw_part.replace("~1", "/").replace("~0", "~")
|
|
259
|
-
if not isinstance(target, dict) or part not in target:
|
|
260
|
-
raise RuntimeError(f"portals schema contains a dangling $ref in {schema_path}: {ref}")
|
|
261
|
-
target = target[part]
|
|
262
|
-
return target
|
|
263
|
-
|
|
264
|
-
pending: list[tuple[str, JSONObject, Path]] = []
|
|
265
|
-
|
|
266
|
-
def add_definition(name: str, source: JSONObject, source_path: Path) -> None:
|
|
267
|
-
if name in definitions:
|
|
268
|
-
return
|
|
269
|
-
source_definitions = source.get("$defs")
|
|
270
|
-
if not isinstance(source_definitions, dict) or name not in source_definitions:
|
|
271
|
-
raise RuntimeError(f"portals schema references an unknown definition: {name}")
|
|
272
|
-
definitions[name] = deepcopy(source_definitions[name])
|
|
273
|
-
pending.append((name, source, source_path))
|
|
274
|
-
|
|
275
|
-
def rewrite(value: Any, source: JSONObject, source_path: Path) -> None:
|
|
276
|
-
if isinstance(value, dict):
|
|
277
|
-
ref = value.get("$ref")
|
|
278
|
-
if isinstance(ref, str):
|
|
279
|
-
ref_file, _, fragment = ref.partition("#")
|
|
280
|
-
target_source = source
|
|
281
|
-
target_path = source_path
|
|
282
|
-
if ref_file:
|
|
283
|
-
target_path = (source_path.parent / ref_file).resolve()
|
|
284
|
-
target_name = next(
|
|
285
|
-
(name for name, path in paths.items() if path.resolve() == target_path),
|
|
286
|
-
None,
|
|
287
|
-
)
|
|
288
|
-
if target_name is None:
|
|
289
|
-
raise RuntimeError(f"portals schema contains an external $ref in {source_path}: {ref}")
|
|
290
|
-
target_source = schemas[target_name]
|
|
291
|
-
dependencies.add(schema_files[target_name])
|
|
292
|
-
value["$ref"] = f"#{fragment}"
|
|
293
|
-
local_ref = str(value["$ref"])
|
|
294
|
-
if local_ref.startswith("#/$defs/"):
|
|
295
|
-
name = local_ref.removeprefix("#/$defs/").split("/", 1)[0]
|
|
296
|
-
add_definition(name, target_source, target_path)
|
|
297
|
-
else:
|
|
298
|
-
resolve_pointer(target_source, fragment, ref=ref)
|
|
299
|
-
for child in value.values():
|
|
300
|
-
rewrite(child, source, source_path)
|
|
301
|
-
elif isinstance(value, list):
|
|
302
|
-
for child in value:
|
|
303
|
-
rewrite(child, source, source_path)
|
|
304
|
-
|
|
305
|
-
for key, value in bundled.items():
|
|
306
|
-
if key != "$defs":
|
|
307
|
-
rewrite(value, schema, schema_path)
|
|
308
|
-
while pending:
|
|
309
|
-
name, source, source_path = pending.pop()
|
|
310
|
-
rewrite(definitions[name], source, source_path)
|
|
311
|
-
if definitions:
|
|
312
|
-
bundled["$defs"] = definitions
|
|
313
|
-
return bundled, sorted(dependencies)
|
|
314
|
-
|
|
315
|
-
def resolve_schema_version(self, schema_version: str | None) -> PortalsSchemaContext:
|
|
316
|
-
version = str(schema_version or self.latest_version).strip()
|
|
317
|
-
if version not in self.stored_versions:
|
|
318
|
-
raise self._version_error("UNSUPPORTED_SCHEMA_VERSION", version)
|
|
319
|
-
if version not in self.enabled_versions:
|
|
320
|
-
raise self._version_error("SCHEMA_VERSION_DISABLED", version)
|
|
321
|
-
return self._contexts[version]
|
|
322
|
-
|
|
323
|
-
def resolve_api_version(self, api_version: str) -> PortalsSchemaContext:
|
|
324
|
-
version = self._api_to_version.get(api_version)
|
|
325
|
-
if version is not None:
|
|
326
|
-
return self._contexts[version]
|
|
327
|
-
stored = self._stored_api_to_version.get(api_version)
|
|
328
|
-
if stored is not None:
|
|
329
|
-
raise self._version_error("SCHEMA_VERSION_DISABLED", stored)
|
|
330
|
-
raise self._version_error("UNSUPPORTED_SCHEMA_VERSION", api_version)
|
|
331
|
-
|
|
332
|
-
def _version_error(self, error_code: str, requested: str) -> PortalsContractError:
|
|
333
|
-
return PortalsContractError(
|
|
334
|
-
error_code,
|
|
335
|
-
f"unsupported portals schema selection: {requested}",
|
|
336
|
-
details={
|
|
337
|
-
"requestedVersion": requested,
|
|
338
|
-
"latestVersion": self.latest_version,
|
|
339
|
-
"storedVersions": list(self.stored_versions),
|
|
340
|
-
"enabledVersions": list(self.enabled_versions),
|
|
341
|
-
},
|
|
342
|
-
)
|
|
343
|
-
|
|
344
|
-
@staticmethod
|
|
345
|
-
def _read_json(path: Path) -> JSONObject:
|
|
346
|
-
value = json.loads(path.read_text(encoding="utf-8"))
|
|
347
|
-
if not isinstance(value, dict):
|
|
348
|
-
raise RuntimeError(f"portals schema artifact must be an object: {path}")
|
|
349
|
-
return value
|
|
350
|
-
|
|
351
|
-
@staticmethod
|
|
352
|
-
def _validate_local_refs(
|
|
353
|
-
document: JSONObject,
|
|
354
|
-
path: Path,
|
|
355
|
-
documents: dict[str, JSONObject],
|
|
356
|
-
paths: dict[str, Path],
|
|
357
|
-
) -> None:
|
|
358
|
-
stack: list[Any] = [document]
|
|
359
|
-
while stack:
|
|
360
|
-
value = stack.pop()
|
|
361
|
-
if isinstance(value, dict):
|
|
362
|
-
ref = value.get("$ref")
|
|
363
|
-
if ref is not None:
|
|
364
|
-
if not isinstance(ref, str):
|
|
365
|
-
raise RuntimeError(f"portals schema contains an invalid $ref in {path}: {ref}")
|
|
366
|
-
ref_file, _, fragment = ref.partition("#")
|
|
367
|
-
target_document = document
|
|
368
|
-
if ref_file:
|
|
369
|
-
target_path = (path.parent / ref_file).resolve()
|
|
370
|
-
if target_path not in {item.resolve() for item in paths.values()}:
|
|
371
|
-
raise RuntimeError(f"portals schema contains an external $ref in {path}: {ref}")
|
|
372
|
-
target_name = next(name for name, item in paths.items() if item.resolve() == target_path)
|
|
373
|
-
target_document = documents[target_name]
|
|
374
|
-
target: Any = target_document
|
|
375
|
-
for raw_part in fragment[1:].split("/") if fragment.startswith("/") else ():
|
|
376
|
-
part = raw_part.replace("~1", "/").replace("~0", "~")
|
|
377
|
-
if not isinstance(target, dict) or part not in target:
|
|
378
|
-
raise RuntimeError(f"portals schema contains a dangling $ref in {path}: {ref}")
|
|
379
|
-
target = target[part]
|
|
380
|
-
stack.extend(value.values())
|
|
381
|
-
elif isinstance(value, list):
|
|
382
|
-
stack.extend(value)
|
|
383
|
-
|
|
384
|
-
@staticmethod
|
|
385
|
-
def _configured_versions() -> tuple[str, ...]:
|
|
386
|
-
raw = os.environ.get("QINGFLOW_PORTALS_SCHEMA_VERSIONS", "")
|
|
387
|
-
return tuple(part.strip() for part in raw.split(",") if part.strip())
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
_REGISTRY: PortalsSchemaRegistry | None = None
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
def portals_schema_registry() -> PortalsSchemaRegistry:
|
|
394
|
-
global _REGISTRY
|
|
395
|
-
if _REGISTRY is None:
|
|
396
|
-
_REGISTRY = PortalsSchemaRegistry()
|
|
397
|
-
return _REGISTRY
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$defs": {
|
|
3
|
-
"EmptyConfig": {
|
|
4
|
-
"additionalProperties": false,
|
|
5
|
-
"description": "该字段类型没有特殊配置。",
|
|
6
|
-
"type": "object"
|
|
7
|
-
},
|
|
8
|
-
"FieldCommon": {
|
|
9
|
-
"allOf": [
|
|
10
|
-
{
|
|
11
|
-
"if": {
|
|
12
|
-
"properties": {
|
|
13
|
-
"defaultType": {
|
|
14
|
-
"const": 2
|
|
15
|
-
}
|
|
16
|
-
},
|
|
17
|
-
"required": [
|
|
18
|
-
"defaultType"
|
|
19
|
-
]
|
|
20
|
-
},
|
|
21
|
-
"then": {
|
|
22
|
-
"properties": {
|
|
23
|
-
"defaultValue": {
|
|
24
|
-
"type": "null"
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
"if": {
|
|
31
|
-
"properties": {
|
|
32
|
-
"defaultType": {
|
|
33
|
-
"const": 3
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
"required": [
|
|
37
|
-
"defaultType"
|
|
38
|
-
]
|
|
39
|
-
},
|
|
40
|
-
"then": {
|
|
41
|
-
"properties": {
|
|
42
|
-
"defaultValue": {
|
|
43
|
-
"minLength": 1,
|
|
44
|
-
"type": [
|
|
45
|
-
"string",
|
|
46
|
-
"null"
|
|
47
|
-
]
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
],
|
|
53
|
-
"anyOf": [
|
|
54
|
-
{
|
|
55
|
-
"required": [
|
|
56
|
-
"queId"
|
|
57
|
-
]
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
"required": [
|
|
61
|
-
"name"
|
|
62
|
-
]
|
|
63
|
-
}
|
|
64
|
-
],
|
|
65
|
-
"description": "顶层业务字段的公共属性;字段类型由 TopField 的判别分支定义。",
|
|
66
|
-
"properties": {
|
|
67
|
-
"beingRequired": {
|
|
68
|
-
"description": "该字段是否必填;缺失时保留已有字段当前值。",
|
|
69
|
-
"type": "boolean"
|
|
70
|
-
},
|
|
71
|
-
"defaultType": {
|
|
72
|
-
"default": 1,
|
|
73
|
-
"description": "默认值类型,与 defaultValue 联动;1 表示静态值,2 表示关联/绑定值且 defaultValue 只能省略或为 null,3 表示公式值且提供 defaultValue 时必须为非空公式文本。省略 defaultType 但提供 defaultValue 时按 1 处理。",
|
|
74
|
-
"enum": [
|
|
75
|
-
1,
|
|
76
|
-
2,
|
|
77
|
-
3
|
|
78
|
-
],
|
|
79
|
-
"type": "integer"
|
|
80
|
-
},
|
|
81
|
-
"defaultValue": {
|
|
82
|
-
"$ref": "#/$defs/ScalarDefaultValue",
|
|
83
|
-
"description": "普通字段的默认值;后端 queDefaultValue 为字符串,因此数字等字面量也必须以字符串传入。defaultType 为 1 时表示静态文本,为 3 时表示公式文本,为 2 时不携带字面量默认值。表格、地址、附件、引用等结构化默认值必须放在具体字段 config.defaultValues。显式 null 表示清除默认值。"
|
|
84
|
-
},
|
|
85
|
-
"name": {
|
|
86
|
-
"description": "字段展示名称;新增字段必填,已有字段可在携带 queId 时省略;同父级允许同名(后端以 queId 为字段身份),配置引用建议携带 queId。",
|
|
87
|
-
"minLength": 1,
|
|
88
|
-
"type": "string"
|
|
89
|
-
},
|
|
90
|
-
"queId": {
|
|
91
|
-
"description": "已有字段的后端 ID;更新时用于精确定位并复用,新增字段通常省略。",
|
|
92
|
-
"minimum": 1,
|
|
93
|
-
"type": "integer"
|
|
94
|
-
},
|
|
95
|
-
"questionDescription": {
|
|
96
|
-
"description": "字段说明;null 或空字符串表示清空。",
|
|
97
|
-
"type": [
|
|
98
|
-
"string",
|
|
99
|
-
"null"
|
|
100
|
-
]
|
|
101
|
-
},
|
|
102
|
-
"questionType": {
|
|
103
|
-
"description": "字段类型;具体可用值由当前字段详情 Schema 的判别分支约束。",
|
|
104
|
-
"type": "string"
|
|
105
|
-
},
|
|
106
|
-
"width": {
|
|
107
|
-
"description": "字段在所在行的宽度百分比(1-100 整数);后端不校验总和与取值,存量数据存在 33/34 等三等分布值。",
|
|
108
|
-
"maximum": 100,
|
|
109
|
-
"minimum": 1,
|
|
110
|
-
"type": "integer"
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
"type": "object"
|
|
114
|
-
},
|
|
115
|
-
"ScalarDefaultValue": {
|
|
116
|
-
"description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。defaultType 为 3 时,通过 AppForm Schema 查询能力传入当前 schema_version 并设置 schema_kind=formula 获取公式语法、字段引用、运算符和全部函数说明;CLI 使用 builder app-form schema --schema-kind formula。需要配置结构化默认值时,传入当前 schema_version,并将字段的 questionType 作为 field_type 获取字段详细 Schema。",
|
|
117
|
-
"type": [
|
|
118
|
-
"null",
|
|
119
|
-
"string"
|
|
120
|
-
]
|
|
121
|
-
}
|
|
122
|
-
},
|
|
123
|
-
"$id": "https://builder.qingflow.com/schemas/app-form-apply/v1alpha1/fields/ai_content_classification.json",
|
|
124
|
-
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
125
|
-
"allOf": [
|
|
126
|
-
{
|
|
127
|
-
"$ref": "#/$defs/FieldCommon"
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
"properties": {
|
|
131
|
-
"config": {
|
|
132
|
-
"$ref": "#/$defs/EmptyConfig"
|
|
133
|
-
},
|
|
134
|
-
"questionType": {
|
|
135
|
-
"const": "ai_content_classification"
|
|
136
|
-
}
|
|
137
|
-
},
|
|
138
|
-
"required": [
|
|
139
|
-
"questionType"
|
|
140
|
-
]
|
|
141
|
-
}
|
|
142
|
-
],
|
|
143
|
-
"title": "Qingflow AppForm v1alpha1 ai_content_classification field",
|
|
144
|
-
"type": "object",
|
|
145
|
-
"unevaluatedProperties": false
|
|
146
|
-
}
|
package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_content_extraction.json
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$defs": {
|
|
3
|
-
"EmptyConfig": {
|
|
4
|
-
"additionalProperties": false,
|
|
5
|
-
"description": "该字段类型没有特殊配置。",
|
|
6
|
-
"type": "object"
|
|
7
|
-
},
|
|
8
|
-
"FieldCommon": {
|
|
9
|
-
"allOf": [
|
|
10
|
-
{
|
|
11
|
-
"if": {
|
|
12
|
-
"properties": {
|
|
13
|
-
"defaultType": {
|
|
14
|
-
"const": 2
|
|
15
|
-
}
|
|
16
|
-
},
|
|
17
|
-
"required": [
|
|
18
|
-
"defaultType"
|
|
19
|
-
]
|
|
20
|
-
},
|
|
21
|
-
"then": {
|
|
22
|
-
"properties": {
|
|
23
|
-
"defaultValue": {
|
|
24
|
-
"type": "null"
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
"if": {
|
|
31
|
-
"properties": {
|
|
32
|
-
"defaultType": {
|
|
33
|
-
"const": 3
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
"required": [
|
|
37
|
-
"defaultType"
|
|
38
|
-
]
|
|
39
|
-
},
|
|
40
|
-
"then": {
|
|
41
|
-
"properties": {
|
|
42
|
-
"defaultValue": {
|
|
43
|
-
"minLength": 1,
|
|
44
|
-
"type": [
|
|
45
|
-
"string",
|
|
46
|
-
"null"
|
|
47
|
-
]
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
],
|
|
53
|
-
"anyOf": [
|
|
54
|
-
{
|
|
55
|
-
"required": [
|
|
56
|
-
"queId"
|
|
57
|
-
]
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
"required": [
|
|
61
|
-
"name"
|
|
62
|
-
]
|
|
63
|
-
}
|
|
64
|
-
],
|
|
65
|
-
"description": "顶层业务字段的公共属性;字段类型由 TopField 的判别分支定义。",
|
|
66
|
-
"properties": {
|
|
67
|
-
"beingRequired": {
|
|
68
|
-
"description": "该字段是否必填;缺失时保留已有字段当前值。",
|
|
69
|
-
"type": "boolean"
|
|
70
|
-
},
|
|
71
|
-
"defaultType": {
|
|
72
|
-
"default": 1,
|
|
73
|
-
"description": "默认值类型,与 defaultValue 联动;1 表示静态值,2 表示关联/绑定值且 defaultValue 只能省略或为 null,3 表示公式值且提供 defaultValue 时必须为非空公式文本。省略 defaultType 但提供 defaultValue 时按 1 处理。",
|
|
74
|
-
"enum": [
|
|
75
|
-
1,
|
|
76
|
-
2,
|
|
77
|
-
3
|
|
78
|
-
],
|
|
79
|
-
"type": "integer"
|
|
80
|
-
},
|
|
81
|
-
"defaultValue": {
|
|
82
|
-
"$ref": "#/$defs/ScalarDefaultValue",
|
|
83
|
-
"description": "普通字段的默认值;后端 queDefaultValue 为字符串,因此数字等字面量也必须以字符串传入。defaultType 为 1 时表示静态文本,为 3 时表示公式文本,为 2 时不携带字面量默认值。表格、地址、附件、引用等结构化默认值必须放在具体字段 config.defaultValues。显式 null 表示清除默认值。"
|
|
84
|
-
},
|
|
85
|
-
"name": {
|
|
86
|
-
"description": "字段展示名称;新增字段必填,已有字段可在携带 queId 时省略;同父级允许同名(后端以 queId 为字段身份),配置引用建议携带 queId。",
|
|
87
|
-
"minLength": 1,
|
|
88
|
-
"type": "string"
|
|
89
|
-
},
|
|
90
|
-
"queId": {
|
|
91
|
-
"description": "已有字段的后端 ID;更新时用于精确定位并复用,新增字段通常省略。",
|
|
92
|
-
"minimum": 1,
|
|
93
|
-
"type": "integer"
|
|
94
|
-
},
|
|
95
|
-
"questionDescription": {
|
|
96
|
-
"description": "字段说明;null 或空字符串表示清空。",
|
|
97
|
-
"type": [
|
|
98
|
-
"string",
|
|
99
|
-
"null"
|
|
100
|
-
]
|
|
101
|
-
},
|
|
102
|
-
"questionType": {
|
|
103
|
-
"description": "字段类型;具体可用值由当前字段详情 Schema 的判别分支约束。",
|
|
104
|
-
"type": "string"
|
|
105
|
-
},
|
|
106
|
-
"width": {
|
|
107
|
-
"description": "字段在所在行的宽度百分比(1-100 整数);后端不校验总和与取值,存量数据存在 33/34 等三等分布值。",
|
|
108
|
-
"maximum": 100,
|
|
109
|
-
"minimum": 1,
|
|
110
|
-
"type": "integer"
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
"type": "object"
|
|
114
|
-
},
|
|
115
|
-
"ScalarDefaultValue": {
|
|
116
|
-
"description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。defaultType 为 3 时,通过 AppForm Schema 查询能力传入当前 schema_version 并设置 schema_kind=formula 获取公式语法、字段引用、运算符和全部函数说明;CLI 使用 builder app-form schema --schema-kind formula。需要配置结构化默认值时,传入当前 schema_version,并将字段的 questionType 作为 field_type 获取字段详细 Schema。",
|
|
117
|
-
"type": [
|
|
118
|
-
"null",
|
|
119
|
-
"string"
|
|
120
|
-
]
|
|
121
|
-
}
|
|
122
|
-
},
|
|
123
|
-
"$id": "https://builder.qingflow.com/schemas/app-form-apply/v1alpha1/fields/ai_content_extraction.json",
|
|
124
|
-
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
125
|
-
"allOf": [
|
|
126
|
-
{
|
|
127
|
-
"$ref": "#/$defs/FieldCommon"
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
"properties": {
|
|
131
|
-
"config": {
|
|
132
|
-
"$ref": "#/$defs/EmptyConfig"
|
|
133
|
-
},
|
|
134
|
-
"questionType": {
|
|
135
|
-
"const": "ai_content_extraction"
|
|
136
|
-
}
|
|
137
|
-
},
|
|
138
|
-
"required": [
|
|
139
|
-
"questionType"
|
|
140
|
-
]
|
|
141
|
-
}
|
|
142
|
-
],
|
|
143
|
-
"title": "Qingflow AppForm v1alpha1 ai_content_extraction field",
|
|
144
|
-
"type": "object",
|
|
145
|
-
"unevaluatedProperties": false
|
|
146
|
-
}
|