@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
|
@@ -9191,6 +9191,7 @@ class AiBuilderFacade:
|
|
|
9191
9191
|
remove_fields: list[FieldRemovePatch],
|
|
9192
9192
|
inline_form_layout_sections: list[LayoutSectionPatch] | None = None,
|
|
9193
9193
|
) -> JSONObject:
|
|
9194
|
+
# TODO(deprecation): Remove after all internal callers use the AppForm reconciler.
|
|
9194
9195
|
apply_started_at = time.perf_counter()
|
|
9195
9196
|
schema_write_ms: int | None = None
|
|
9196
9197
|
inline_layout_ms: int | None = None
|
|
@@ -10182,6 +10183,7 @@ class AiBuilderFacade:
|
|
|
10182
10183
|
sections: list[LayoutSectionPatch],
|
|
10183
10184
|
publish: bool = True,
|
|
10184
10185
|
) -> JSONObject:
|
|
10186
|
+
# TODO(deprecation): Remove after all internal callers use the AppForm reconciler.
|
|
10185
10187
|
requested_sections = [section.model_dump(mode="json", exclude_none=True) for section in sections]
|
|
10186
10188
|
normalized_args = {
|
|
10187
10189
|
"app_key": app_key,
|
|
@@ -22236,7 +22238,7 @@ def _compile_code_block_binding_fields(
|
|
|
22236
22238
|
alias_name = str(output_item.get("alias") or "").strip()
|
|
22237
22239
|
alias_path = str(output_item.get("path") or "").strip()
|
|
22238
22240
|
existing_alias = existing_alias_by_name.get(alias_name) or {}
|
|
22239
|
-
alias_id = _coerce_positive_int(existing_alias.get("alias_id"))
|
|
22241
|
+
alias_id = _coerce_positive_int(output_item.get("alias_id")) or _coerce_positive_int(existing_alias.get("alias_id"))
|
|
22240
22242
|
compiled_outputs.append(
|
|
22241
22243
|
{
|
|
22242
22244
|
"alias_name": alias_name,
|
|
@@ -22649,7 +22651,8 @@ def _compile_q_linker_binding_fields(
|
|
|
22649
22651
|
{
|
|
22650
22652
|
"alias_name": alias_name,
|
|
22651
22653
|
"alias_path": alias_path,
|
|
22652
|
-
"alias_id": _coerce_positive_int(
|
|
22654
|
+
"alias_id": _coerce_positive_int(output_item.get("alias_id"))
|
|
22655
|
+
or _coerce_positive_int((existing_alias_by_name.get(alias_name) or {}).get("alias_id")),
|
|
22653
22656
|
"target_field": {
|
|
22654
22657
|
"field_id": target_field.get("field_id"),
|
|
22655
22658
|
"que_id": target_ref,
|
|
@@ -4,6 +4,7 @@ import argparse
|
|
|
4
4
|
from copy import deepcopy
|
|
5
5
|
|
|
6
6
|
from ..context import CliContext
|
|
7
|
+
from ...app_form_validator import AppFormValidationError, validate_declaration
|
|
7
8
|
from ..json_io import load_json_value
|
|
8
9
|
from .common import load_list_arg, load_object_arg, parse_bool_text, raise_config_error, require_list_arg
|
|
9
10
|
|
|
@@ -98,6 +99,29 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
98
99
|
package_apply.add_argument("--config-file", required=True)
|
|
99
100
|
package_apply.set_defaults(handler=_handle_package_apply, format_hint="builder_summary", force_json_output=True)
|
|
100
101
|
|
|
102
|
+
app_form = builder_subparsers.add_parser("app-form", help="声明式应用表单")
|
|
103
|
+
app_form_subparsers = app_form.add_subparsers(dest="builder_app_form_command", required=True)
|
|
104
|
+
|
|
105
|
+
app_form_schema = app_form_subparsers.add_parser("schema", help="读取 AppForm Schema")
|
|
106
|
+
app_form_schema.add_argument("--schema-version")
|
|
107
|
+
app_form_schema.add_argument("--field-type")
|
|
108
|
+
app_form_schema.set_defaults(handler=_handle_app_form_schema, format_hint="builder_summary", force_json_output=True)
|
|
109
|
+
|
|
110
|
+
app_form_get = app_form_subparsers.add_parser("get", help="读取 canonical AppForm")
|
|
111
|
+
app_form_get.add_argument("--app-key", required=True)
|
|
112
|
+
app_form_get.add_argument("--schema-version")
|
|
113
|
+
app_form_get.add_argument("--being-draft", action=argparse.BooleanOptionalAction, default=False)
|
|
114
|
+
app_form_get.set_defaults(handler=_handle_app_form_get, format_hint="builder_summary", force_json_output=True)
|
|
115
|
+
|
|
116
|
+
app_form_apply = app_form_subparsers.add_parser("apply", help="应用完整 AppForm 声明")
|
|
117
|
+
app_form_apply.add_argument("--file", required=True, help="AppForm JSON 文件,- 表示 stdin")
|
|
118
|
+
app_form_apply.set_defaults(handler=_handle_app_form_apply, format_hint="builder_summary", force_json_output=True)
|
|
119
|
+
|
|
120
|
+
app_form_validate = app_form_subparsers.add_parser("validate", help="只校验 AppForm 声明,不访问后端")
|
|
121
|
+
app_form_validate.add_argument("--file", required=True, help="AppForm JSON 文件,- 表示 stdin")
|
|
122
|
+
app_form_validate.add_argument("--schema-version", help="固定使用的 AppForm Schema 版本")
|
|
123
|
+
app_form_validate.set_defaults(handler=_handle_app_form_validate, format_hint="builder_summary", force_json_output=True)
|
|
124
|
+
|
|
101
125
|
app = builder_subparsers.add_parser("app", help="应用")
|
|
102
126
|
app_subparsers = app.add_subparsers(dest="builder_app_command", required=True)
|
|
103
127
|
|
|
@@ -113,6 +137,10 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
113
137
|
app_release_lock.add_argument("--lock-owner-name", required=True)
|
|
114
138
|
app_release_lock.set_defaults(handler=_handle_app_release_edit_lock_if_mine, format_hint="builder_summary", force_json_output=True)
|
|
115
139
|
|
|
140
|
+
app_delete = app_subparsers.add_parser("delete", help="删除应用")
|
|
141
|
+
app_delete.add_argument("--app-key", required=True)
|
|
142
|
+
app_delete.set_defaults(handler=_handle_app_delete, format_hint="builder_summary", force_json_output=True)
|
|
143
|
+
|
|
116
144
|
app_get = app_subparsers.add_parser("get", help="读取应用配置(字段请使用: builder app get --app-key APP fields)")
|
|
117
145
|
app_get.add_argument(
|
|
118
146
|
"builder_app_get_section",
|
|
@@ -202,34 +230,6 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
202
230
|
portal_delete.add_argument("--dash-key", required=True)
|
|
203
231
|
portal_delete.set_defaults(handler=_handle_portal_delete, format_hint="builder_summary", force_json_output=True)
|
|
204
232
|
|
|
205
|
-
schema_apply = builder_subparsers.add_parser("schema", help="字段搭建")
|
|
206
|
-
schema_apply_subparsers = schema_apply.add_subparsers(dest="builder_schema_command", required=True)
|
|
207
|
-
schema_apply_apply = schema_apply_subparsers.add_parser("apply", help="执行字段变更")
|
|
208
|
-
schema_apply_apply.add_argument("--app-key", default="")
|
|
209
|
-
schema_apply_apply.add_argument("--package-id", type=int)
|
|
210
|
-
schema_apply_apply.add_argument("--app-name", default="")
|
|
211
|
-
schema_apply_apply.add_argument("--app-title", default="")
|
|
212
|
-
schema_apply_apply.add_argument("--icon")
|
|
213
|
-
schema_apply_apply.add_argument("--color")
|
|
214
|
-
schema_apply_apply.add_argument("--visibility-file")
|
|
215
|
-
schema_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=None)
|
|
216
|
-
schema_apply_apply.add_argument("--apps-file", help="多应用 schema JSON 对象;每项可带 client_key/app_name/form,支持 relation target_app_ref")
|
|
217
|
-
schema_apply_apply.add_argument("--form-file", help="单应用表单结构 JSON 数组;form[].rows[][] 同时声明字段和表单分组布局")
|
|
218
|
-
schema_apply_apply.add_argument("--add-fields-file", help="字段 JSON 数组;字段可用 as_data_title/as_data_cover 标记数据标题/封面")
|
|
219
|
-
schema_apply_apply.add_argument("--update-fields-file", help="字段更新 JSON 数组;set 内可用 as_data_title/as_data_cover 标记数据标题/封面")
|
|
220
|
-
schema_apply_apply.add_argument("--remove-fields-file")
|
|
221
|
-
schema_apply_apply.set_defaults(handler=_handle_schema_apply, format_hint="builder_summary", force_json_output=True)
|
|
222
|
-
|
|
223
|
-
layout_apply = builder_subparsers.add_parser("layout", help="布局")
|
|
224
|
-
layout_apply_subparsers = layout_apply.add_subparsers(dest="builder_layout_command", required=True)
|
|
225
|
-
layout_apply_apply = layout_apply_subparsers.add_parser("apply", help="执行布局变更")
|
|
226
|
-
layout_apply_apply.add_argument("--app-key", default="")
|
|
227
|
-
layout_apply_apply.add_argument("--apps-file", help="多应用布局 JSON 数组;每项包含 app_key/mode?/publish?/sections")
|
|
228
|
-
layout_apply_apply.add_argument("--mode", choices=["merge", "replace"], default="merge")
|
|
229
|
-
layout_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
|
|
230
|
-
layout_apply_apply.add_argument("--sections-file")
|
|
231
|
-
layout_apply_apply.set_defaults(handler=_handle_layout_apply, format_hint="builder_summary", force_json_output=True)
|
|
232
|
-
|
|
233
233
|
views_apply = builder_subparsers.add_parser("views", help="视图")
|
|
234
234
|
views_apply_subparsers = views_apply.add_subparsers(dest="builder_views_command", required=True)
|
|
235
235
|
views_apply_apply = views_apply_subparsers.add_parser("apply", help="执行视图变更")
|
|
@@ -331,6 +331,67 @@ def _handle_contract(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
331
331
|
return context.builder.builder_tool_contract(tool_name=args.tool_name)
|
|
332
332
|
|
|
333
333
|
|
|
334
|
+
def _handle_app_form_schema(args: argparse.Namespace, context: CliContext) -> dict:
|
|
335
|
+
return context.builder.app_form_schema(
|
|
336
|
+
schema_version=args.schema_version,
|
|
337
|
+
field_type=args.field_type,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _handle_app_form_get(args: argparse.Namespace, context: CliContext) -> dict:
|
|
342
|
+
return context.builder.app_form_get(
|
|
343
|
+
profile=args.profile,
|
|
344
|
+
app_key=args.app_key,
|
|
345
|
+
schema_version=args.schema_version,
|
|
346
|
+
being_draft=bool(args.being_draft),
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _handle_app_form_apply(args: argparse.Namespace, context: CliContext) -> dict:
|
|
351
|
+
payload = load_json_value(
|
|
352
|
+
args.file,
|
|
353
|
+
option_name="--file",
|
|
354
|
+
max_bytes=2 * 1024 * 1024,
|
|
355
|
+
max_depth=64,
|
|
356
|
+
)
|
|
357
|
+
if not isinstance(payload, dict):
|
|
358
|
+
raise_config_error("builder app-form apply --file must contain a JSON object")
|
|
359
|
+
return context.builder.app_form_apply(profile=args.profile, spec=payload)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _handle_app_form_validate(args: argparse.Namespace, _context: CliContext) -> dict:
|
|
363
|
+
payload = load_json_value(
|
|
364
|
+
args.file,
|
|
365
|
+
option_name="--file",
|
|
366
|
+
max_bytes=2 * 1024 * 1024,
|
|
367
|
+
max_depth=64,
|
|
368
|
+
)
|
|
369
|
+
if not isinstance(payload, dict):
|
|
370
|
+
raise_config_error("builder app-form validate --file must contain a JSON object")
|
|
371
|
+
try:
|
|
372
|
+
result = validate_declaration(payload, schema_version=args.schema_version)
|
|
373
|
+
except AppFormValidationError as error:
|
|
374
|
+
return {
|
|
375
|
+
"category": "validation",
|
|
376
|
+
"status": "failed",
|
|
377
|
+
"ok": False,
|
|
378
|
+
"error_code": "INVALID_SPEC",
|
|
379
|
+
"message": "AppForm declaration failed validation",
|
|
380
|
+
"details": {
|
|
381
|
+
"schemaVersion": error.schema_version,
|
|
382
|
+
"apiVersion": error.api_version,
|
|
383
|
+
"issues": error.issues,
|
|
384
|
+
},
|
|
385
|
+
}
|
|
386
|
+
except (ValueError, RuntimeError) as error:
|
|
387
|
+
raise_config_error(str(error), error_code="VALIDATION_ERROR")
|
|
388
|
+
return {"category": "validation", "status": "success", "ok": True, **result, "message": "AppForm declaration is valid"}
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _handle_app_delete(args: argparse.Namespace, context: CliContext) -> dict:
|
|
392
|
+
return context.builder.app_delete(profile=args.profile, app_key=args.app_key)
|
|
393
|
+
|
|
394
|
+
|
|
334
395
|
def _handle_icon_catalog(args: argparse.Namespace, context: CliContext) -> dict:
|
|
335
396
|
return context.builder.workspace_icon_catalog_get(profile=args.profile)
|
|
336
397
|
|
|
@@ -554,6 +615,8 @@ def _handle_chart_get(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
554
615
|
return context.builder.chart_get(profile=args.profile, chart_id=args.chart_id)
|
|
555
616
|
|
|
556
617
|
|
|
618
|
+
# TODO(deprecation): The legacy schema/layout CLI routes are removed. Keep the implementation below only
|
|
619
|
+
# while internal migration and focused regression coverage still depend on the imperative adapters.
|
|
557
620
|
def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
|
|
558
621
|
apps_payload = _load_apps_file_arg(args.apps_file)
|
|
559
622
|
apps = apps_payload["apps"]
|
|
@@ -8,7 +8,13 @@ from typing import Any
|
|
|
8
8
|
from ..errors import QingflowApiError
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
def load_json_value(
|
|
11
|
+
def load_json_value(
|
|
12
|
+
path: str,
|
|
13
|
+
*,
|
|
14
|
+
option_name: str,
|
|
15
|
+
max_bytes: int | None = None,
|
|
16
|
+
max_depth: int | None = None,
|
|
17
|
+
) -> Any:
|
|
12
18
|
if not path:
|
|
13
19
|
raise QingflowApiError.config_error(f"{option_name} is required")
|
|
14
20
|
if path == "-":
|
|
@@ -18,10 +24,23 @@ def load_json_value(path: str, *, option_name: str) -> Any:
|
|
|
18
24
|
raw = Path(path).expanduser().read_text(encoding="utf-8")
|
|
19
25
|
except OSError as exc:
|
|
20
26
|
raise QingflowApiError.config_error(f"failed to read {option_name} from '{path}': {exc}") from exc
|
|
27
|
+
if max_bytes is not None and len(raw.encode("utf-8")) > max_bytes:
|
|
28
|
+
raise QingflowApiError.config_error(f"{option_name} exceeds the {max_bytes}-byte limit")
|
|
21
29
|
try:
|
|
22
|
-
|
|
30
|
+
value = json.loads(raw)
|
|
23
31
|
except json.JSONDecodeError as exc:
|
|
24
32
|
raise QingflowApiError.config_error(f"{option_name} must contain valid JSON: {exc}") from exc
|
|
33
|
+
if max_depth is not None:
|
|
34
|
+
stack = [(value, 1)]
|
|
35
|
+
while stack:
|
|
36
|
+
item, depth = stack.pop()
|
|
37
|
+
if depth > max_depth:
|
|
38
|
+
raise QingflowApiError.config_error(f"{option_name} exceeds the maximum nesting depth {max_depth}")
|
|
39
|
+
if isinstance(item, dict):
|
|
40
|
+
stack.extend((child, depth + 1) for child in item.values())
|
|
41
|
+
elif isinstance(item, list):
|
|
42
|
+
stack.extend((child, depth + 1) for child in item)
|
|
43
|
+
return value
|
|
25
44
|
|
|
26
45
|
|
|
27
46
|
def load_json_object(path: str, *, option_name: str) -> dict[str, Any]:
|
|
@@ -79,23 +79,6 @@ def run(
|
|
|
79
79
|
return 2
|
|
80
80
|
except SystemExit as exc:
|
|
81
81
|
return int(exc.code or 0)
|
|
82
|
-
try:
|
|
83
|
-
_validate_conditional_args(args)
|
|
84
|
-
except _CliArgumentError as exc:
|
|
85
|
-
if _should_force_json_output_argv(normalized_argv):
|
|
86
|
-
payload = {
|
|
87
|
-
"category": "config",
|
|
88
|
-
"status": "failed",
|
|
89
|
-
"error_code": "ARGUMENT_ERROR",
|
|
90
|
-
"message": exc.message,
|
|
91
|
-
"details": {"usage": exc.usage.strip(), "prog": exc.prog},
|
|
92
|
-
}
|
|
93
|
-
payload = _maybe_attach_builder_apply_error_envelope_from_args(args, payload)
|
|
94
|
-
emit_json_result(payload, stream=out)
|
|
95
|
-
return 2
|
|
96
|
-
err.write(exc.usage)
|
|
97
|
-
err.write(f"{exc.prog}: error: {exc.message}\n")
|
|
98
|
-
return 2
|
|
99
82
|
setattr(args, "_stdin", sys.stdin)
|
|
100
83
|
setattr(args, "_stdout_stream", out)
|
|
101
84
|
setattr(args, "_stderr_stream", err)
|
|
@@ -161,21 +144,6 @@ def _emit_version(*, json_mode: bool, stdout: TextIO) -> int:
|
|
|
161
144
|
return 0
|
|
162
145
|
|
|
163
146
|
|
|
164
|
-
def _validate_conditional_args(args: argparse.Namespace) -> None:
|
|
165
|
-
if (
|
|
166
|
-
getattr(args, "command", "") in {"builder", "build"}
|
|
167
|
-
and getattr(args, "builder_command", "") == "layout"
|
|
168
|
-
and getattr(args, "builder_layout_command", "") == "apply"
|
|
169
|
-
and not getattr(args, "apps_file", None)
|
|
170
|
-
and not getattr(args, "sections_file", None)
|
|
171
|
-
):
|
|
172
|
-
raise _CliArgumentError(
|
|
173
|
-
prog="qingflow builder layout apply",
|
|
174
|
-
message="--sections-file is required",
|
|
175
|
-
usage="usage: qingflow builder layout apply --app-key APP_KEY --sections-file SECTIONS.json\n",
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
|
|
179
147
|
def _should_force_json_output_argv_for_version(argv: list[str]) -> bool:
|
|
180
148
|
return "--json" in argv
|
|
181
149
|
|
|
@@ -231,8 +199,10 @@ def _should_force_json_output_argv(argv: list[str]) -> bool:
|
|
|
231
199
|
return False
|
|
232
200
|
section = tokens[1]
|
|
233
201
|
action = tokens[2]
|
|
234
|
-
if section in {"package", "button", "associated-resource", "associated-resources", "portal", "
|
|
202
|
+
if section in {"package", "button", "associated-resource", "associated-resources", "portal", "views", "flow", "charts"}:
|
|
235
203
|
return action == "apply"
|
|
204
|
+
if section == "app-form":
|
|
205
|
+
return action in {"schema", "get", "validate", "apply"}
|
|
236
206
|
if section == "publish":
|
|
237
207
|
return action == "verify"
|
|
238
208
|
if section == "app":
|
|
@@ -283,10 +253,6 @@ def _builder_apply_operation_from_args(args: argparse.Namespace) -> str | None:
|
|
|
283
253
|
return "portal_apply"
|
|
284
254
|
if section == "portal" and getattr(args, "builder_portal_command", "") == "delete":
|
|
285
255
|
return "portal_delete"
|
|
286
|
-
if section == "schema" and getattr(args, "builder_schema_command", "") == "apply":
|
|
287
|
-
return "app_schema_apply"
|
|
288
|
-
if section == "layout" and getattr(args, "builder_layout_command", "") == "apply":
|
|
289
|
-
return "app_layout_apply"
|
|
290
256
|
if section == "views" and getattr(args, "builder_views_command", "") == "apply":
|
|
291
257
|
return "app_views_apply"
|
|
292
258
|
if section == "flow" and getattr(args, "builder_flow_command", "") == "apply":
|
|
@@ -312,8 +278,6 @@ def _builder_apply_operation_from_argv(argv: list[str]) -> str | None:
|
|
|
312
278
|
"associated-resource": "app_associated_resources_apply",
|
|
313
279
|
"associated-resources": "app_associated_resources_apply",
|
|
314
280
|
"portal": "portal_apply",
|
|
315
|
-
"schema": "app_schema_apply",
|
|
316
|
-
"layout": "app_layout_apply",
|
|
317
281
|
"views": "app_views_apply",
|
|
318
282
|
"flow": "app_flow_apply",
|
|
319
283
|
"charts": "app_charts_apply",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from typing import Any
|
|
5
6
|
from urllib.parse import urlsplit, urlunsplit
|
|
@@ -22,8 +23,16 @@ from .json_types import JSONObject
|
|
|
22
23
|
|
|
23
24
|
PROTECTED_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource"
|
|
24
25
|
AUTHORIZATION_SERVER_METADATA_PATH = "/.well-known/oauth-authorization-server"
|
|
26
|
+
OAUTH_ENDPOINT_PATHS = {
|
|
27
|
+
"authorization_endpoint": "/oauth/authorize",
|
|
28
|
+
"token_endpoint": "/oauth/token",
|
|
29
|
+
"registration_endpoint": "/oauth/register",
|
|
30
|
+
}
|
|
31
|
+
FORWARDED_REQUEST_HEADERS = {"accept", "content-type"}
|
|
25
32
|
HOP_BY_HOP_RESPONSE_HEADERS = {
|
|
26
33
|
"connection",
|
|
34
|
+
# httpx decodes compressed response bodies before exposing response.content.
|
|
35
|
+
"content-encoding",
|
|
27
36
|
"keep-alive",
|
|
28
37
|
"proxy-authenticate",
|
|
29
38
|
"proxy-authorization",
|
|
@@ -58,10 +67,12 @@ class CloudOAuthProxyConfig:
|
|
|
58
67
|
|
|
59
68
|
@property
|
|
60
69
|
def resource_metadata_url(self) -> str:
|
|
70
|
+
return f"{self.metadata_issuer_url}{PROTECTED_RESOURCE_METADATA_PATH}"
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def metadata_issuer_url(self) -> str:
|
|
61
74
|
parsed = urlsplit(self.resource_url)
|
|
62
|
-
return urlunsplit(
|
|
63
|
-
(parsed.scheme, parsed.netloc, PROTECTED_RESOURCE_METADATA_PATH, "", "")
|
|
64
|
-
)
|
|
75
|
+
return urlunsplit((parsed.scheme, parsed.netloc, "", "", ""))
|
|
65
76
|
|
|
66
77
|
def backend_metadata_path(self, request_path: str) -> str | None:
|
|
67
78
|
if request_path == PROTECTED_RESOURCE_METADATA_PATH or request_path.startswith(
|
|
@@ -74,6 +85,11 @@ class CloudOAuthProxyConfig:
|
|
|
74
85
|
return AUTHORIZATION_SERVER_METADATA_PATH
|
|
75
86
|
return None
|
|
76
87
|
|
|
88
|
+
def backend_oauth_endpoint_path(self, request_path: str) -> str | None:
|
|
89
|
+
if request_path in OAUTH_ENDPOINT_PATHS.values():
|
|
90
|
+
return request_path
|
|
91
|
+
return None
|
|
92
|
+
|
|
77
93
|
|
|
78
94
|
@dataclass(frozen=True, slots=True)
|
|
79
95
|
class OAuthProxyResponse:
|
|
@@ -131,9 +147,86 @@ class OAuthServiceProxy:
|
|
|
131
147
|
return OAuthProxyResponse(
|
|
132
148
|
status_code=response.status_code,
|
|
133
149
|
headers=headers,
|
|
150
|
+
body=self._rewrite_metadata_body(backend_path, response),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def request_oauth_endpoint(
|
|
154
|
+
self,
|
|
155
|
+
method: str,
|
|
156
|
+
backend_path: str,
|
|
157
|
+
query_string: bytes = b"",
|
|
158
|
+
body: bytes = b"",
|
|
159
|
+
headers: tuple[tuple[bytes, bytes], ...] = (),
|
|
160
|
+
) -> OAuthProxyResponse:
|
|
161
|
+
url = f"{self.config.service_base_url}/{backend_path.lstrip('/')}"
|
|
162
|
+
if query_string:
|
|
163
|
+
url = f"{url}?{query_string.decode('latin1')}"
|
|
164
|
+
forwarded_headers = {
|
|
165
|
+
name.decode("latin1"): value.decode("latin1")
|
|
166
|
+
for name, value in headers
|
|
167
|
+
if name.decode("latin1").lower() in FORWARDED_REQUEST_HEADERS
|
|
168
|
+
}
|
|
169
|
+
try:
|
|
170
|
+
response = self._client.request(
|
|
171
|
+
method.upper(),
|
|
172
|
+
url,
|
|
173
|
+
content=body,
|
|
174
|
+
headers=forwarded_headers,
|
|
175
|
+
)
|
|
176
|
+
except httpx.RequestError as error:
|
|
177
|
+
raise QingflowApiError(
|
|
178
|
+
category="network",
|
|
179
|
+
message=f"OAuth service request failed: {error}",
|
|
180
|
+
) from error
|
|
181
|
+
|
|
182
|
+
response_headers = tuple(
|
|
183
|
+
(name.encode("latin1"), value.encode("latin1"))
|
|
184
|
+
for name, value in response.headers.multi_items()
|
|
185
|
+
if name.lower() not in HOP_BY_HOP_RESPONSE_HEADERS
|
|
186
|
+
and name.lower() != "content-length"
|
|
187
|
+
)
|
|
188
|
+
return OAuthProxyResponse(
|
|
189
|
+
status_code=response.status_code,
|
|
190
|
+
headers=response_headers,
|
|
134
191
|
body=response.content,
|
|
135
192
|
)
|
|
136
193
|
|
|
194
|
+
def _rewrite_metadata_body(
|
|
195
|
+
self,
|
|
196
|
+
backend_path: str,
|
|
197
|
+
response: httpx.Response,
|
|
198
|
+
) -> bytes:
|
|
199
|
+
if not 200 <= response.status_code < 300 or not response.content:
|
|
200
|
+
return response.content
|
|
201
|
+
try:
|
|
202
|
+
payload = json.loads(response.content)
|
|
203
|
+
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
204
|
+
return response.content
|
|
205
|
+
if not isinstance(payload, dict):
|
|
206
|
+
return response.content
|
|
207
|
+
|
|
208
|
+
changed = False
|
|
209
|
+
issuer = self.config.metadata_issuer_url
|
|
210
|
+
if backend_path == PROTECTED_RESOURCE_METADATA_PATH:
|
|
211
|
+
for field_name in ("authorization_servers", "authorizationServers"):
|
|
212
|
+
if field_name in payload:
|
|
213
|
+
payload[field_name] = [issuer]
|
|
214
|
+
changed = True
|
|
215
|
+
elif backend_path == AUTHORIZATION_SERVER_METADATA_PATH:
|
|
216
|
+
if "issuer" in payload:
|
|
217
|
+
payload["issuer"] = issuer
|
|
218
|
+
changed = True
|
|
219
|
+
for field_name, endpoint_path in OAUTH_ENDPOINT_PATHS.items():
|
|
220
|
+
if field_name in payload:
|
|
221
|
+
payload[field_name] = f"{issuer}{endpoint_path}"
|
|
222
|
+
changed = True
|
|
223
|
+
|
|
224
|
+
if not changed:
|
|
225
|
+
return response.content
|
|
226
|
+
return json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode(
|
|
227
|
+
"utf-8"
|
|
228
|
+
)
|
|
229
|
+
|
|
137
230
|
|
|
138
231
|
@dataclass(slots=True)
|
|
139
232
|
class OAuthAccessTokenResolver:
|
|
@@ -142,6 +142,11 @@ BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
|
|
|
142
142
|
PublicToolSpec(BUILDER_DOMAIN, "role_create", ("role_create",), ("builder", "role", "create"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
|
|
143
143
|
PublicToolSpec(BUILDER_DOMAIN, "app_release_edit_lock_if_mine", ("app_release_edit_lock_if_mine",), ("builder", "app", "release-edit-lock-if-mine"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
|
|
144
144
|
PublicToolSpec(BUILDER_DOMAIN, "app_resolve", ("app_resolve",), ("builder", "app", "resolve"), has_contract=True, cli_show_effective_context=True),
|
|
145
|
+
PublicToolSpec(BUILDER_DOMAIN, "app_form_schema", ("app_form_schema",), ("builder", "app-form", "schema")),
|
|
146
|
+
PublicToolSpec(BUILDER_DOMAIN, "app_form_validate", (), ("builder", "app-form", "validate"), mcp_public=False),
|
|
147
|
+
PublicToolSpec(BUILDER_DOMAIN, "app_form_get", ("app_form_get",), ("builder", "app-form", "get"), cli_show_effective_context=True),
|
|
148
|
+
PublicToolSpec(BUILDER_DOMAIN, "app_form_apply", ("app_form_apply",), ("builder", "app-form", "apply"), cli_show_effective_context=True, cli_context_write=True),
|
|
149
|
+
PublicToolSpec(BUILDER_DOMAIN, "app_delete", ("app_delete",), ("builder", "app", "delete"), cli_show_effective_context=True, cli_context_write=True),
|
|
145
150
|
PublicToolSpec(BUILDER_DOMAIN, "button_style_catalog_get", ("button_style_catalog_get",), ("builder", "button", "catalog"), has_contract=True, cli_show_effective_context=True),
|
|
146
151
|
PublicToolSpec(BUILDER_DOMAIN, "app_custom_buttons_apply", ("app_custom_buttons_apply",), ("builder", "button", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
|
|
147
152
|
PublicToolSpec(BUILDER_DOMAIN, "app_associated_resources_apply", ("app_associated_resources_apply",), ("builder", "associated-resource", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
|
|
@@ -158,8 +163,6 @@ BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
|
|
|
158
163
|
PublicToolSpec(BUILDER_DOMAIN, "portal_get", ("portal_get",), ("builder", "portal", "get"), has_contract=True, cli_show_effective_context=True),
|
|
159
164
|
PublicToolSpec(BUILDER_DOMAIN, "view_get", ("view_get",), ("builder", "view", "get"), has_contract=True, cli_show_effective_context=True),
|
|
160
165
|
PublicToolSpec(BUILDER_DOMAIN, "chart_get", ("chart_get",), ("builder", "chart", "get"), has_contract=True, cli_show_effective_context=True),
|
|
161
|
-
PublicToolSpec(BUILDER_DOMAIN, "app_schema_apply", ("app_schema_apply",), ("builder", "schema", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
|
|
162
|
-
PublicToolSpec(BUILDER_DOMAIN, "app_layout_apply", ("app_layout_apply",), ("builder", "layout", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
|
|
163
166
|
PublicToolSpec(BUILDER_DOMAIN, "app_flow_get_schema", ("app_flow_get_schema",), ("builder", "flow", "schema"), has_contract=True, cli_show_effective_context=True),
|
|
164
167
|
PublicToolSpec(BUILDER_DOMAIN, "app_flow_get", ("app_flow_get",), ("builder", "flow", "get"), has_contract=True, cli_show_effective_context=True),
|
|
165
168
|
PublicToolSpec(BUILDER_DOMAIN, "app_flow_apply", ("app_flow_apply",), ("builder", "flow", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
|
|
@@ -55,6 +55,8 @@ SUCCESS_POLICY_BY_TOOL: dict[str, TransformFn] = {}
|
|
|
55
55
|
def trim_public_response(tool_name: str | None, payload: dict[str, Any]) -> dict[str, Any]:
|
|
56
56
|
if not isinstance(payload, dict):
|
|
57
57
|
return payload
|
|
58
|
+
if tool_name == "builder:app_form_apply" and str(payload.get("status") or "").lower() == "partial_success":
|
|
59
|
+
return trim_success_response(tool_name, payload)
|
|
58
60
|
if _looks_like_failure_payload(payload):
|
|
59
61
|
status = str(payload.get("status") or "").lower()
|
|
60
62
|
if _is_executed_nonfatal_payload(payload) or (
|
|
@@ -1086,6 +1088,44 @@ def _trim_builder_list_like(payload: JSONObject) -> None:
|
|
|
1086
1088
|
_trim_builder_envelope(payload)
|
|
1087
1089
|
|
|
1088
1090
|
|
|
1091
|
+
def _trim_app_form_apply(payload: JSONObject) -> None:
|
|
1092
|
+
details = payload.get("details")
|
|
1093
|
+
details_diff = deepcopy(details.get("diff")) if isinstance(details, dict) and isinstance(details.get("diff"), dict) else None
|
|
1094
|
+
if isinstance(details, dict):
|
|
1095
|
+
_drop_deep_keys(details, {"transportError"})
|
|
1096
|
+
_trim_builder_envelope(payload)
|
|
1097
|
+
_trim_form_diff(payload)
|
|
1098
|
+
if details_diff is not None:
|
|
1099
|
+
payload.setdefault("details", {})["diff"] = _summarize_form_diff(details_diff)
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
def _trim_form_diff(payload: JSONObject) -> None:
|
|
1103
|
+
diff = payload.get("diff")
|
|
1104
|
+
if isinstance(diff, dict):
|
|
1105
|
+
payload["diff"] = _summarize_form_diff(diff)
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
def _summarize_form_diff(diff: dict[str, Any]) -> dict[str, Any]:
|
|
1109
|
+
summary: dict[str, Any] = {}
|
|
1110
|
+
for key in ("added", "updated", "removed"):
|
|
1111
|
+
items = diff.get(key)
|
|
1112
|
+
if not isinstance(items, list):
|
|
1113
|
+
continue
|
|
1114
|
+
names = []
|
|
1115
|
+
for item in items:
|
|
1116
|
+
if isinstance(item, str):
|
|
1117
|
+
names.append(item)
|
|
1118
|
+
elif isinstance(item, dict):
|
|
1119
|
+
name = item.get("name") or item.get("queTitle")
|
|
1120
|
+
if name:
|
|
1121
|
+
names.append(name)
|
|
1122
|
+
summary[key] = names
|
|
1123
|
+
for key in ("layoutChanged", "configChanged"):
|
|
1124
|
+
if key in diff:
|
|
1125
|
+
summary[key] = bool(diff[key])
|
|
1126
|
+
return summary
|
|
1127
|
+
|
|
1128
|
+
|
|
1089
1129
|
def _register_policy(domains: tuple[str, ...], names: tuple[str, ...], transform: TransformFn) -> None:
|
|
1090
1130
|
for domain in domains:
|
|
1091
1131
|
for name in names:
|
|
@@ -1190,6 +1230,9 @@ _register_policy(
|
|
|
1190
1230
|
"role_create",
|
|
1191
1231
|
"app_release_edit_lock_if_mine",
|
|
1192
1232
|
"app_resolve",
|
|
1233
|
+
"app_form_schema",
|
|
1234
|
+
"app_form_get",
|
|
1235
|
+
"app_delete",
|
|
1193
1236
|
"button_style_catalog_get",
|
|
1194
1237
|
"app_custom_buttons_apply",
|
|
1195
1238
|
"app_associated_resources_apply",
|
|
@@ -1198,7 +1241,11 @@ _register_policy(
|
|
|
1198
1241
|
"app_get_layout",
|
|
1199
1242
|
"app_get_views",
|
|
1200
1243
|
"app_get_flow",
|
|
1244
|
+
"app_flow_get_schema",
|
|
1245
|
+
"app_flow_get",
|
|
1201
1246
|
"app_get_charts",
|
|
1247
|
+
"app_get_buttons",
|
|
1248
|
+
"app_get_associated_resources",
|
|
1202
1249
|
"app_schema_apply",
|
|
1203
1250
|
"app_layout_apply",
|
|
1204
1251
|
"app_flow_apply",
|
|
@@ -1210,3 +1257,4 @@ _register_policy(
|
|
|
1210
1257
|
),
|
|
1211
1258
|
_trim_builder_list_like,
|
|
1212
1259
|
)
|
|
1260
|
+
_register_policy((BUILDER_DOMAIN,), ("app_form_apply",), _trim_app_form_apply)
|