@qingflow-tech/qingflow-cli 1.1.41 → 1.1.43-beta.1

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 (51) hide show
  1. package/README.md +4 -4
  2. package/package.json +2 -1
  3. package/pyproject.toml +3 -2
  4. package/skills/qingflow-cli/reference/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md +1 -1
  5. package/skills/qingflow-cli/reference/task/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md +1 -1
  6. package/src/qingflow_mcp/app_form_compat.py +115 -0
  7. package/src/qingflow_mcp/app_form_validator.py +199 -0
  8. package/src/qingflow_mcp/builder_facade/app_form.py +2836 -0
  9. package/src/qingflow_mcp/builder_facade/app_form_schema.py +269 -0
  10. package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +37 -0
  11. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +1766 -0
  12. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +76 -0
  13. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +76 -0
  14. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +76 -0
  15. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +223 -0
  16. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +538 -0
  17. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +76 -0
  18. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +76 -0
  19. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +149 -0
  20. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +76 -0
  21. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +76 -0
  22. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +76 -0
  23. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +76 -0
  24. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +92 -0
  25. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +76 -0
  26. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +76 -0
  27. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +338 -0
  28. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +137 -0
  29. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +92 -0
  30. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +92 -0
  31. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +810 -0
  32. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +76 -0
  33. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +713 -0
  34. package/src/qingflow_mcp/builder_facade/service.py +5 -2
  35. package/src/qingflow_mcp/cli/commands/builder.py +91 -28
  36. package/src/qingflow_mcp/cli/json_io.py +21 -2
  37. package/src/qingflow_mcp/cli/main.py +3 -39
  38. package/src/qingflow_mcp/cloud_auth.py +11 -0
  39. package/src/qingflow_mcp/cloud_context.py +5 -0
  40. package/src/qingflow_mcp/cloud_oauth_proxy.py +453 -0
  41. package/src/qingflow_mcp/cloud_redis.py +25 -0
  42. package/src/qingflow_mcp/config.py +44 -0
  43. package/src/qingflow_mcp/public_surface.py +5 -2
  44. package/src/qingflow_mcp/response_trim.py +8 -0
  45. package/src/qingflow_mcp/server_app_builder.py +38 -86
  46. package/src/qingflow_mcp/server_app_user.py +2 -1
  47. package/src/qingflow_mcp/server_cloud.py +245 -22
  48. package/src/qingflow_mcp/tools/ai_builder_tools.py +93 -100
  49. package/src/qingflow_mcp/tools/app_tools.py +46 -65
  50. package/src/qingflow_mcp/tools/base.py +2 -1
  51. package/src/qingflow_mcp/tools/task_context_tools.py +6 -0
@@ -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
+ }