@qingflow-tech/qingflow-cli 1.1.42 → 1.1.43-beta.2
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 +5 -4
- package/package.json +2 -1
- package/pyproject.toml +3 -2
- package/skills/qingflow-app-form/SKILL.md +53 -0
- package/skills/qingflow-app-form/agents/openai.yaml +4 -0
- package/skills/qingflow-cli/SKILL.md +6 -6
- package/skills/qingflow-cli/manifest.yaml +1 -1
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md +4 -4
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +1 -1
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_FIELD_DATA_TYPES.md +1 -1
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md +4 -4
- package/skills/qingflow-cli/reference/builder/10-build-single-app.md +5 -5
- package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +14 -14
- package/skills/qingflow-cli/reference/builder/30-schema-fields.md +98 -101
- package/skills/qingflow-cli/reference/builder/40-layout.md +39 -31
- package/skills/qingflow-cli/reference/builder/README.md +9 -9
- package/skills/qingflow-cli/reference/builder/code-integrations/README.md +4 -2
- package/skills/qingflow-cli/reference/builder/code-integrations/code-block.md +2 -0
- package/skills/qingflow-cli/reference/builder/code-integrations/q-linker.md +2 -0
- package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +45 -38
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_EXPLORATION_REPORT.md +1 -1
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_FIELD_DATA_TYPES.md +1 -1
- package/skills/qingflow-cli/reference/examples/README.md +3 -3
- package/skills/qingflow-mcp-setup/SKILL.md +1 -1
- package/src/qingflow_mcp/app_form_compat.py +120 -0
- package/src/qingflow_mcp/app_form_validator.py +199 -0
- package/src/qingflow_mcp/builder_facade/app_form.py +3365 -0
- package/src/qingflow_mcp/builder_facade/app_form_schema.py +269 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +37 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +2909 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +337 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +181 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +373 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +242 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +557 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +95 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +95 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +290 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +135 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +135 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +135 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +429 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +159 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +181 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +144 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +357 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +382 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +159 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +159 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +1831 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +135 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +736 -0
- package/src/qingflow_mcp/builder_facade/service.py +5 -2
- package/src/qingflow_mcp/cli/commands/builder.py +91 -28
- package/src/qingflow_mcp/cli/json_io.py +21 -2
- package/src/qingflow_mcp/cli/main.py +3 -39
- package/src/qingflow_mcp/cloud_oauth_proxy.py +96 -3
- package/src/qingflow_mcp/public_surface.py +5 -2
- package/src/qingflow_mcp/response_trim.py +48 -0
- package/src/qingflow_mcp/server_app_builder.py +38 -86
- package/src/qingflow_mcp/server_cloud.py +66 -1
- package/src/qingflow_mcp/tools/ai_builder_tools.py +93 -100
- package/src/qingflow_mcp/tools/app_tools.py +46 -65
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from jsonschema import Draft202012Validator, FormatChecker
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
JSONObject = dict[str, Any]
|
|
14
|
+
SCHEMA_ROOT = Path(__file__).with_name("schemas") / "app_form"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AppFormContractError(ValueError):
|
|
18
|
+
def __init__(self, error_code: str, message: str, *, path: str | None = None, details: JSONObject | None = None) -> None:
|
|
19
|
+
super().__init__(message)
|
|
20
|
+
self.error_code = error_code
|
|
21
|
+
self.message = message
|
|
22
|
+
self.path = path
|
|
23
|
+
self.details = details or {}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True, slots=True)
|
|
27
|
+
class AppFormSchemaContext:
|
|
28
|
+
schema_version: str
|
|
29
|
+
api_version: str
|
|
30
|
+
complete_schema: JSONObject
|
|
31
|
+
supported_types: tuple[str, ...]
|
|
32
|
+
contract_source: str
|
|
33
|
+
contract_version: str
|
|
34
|
+
normalizer_version: str
|
|
35
|
+
apply_dto: type[AppFormApplyDTO]
|
|
36
|
+
|
|
37
|
+
def validate(self, payload: JSONObject) -> None:
|
|
38
|
+
validator = Draft202012Validator(self.complete_schema, format_checker=FormatChecker())
|
|
39
|
+
errors = sorted(validator.iter_errors(payload), key=lambda item: (list(item.absolute_path), item.message))
|
|
40
|
+
if not errors:
|
|
41
|
+
return
|
|
42
|
+
error = errors[0]
|
|
43
|
+
pointer = "".join(f"/{str(part).replace('~', '~0').replace('/', '~1')}" for part in error.absolute_path) or "/"
|
|
44
|
+
raise AppFormContractError(
|
|
45
|
+
"INVALID_SPEC",
|
|
46
|
+
error.message,
|
|
47
|
+
path=pointer,
|
|
48
|
+
details={"validator": error.validator, "schemaPath": "/".join(str(part) for part in error.absolute_schema_path)},
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def parse_apply(self, payload: JSONObject) -> AppFormApplyDTO:
|
|
52
|
+
self.validate(payload)
|
|
53
|
+
return self.apply_dto(
|
|
54
|
+
contract_source=self.contract_source,
|
|
55
|
+
contract_version=self.contract_version,
|
|
56
|
+
normalizer_version=self.normalizer_version,
|
|
57
|
+
resource=deepcopy(payload),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def assert_apply_dto(self, value: AppFormApplyDTO) -> None:
|
|
61
|
+
if not isinstance(value, self.apply_dto):
|
|
62
|
+
raise AppFormContractError(
|
|
63
|
+
"SCHEMA_VERSION_MISMATCH",
|
|
64
|
+
"AppForm declaration was parsed by a different DTO type",
|
|
65
|
+
details={"expected": self.apply_dto.__name__, "actual": type(value).__name__},
|
|
66
|
+
)
|
|
67
|
+
actual = (value.contract_source, value.contract_version, value.normalizer_version)
|
|
68
|
+
expected = (self.contract_source, self.contract_version, self.normalizer_version)
|
|
69
|
+
if actual != expected:
|
|
70
|
+
raise AppFormContractError(
|
|
71
|
+
"SCHEMA_VERSION_MISMATCH",
|
|
72
|
+
"AppForm declaration was parsed by a different contract version",
|
|
73
|
+
details={"expected": expected, "actual": actual},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True, slots=True)
|
|
78
|
+
class AppFormApplyDTO:
|
|
79
|
+
contract_source: str
|
|
80
|
+
contract_version: str
|
|
81
|
+
normalizer_version: str
|
|
82
|
+
resource: JSONObject
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True, slots=True)
|
|
86
|
+
class AppFormV1Alpha1ApplyDTO(AppFormApplyDTO):
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
APPLY_DTO_TYPES: dict[str, type[AppFormApplyDTO]] = {
|
|
91
|
+
"v1alpha1": AppFormV1Alpha1ApplyDTO,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class AppFormSchemaRegistry:
|
|
96
|
+
def __init__(self, root: Path = SCHEMA_ROOT, *, enabled_versions: tuple[str, ...] | None = None) -> None:
|
|
97
|
+
self.root = root
|
|
98
|
+
self.manifest = self._read_json(root / "manifest.json")
|
|
99
|
+
versions = self.manifest.get("versions")
|
|
100
|
+
if not isinstance(versions, dict) or not versions:
|
|
101
|
+
raise RuntimeError("AppForm schema manifest must define versions")
|
|
102
|
+
configured = enabled_versions or self._configured_versions()
|
|
103
|
+
self.stored_versions = tuple(str(version) for version in versions)
|
|
104
|
+
self.latest_version = str(self.manifest.get("latestVersion") or "")
|
|
105
|
+
self._stored_api_to_version: dict[str, str] = {}
|
|
106
|
+
for version, metadata in versions.items():
|
|
107
|
+
if not isinstance(metadata, dict):
|
|
108
|
+
raise RuntimeError(f"AppForm schema metadata must be an object: {version}")
|
|
109
|
+
api_version = str(metadata.get("apiVersion") or "")
|
|
110
|
+
if not api_version:
|
|
111
|
+
raise RuntimeError(f"AppForm schema version has no apiVersion: {version}")
|
|
112
|
+
if api_version in self._stored_api_to_version:
|
|
113
|
+
raise RuntimeError(f"duplicate AppForm apiVersion: {api_version}")
|
|
114
|
+
self._stored_api_to_version[api_version] = str(version)
|
|
115
|
+
defaults = tuple(
|
|
116
|
+
str(version)
|
|
117
|
+
for version, metadata in versions.items()
|
|
118
|
+
if isinstance(metadata, dict) and bool(metadata.get("enabledByDefault"))
|
|
119
|
+
)
|
|
120
|
+
self.enabled_versions = configured or defaults
|
|
121
|
+
if self.latest_version not in versions:
|
|
122
|
+
raise RuntimeError("AppForm latestVersion is not stored")
|
|
123
|
+
if self.latest_version not in self.enabled_versions:
|
|
124
|
+
raise RuntimeError("AppForm latestVersion must remain enabled")
|
|
125
|
+
self._contexts: dict[str, AppFormSchemaContext] = {}
|
|
126
|
+
self._api_to_version: dict[str, str] = {}
|
|
127
|
+
for version in self.enabled_versions:
|
|
128
|
+
metadata = versions.get(version)
|
|
129
|
+
if not isinstance(metadata, dict):
|
|
130
|
+
raise RuntimeError(f"enabled AppForm schema version is not stored: {version}")
|
|
131
|
+
api_version = str(metadata.get("apiVersion") or "")
|
|
132
|
+
complete = self._read_json(root / version / "complete.json")
|
|
133
|
+
supported = tuple(str(item) for item in metadata.get("fieldTypes") or ())
|
|
134
|
+
if complete.get("properties", {}).get("apiVersion", {}).get("const") != api_version:
|
|
135
|
+
raise RuntimeError(f"AppForm schema apiVersion mismatch for {version}")
|
|
136
|
+
schema_id = str(complete.get("$id") or "")
|
|
137
|
+
if version not in schema_id:
|
|
138
|
+
raise RuntimeError(f"AppForm schema $id mismatch for {version}")
|
|
139
|
+
Draft202012Validator.check_schema(complete)
|
|
140
|
+
self._validate_local_refs(complete, root / version / "complete.json")
|
|
141
|
+
summary = self._read_json(root / version / "summary.json")
|
|
142
|
+
if summary.get("properties", {}).get("apiVersion", {}).get("const") != api_version:
|
|
143
|
+
raise RuntimeError(f"AppForm summary apiVersion mismatch for {version}")
|
|
144
|
+
Draft202012Validator.check_schema(summary)
|
|
145
|
+
self._validate_local_refs(summary, root / version / "summary.json")
|
|
146
|
+
for field_type in supported:
|
|
147
|
+
field_path = root / version / "fields" / f"{field_type}.json"
|
|
148
|
+
field_schema = self._read_json(field_path)
|
|
149
|
+
Draft202012Validator.check_schema(field_schema)
|
|
150
|
+
self._validate_local_refs(field_schema, field_path)
|
|
151
|
+
context = AppFormSchemaContext(
|
|
152
|
+
version,
|
|
153
|
+
api_version,
|
|
154
|
+
complete,
|
|
155
|
+
supported,
|
|
156
|
+
str(metadata.get("contractSource") or "app_form"),
|
|
157
|
+
str(metadata.get("contractVersion") or version),
|
|
158
|
+
str(metadata.get("normalizerVersion") or version),
|
|
159
|
+
self._apply_dto_type(version, metadata),
|
|
160
|
+
)
|
|
161
|
+
self._contexts[version] = context
|
|
162
|
+
self._api_to_version[api_version] = version
|
|
163
|
+
|
|
164
|
+
def schema(self, *, schema_version: str | None = None, field_type: str | None = None) -> JSONObject:
|
|
165
|
+
context = self.resolve_schema_version(schema_version)
|
|
166
|
+
if field_type is None:
|
|
167
|
+
schema = self._read_json(self.root / context.schema_version / "summary.json")
|
|
168
|
+
schema_kind = "summary"
|
|
169
|
+
else:
|
|
170
|
+
if field_type not in context.supported_types:
|
|
171
|
+
raise AppFormContractError(
|
|
172
|
+
"UNSUPPORTED_SCHEMA_FIELD_TYPE",
|
|
173
|
+
f"unsupported AppForm field type: {field_type}",
|
|
174
|
+
details={"supportedTypes": list(context.supported_types)},
|
|
175
|
+
)
|
|
176
|
+
schema = self._read_json(self.root / context.schema_version / "fields" / f"{field_type}.json")
|
|
177
|
+
schema_kind = "field"
|
|
178
|
+
return {
|
|
179
|
+
"schemaVersion": context.schema_version,
|
|
180
|
+
"apiVersion": context.api_version,
|
|
181
|
+
"latestVersion": self.latest_version,
|
|
182
|
+
"storedVersions": list(self.stored_versions),
|
|
183
|
+
"enabledVersions": list(self.enabled_versions),
|
|
184
|
+
"schemaKind": schema_kind,
|
|
185
|
+
"fieldType": field_type,
|
|
186
|
+
"schema": schema,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
def resolve_schema_version(self, schema_version: str | None) -> AppFormSchemaContext:
|
|
190
|
+
version = str(schema_version or self.latest_version).strip()
|
|
191
|
+
if version not in self.stored_versions:
|
|
192
|
+
raise self._version_error("UNSUPPORTED_SCHEMA_VERSION", version)
|
|
193
|
+
if version not in self.enabled_versions:
|
|
194
|
+
raise self._version_error("SCHEMA_VERSION_DISABLED", version)
|
|
195
|
+
return self._resolved_context(version)
|
|
196
|
+
|
|
197
|
+
def resolve_api_version(self, api_version: str) -> AppFormSchemaContext:
|
|
198
|
+
version = self._api_to_version.get(api_version)
|
|
199
|
+
if version is not None:
|
|
200
|
+
return self._resolved_context(version)
|
|
201
|
+
stored = self._stored_api_to_version.get(api_version)
|
|
202
|
+
if stored is not None:
|
|
203
|
+
raise self._version_error("SCHEMA_VERSION_DISABLED", stored)
|
|
204
|
+
raise self._version_error("UNSUPPORTED_SCHEMA_VERSION", api_version)
|
|
205
|
+
|
|
206
|
+
def _resolved_context(self, version: str) -> AppFormSchemaContext:
|
|
207
|
+
return self._contexts[version]
|
|
208
|
+
|
|
209
|
+
@staticmethod
|
|
210
|
+
def _apply_dto_type(version: str, metadata: JSONObject) -> type[AppFormApplyDTO]:
|
|
211
|
+
dto_name = str(metadata.get("applyDto") or version)
|
|
212
|
+
dto_type = APPLY_DTO_TYPES.get(dto_name)
|
|
213
|
+
if dto_type is None:
|
|
214
|
+
raise RuntimeError(f"AppForm schema version {version} references unknown apply DTO: {dto_name}")
|
|
215
|
+
return dto_type
|
|
216
|
+
|
|
217
|
+
def _version_error(self, error_code: str, requested: str) -> AppFormContractError:
|
|
218
|
+
return AppFormContractError(
|
|
219
|
+
error_code,
|
|
220
|
+
f"unsupported AppForm schema selection: {requested}",
|
|
221
|
+
details={
|
|
222
|
+
"requestedVersion": requested,
|
|
223
|
+
"latestVersion": self.latest_version,
|
|
224
|
+
"storedVersions": list(self.stored_versions),
|
|
225
|
+
"enabledVersions": list(self.enabled_versions),
|
|
226
|
+
},
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
@staticmethod
|
|
230
|
+
def _read_json(path: Path) -> JSONObject:
|
|
231
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
232
|
+
if not isinstance(value, dict):
|
|
233
|
+
raise RuntimeError(f"AppForm schema artifact must be an object: {path}")
|
|
234
|
+
return value
|
|
235
|
+
|
|
236
|
+
@staticmethod
|
|
237
|
+
def _validate_local_refs(document: JSONObject, path: Path) -> None:
|
|
238
|
+
stack: list[Any] = [document]
|
|
239
|
+
while stack:
|
|
240
|
+
value = stack.pop()
|
|
241
|
+
if isinstance(value, dict):
|
|
242
|
+
ref = value.get("$ref")
|
|
243
|
+
if ref is not None:
|
|
244
|
+
if not isinstance(ref, str) or (ref != "#" and not ref.startswith("#/")):
|
|
245
|
+
raise RuntimeError(f"AppForm schema contains an external or invalid $ref in {path}: {ref}")
|
|
246
|
+
target: Any = document
|
|
247
|
+
for raw_part in ref[2:].split("/") if ref.startswith("#/") else ():
|
|
248
|
+
part = raw_part.replace("~1", "/").replace("~0", "~")
|
|
249
|
+
if not isinstance(target, dict) or part not in target:
|
|
250
|
+
raise RuntimeError(f"AppForm schema contains a dangling $ref in {path}: {ref}")
|
|
251
|
+
target = target[part]
|
|
252
|
+
stack.extend(value.values())
|
|
253
|
+
elif isinstance(value, list):
|
|
254
|
+
stack.extend(value)
|
|
255
|
+
|
|
256
|
+
@staticmethod
|
|
257
|
+
def _configured_versions() -> tuple[str, ...]:
|
|
258
|
+
raw = os.environ.get("QINGFLOW_APP_FORM_SCHEMA_VERSIONS", "")
|
|
259
|
+
return tuple(part.strip() for part in raw.split(",") if part.strip())
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
_REGISTRY: AppFormSchemaRegistry | None = None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def app_form_schema_registry() -> AppFormSchemaRegistry:
|
|
266
|
+
global _REGISTRY
|
|
267
|
+
if _REGISTRY is None:
|
|
268
|
+
_REGISTRY = AppFormSchemaRegistry()
|
|
269
|
+
return _REGISTRY
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"latestVersion": "v1alpha1",
|
|
3
|
+
"versions": {
|
|
4
|
+
"v1alpha1": {
|
|
5
|
+
"apiVersion": "builder.qingflow.com/v1alpha1",
|
|
6
|
+
"applyDto": "v1alpha1",
|
|
7
|
+
"contractSource": "app_form",
|
|
8
|
+
"contractVersion": "v1alpha1",
|
|
9
|
+
"enabledByDefault": true,
|
|
10
|
+
"fieldTypes": [
|
|
11
|
+
"text",
|
|
12
|
+
"long_text",
|
|
13
|
+
"number",
|
|
14
|
+
"amount",
|
|
15
|
+
"link",
|
|
16
|
+
"date",
|
|
17
|
+
"datetime",
|
|
18
|
+
"member",
|
|
19
|
+
"department",
|
|
20
|
+
"single_choice",
|
|
21
|
+
"single_select",
|
|
22
|
+
"multi_select",
|
|
23
|
+
"phone",
|
|
24
|
+
"email",
|
|
25
|
+
"address",
|
|
26
|
+
"attachment",
|
|
27
|
+
"q_linker",
|
|
28
|
+
"code_block",
|
|
29
|
+
"data_relation",
|
|
30
|
+
"relation",
|
|
31
|
+
"subtable"
|
|
32
|
+
],
|
|
33
|
+
"normalizerVersion": "v1alpha1",
|
|
34
|
+
"status": "active"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|