@qingflow-tech/qingflow-cli 1.1.61-beta.4 → 1.1.61-beta.6
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 +2 -1
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_CHARTS_WORKFLOW.md +1 -1
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_PORTAL_WORKFLOW.md +6 -258
- package/skills/qingflow-cli/reference/builder/30-schema-fields.md +13 -0
- package/skills/qingflow-cli/reference/builder/70-portal.md +38 -17
- package/skills/qingflow-cli/reference/examples/portal/portal_apply_v1alpha1.example.json +100 -0
- package/skills/qingflow-cli/reference/examples/portal/portal_sections_all_types.example.json +6 -7
- package/src/qingflow_mcp/app_form_validator.py +20 -3
- package/src/qingflow_mcp/builder_facade/app_form.py +137 -17
- package/src/qingflow_mcp/builder_facade/models.py +177 -15
- package/src/qingflow_mcp/builder_facade/portals_schema.py +397 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +14 -1
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +49 -9
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +27 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_content_classification.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_content_extraction.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_content_generation.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_document_parsing.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_image_generation.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_image_recognition.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_machine_translation.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_text_summary.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +2 -2
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +27 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date_duration.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/embed_field.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/image_select.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/location.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +2 -2
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ocr.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +2 -2
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +27 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +29 -2
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +20 -7
- package/src/qingflow_mcp/builder_facade/schemas/portals/manifest.json +59 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/complete.json +1397 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/carousel.json +193 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/chart.json +643 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/favourite.json +203 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/filter.json +268 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/grid.json +265 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/link.json +131 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/task.json +199 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/text.json +123 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/view.json +196 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/summary.json +800 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/complete.json +1 -1
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/display.json +1 -1
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/summary.json +1 -1
- package/src/qingflow_mcp/builder_facade/service.py +1251 -88
- package/src/qingflow_mcp/cli/commands/builder.py +132 -5
- package/src/qingflow_mcp/public_surface.py +2 -0
- package/src/qingflow_mcp/server_app_builder.py +34 -3
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +73 -0
- package/src/qingflow_mcp/solution/executor.py +5 -2
- package/src/qingflow_mcp/tools/ai_builder_tools.py +118 -12
- package/src/qingflow_mcp/tools/portal_tools.py +4 -54
- package/src/qingflow_mcp/tools/portal_writer.py +59 -0
- package/src/qingflow_mcp/tools/solution_tools.py +2 -0
|
@@ -0,0 +1,397 @@
|
|
|
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
|
|
@@ -29,7 +29,20 @@
|
|
|
29
29
|
"code_block",
|
|
30
30
|
"data_relation",
|
|
31
31
|
"relation",
|
|
32
|
-
"subtable"
|
|
32
|
+
"subtable",
|
|
33
|
+
"date_duration",
|
|
34
|
+
"image_select",
|
|
35
|
+
"location",
|
|
36
|
+
"ocr",
|
|
37
|
+
"embed_field",
|
|
38
|
+
"ai_content_generation",
|
|
39
|
+
"ai_content_extraction",
|
|
40
|
+
"ai_content_classification",
|
|
41
|
+
"ai_machine_translation",
|
|
42
|
+
"ai_image_recognition",
|
|
43
|
+
"ai_image_generation",
|
|
44
|
+
"ai_document_parsing",
|
|
45
|
+
"ai_text_summary"
|
|
33
46
|
],
|
|
34
47
|
"normalizerVersion": "v1alpha1",
|
|
35
48
|
"schemaKinds": [
|
|
@@ -910,6 +910,12 @@
|
|
|
910
910
|
"null"
|
|
911
911
|
]
|
|
912
912
|
},
|
|
913
|
+
"auditValueDescription": {
|
|
914
|
+
"type": [
|
|
915
|
+
"string",
|
|
916
|
+
"null"
|
|
917
|
+
]
|
|
918
|
+
},
|
|
913
919
|
"beingCanDecrypted": {
|
|
914
920
|
"type": [
|
|
915
921
|
"boolean",
|
|
@@ -940,6 +946,12 @@
|
|
|
940
946
|
"null"
|
|
941
947
|
]
|
|
942
948
|
},
|
|
949
|
+
"dataId": {
|
|
950
|
+
"type": [
|
|
951
|
+
"integer",
|
|
952
|
+
"null"
|
|
953
|
+
]
|
|
954
|
+
},
|
|
943
955
|
"dateType": {
|
|
944
956
|
"type": [
|
|
945
957
|
"integer",
|
|
@@ -958,6 +970,15 @@
|
|
|
958
970
|
"null"
|
|
959
971
|
]
|
|
960
972
|
},
|
|
973
|
+
"previousTableRowOrdinalList": {
|
|
974
|
+
"items": {
|
|
975
|
+
"type": "integer"
|
|
976
|
+
},
|
|
977
|
+
"type": [
|
|
978
|
+
"array",
|
|
979
|
+
"null"
|
|
980
|
+
]
|
|
981
|
+
},
|
|
961
982
|
"queId": {
|
|
962
983
|
"type": [
|
|
963
984
|
"integer",
|
|
@@ -976,6 +997,12 @@
|
|
|
976
997
|
"null"
|
|
977
998
|
]
|
|
978
999
|
},
|
|
1000
|
+
"questionOrdinal": {
|
|
1001
|
+
"type": [
|
|
1002
|
+
"integer",
|
|
1003
|
+
"null"
|
|
1004
|
+
]
|
|
1005
|
+
},
|
|
979
1006
|
"referValues": {
|
|
980
1007
|
"items": {
|
|
981
1008
|
"$ref": "#/$defs/ReferenceDefaultValue"
|
|
@@ -1309,11 +1336,11 @@
|
|
|
1309
1336
|
"description": "段落内部字段行。",
|
|
1310
1337
|
"properties": {
|
|
1311
1338
|
"fields": {
|
|
1312
|
-
"description": "本行的 1 到
|
|
1339
|
+
"description": "本行的 1 到 12 个顶层字段(后端无每行字段数上限,按 queWidth 渲染;上限仅约束声明输入)。",
|
|
1313
1340
|
"items": {
|
|
1314
1341
|
"$ref": "#/$defs/TopField"
|
|
1315
1342
|
},
|
|
1316
|
-
"maxItems":
|
|
1343
|
+
"maxItems": 12,
|
|
1317
1344
|
"minItems": 1,
|
|
1318
1345
|
"type": "array"
|
|
1319
1346
|
}
|
|
@@ -1626,8 +1653,8 @@
|
|
|
1626
1653
|
]
|
|
1627
1654
|
},
|
|
1628
1655
|
"numberFormat": {
|
|
1629
|
-
"description": "数字展示格式 ID。",
|
|
1630
|
-
"minimum":
|
|
1656
|
+
"description": "数字展示格式 ID(1 默认/2 千分位/3 百分比;0 为历史存量值——后端写链无值域校验,原样落库回读)。",
|
|
1657
|
+
"minimum": 0,
|
|
1631
1658
|
"type": [
|
|
1632
1659
|
"integer",
|
|
1633
1660
|
"null"
|
|
@@ -1918,8 +1945,8 @@
|
|
|
1918
1945
|
]
|
|
1919
1946
|
},
|
|
1920
1947
|
"url": {
|
|
1921
|
-
"description": "请求 URL;动态嵌入字段值时使用 {{key}},并在 inputs 中提供同名 source=json_path
|
|
1922
|
-
"minLength":
|
|
1948
|
+
"description": "请求 URL;动态嵌入字段值时使用 {{key}},并在 inputs 中提供同名 source=json_path 映射。自定义模式(configMode=1)后端要求非空;插件模板/连接中心/轻翼智能体模式的 URL 由模板侧提供,存量声明可为空字符串(写回按读面形态透传)。",
|
|
1949
|
+
"minLength": 0,
|
|
1923
1950
|
"type": "string"
|
|
1924
1951
|
},
|
|
1925
1952
|
"urlEncodedValue": {
|
|
@@ -2093,11 +2120,11 @@
|
|
|
2093
2120
|
"description": "根级普通字段行;字段顺序即展示顺序。",
|
|
2094
2121
|
"properties": {
|
|
2095
2122
|
"fields": {
|
|
2096
|
-
"description": "本行的 1 到
|
|
2123
|
+
"description": "本行的 1 到 12 个顶层字段(后端无每行字段数上限,按 queWidth 渲染;上限仅约束声明输入)。",
|
|
2097
2124
|
"items": {
|
|
2098
2125
|
"$ref": "#/$defs/TopField"
|
|
2099
2126
|
},
|
|
2100
|
-
"maxItems":
|
|
2127
|
+
"maxItems": 12,
|
|
2101
2128
|
"minItems": 1,
|
|
2102
2129
|
"type": "array"
|
|
2103
2130
|
},
|
|
@@ -2736,7 +2763,20 @@
|
|
|
2736
2763
|
"enum": [
|
|
2737
2764
|
"date",
|
|
2738
2765
|
"datetime",
|
|
2739
|
-
"rich_text"
|
|
2766
|
+
"rich_text",
|
|
2767
|
+
"date_duration",
|
|
2768
|
+
"image_select",
|
|
2769
|
+
"location",
|
|
2770
|
+
"ocr",
|
|
2771
|
+
"embed_field",
|
|
2772
|
+
"ai_content_generation",
|
|
2773
|
+
"ai_content_extraction",
|
|
2774
|
+
"ai_content_classification",
|
|
2775
|
+
"ai_machine_translation",
|
|
2776
|
+
"ai_image_recognition",
|
|
2777
|
+
"ai_image_generation",
|
|
2778
|
+
"ai_document_parsing",
|
|
2779
|
+
"ai_text_summary"
|
|
2740
2780
|
]
|
|
2741
2781
|
}
|
|
2742
2782
|
},
|
|
@@ -118,6 +118,12 @@
|
|
|
118
118
|
"null"
|
|
119
119
|
]
|
|
120
120
|
},
|
|
121
|
+
"auditValueDescription": {
|
|
122
|
+
"type": [
|
|
123
|
+
"string",
|
|
124
|
+
"null"
|
|
125
|
+
]
|
|
126
|
+
},
|
|
121
127
|
"beingCanDecrypted": {
|
|
122
128
|
"type": [
|
|
123
129
|
"boolean",
|
|
@@ -148,6 +154,12 @@
|
|
|
148
154
|
"null"
|
|
149
155
|
]
|
|
150
156
|
},
|
|
157
|
+
"dataId": {
|
|
158
|
+
"type": [
|
|
159
|
+
"integer",
|
|
160
|
+
"null"
|
|
161
|
+
]
|
|
162
|
+
},
|
|
151
163
|
"dateType": {
|
|
152
164
|
"type": [
|
|
153
165
|
"integer",
|
|
@@ -166,6 +178,15 @@
|
|
|
166
178
|
"null"
|
|
167
179
|
]
|
|
168
180
|
},
|
|
181
|
+
"previousTableRowOrdinalList": {
|
|
182
|
+
"items": {
|
|
183
|
+
"type": "integer"
|
|
184
|
+
},
|
|
185
|
+
"type": [
|
|
186
|
+
"array",
|
|
187
|
+
"null"
|
|
188
|
+
]
|
|
189
|
+
},
|
|
169
190
|
"queId": {
|
|
170
191
|
"type": [
|
|
171
192
|
"integer",
|
|
@@ -184,6 +205,12 @@
|
|
|
184
205
|
"null"
|
|
185
206
|
]
|
|
186
207
|
},
|
|
208
|
+
"questionOrdinal": {
|
|
209
|
+
"type": [
|
|
210
|
+
"integer",
|
|
211
|
+
"null"
|
|
212
|
+
]
|
|
213
|
+
},
|
|
187
214
|
"referValues": {
|
|
188
215
|
"items": {
|
|
189
216
|
"$ref": "#/$defs/ReferenceDefaultValue"
|