@qingflow-tech/qingflow-cli 1.1.43 → 1.1.44-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 +13 -6
- package/entry_point.py +1 -1
- package/npm/bin/qingflow.mjs +1 -2
- package/npm/lib/runtime.mjs +3 -7
- package/package.json +2 -2
- package/pyproject.toml +3 -4
- package/skills/qingflow-app-form/SKILL.md +66 -0
- package/skills/qingflow-app-form/agents/openai.yaml +4 -0
- package/skills/qingflow-cli/SKILL.md +8 -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 +108 -103
- 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-common/SKILL.md +47 -0
- package/skills/qingflow-common/agents/openai.yaml +4 -0
- package/src/qingflow_mcp/__main__.py +2 -3
- package/src/qingflow_mcp/app_form_compat.py +104 -0
- package/src/qingflow_mcp/app_form_validator.py +462 -0
- package/src/qingflow_mcp/builder_facade/app_form.py +4205 -0
- package/src/qingflow_mcp/builder_facade/app_form_schema.py +425 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +42 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +2969 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +392 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +236 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +425 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +318 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +580 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +150 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +150 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +345 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +190 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +190 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +190 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +484 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +214 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +236 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +199 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +412 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +454 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +214 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +214 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +1921 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +190 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/formula.json +1594 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +718 -0
- package/src/qingflow_mcp/builder_facade/service.py +8 -4
- package/src/qingflow_mcp/cli/commands/builder.py +93 -28
- package/src/qingflow_mcp/cli/json_io.py +21 -2
- package/src/qingflow_mcp/cli/main.py +3 -39
- package/src/qingflow_mcp/public_surface.py +5 -2
- package/src/qingflow_mcp/response_trim.py +50 -0
- package/src/qingflow_mcp/server.py +24 -3
- package/src/qingflow_mcp/server_app_builder.py +44 -87
- package/src/qingflow_mcp/tools/ai_builder_tools.py +103 -100
- package/src/qingflow_mcp/tools/app_tools.py +46 -65
- package/src/qingflow_mcp/tools/record_tools.py +3 -3
- package/src/qingflow_mcp/version.py +2 -4
- package/docs/local-agent-install.md +0 -370
- package/skills/qingflow-mcp-setup/SKILL.md +0 -115
- package/skills/qingflow-mcp-setup/agents/openai.yaml +0 -4
- package/skills/qingflow-mcp-setup/references/claude-desktop.md +0 -34
- package/skills/qingflow-mcp-setup/references/environments.md +0 -62
- package/skills/qingflow-mcp-setup/references/generic-stdio.md +0 -32
- package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +0 -38
|
@@ -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,
|
|
@@ -22715,6 +22718,7 @@ def _compile_q_linker_binding_fields(
|
|
|
22715
22718
|
"target_field_name": target_field.get("name"),
|
|
22716
22719
|
"target_field_ref": target_ref,
|
|
22717
22720
|
"alias": output_item.get("alias_name"),
|
|
22721
|
+
"alias_id": _coerce_positive_int(output_item.get("alias_id")),
|
|
22718
22722
|
}
|
|
22719
22723
|
)
|
|
22720
22724
|
|
|
@@ -22787,7 +22791,7 @@ def _materialize_q_linker_question_relations(
|
|
|
22787
22791
|
"queId": source_ref,
|
|
22788
22792
|
"queTitle": source_field.get("name"),
|
|
22789
22793
|
"qlinkerAlias": relation.get("alias"),
|
|
22790
|
-
"aliasId":
|
|
22794
|
+
"aliasId": _coerce_positive_int(relation.get("alias_id")),
|
|
22791
22795
|
},
|
|
22792
22796
|
"matchRuleType": 1,
|
|
22793
22797
|
"matchRules": [],
|
|
@@ -24004,7 +24008,7 @@ def _normalize_formula_defaults_for_save(value: Any) -> None:
|
|
|
24004
24008
|
return
|
|
24005
24009
|
if not isinstance(value, dict):
|
|
24006
24010
|
return
|
|
24007
|
-
if
|
|
24011
|
+
if value.get("queDefaultValue"):
|
|
24008
24012
|
value["queDefaultValue"] = _encode_formula_for_backend_save(value.get("queDefaultValue"))
|
|
24009
24013
|
for key in ("subQuestions", "innerQuestions"):
|
|
24010
24014
|
nested = value.get(key)
|
|
@@ -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,30 @@ 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("--schema-kind", choices=("summary", "field", "formula"))
|
|
108
|
+
app_form_schema.add_argument("--field-type")
|
|
109
|
+
app_form_schema.set_defaults(handler=_handle_app_form_schema, format_hint="builder_summary", force_json_output=True)
|
|
110
|
+
|
|
111
|
+
app_form_get = app_form_subparsers.add_parser("get", help="读取 canonical AppForm")
|
|
112
|
+
app_form_get.add_argument("--app-key", required=True)
|
|
113
|
+
app_form_get.add_argument("--schema-version")
|
|
114
|
+
app_form_get.add_argument("--being-draft", action=argparse.BooleanOptionalAction, default=False)
|
|
115
|
+
app_form_get.set_defaults(handler=_handle_app_form_get, format_hint="builder_summary", force_json_output=True)
|
|
116
|
+
|
|
117
|
+
app_form_apply = app_form_subparsers.add_parser("apply", help="应用完整 AppForm 声明")
|
|
118
|
+
app_form_apply.add_argument("--file", required=True, help="AppForm JSON 文件,- 表示 stdin")
|
|
119
|
+
app_form_apply.set_defaults(handler=_handle_app_form_apply, format_hint="builder_summary", force_json_output=True)
|
|
120
|
+
|
|
121
|
+
app_form_validate = app_form_subparsers.add_parser("validate", help="只校验 AppForm 声明,不访问后端")
|
|
122
|
+
app_form_validate.add_argument("--file", required=True, help="AppForm JSON 文件,- 表示 stdin")
|
|
123
|
+
app_form_validate.add_argument("--schema-version", help="固定使用的 AppForm Schema 版本")
|
|
124
|
+
app_form_validate.set_defaults(handler=_handle_app_form_validate, format_hint="builder_summary", force_json_output=True)
|
|
125
|
+
|
|
101
126
|
app = builder_subparsers.add_parser("app", help="应用")
|
|
102
127
|
app_subparsers = app.add_subparsers(dest="builder_app_command", required=True)
|
|
103
128
|
|
|
@@ -113,6 +138,10 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
113
138
|
app_release_lock.add_argument("--lock-owner-name", required=True)
|
|
114
139
|
app_release_lock.set_defaults(handler=_handle_app_release_edit_lock_if_mine, format_hint="builder_summary", force_json_output=True)
|
|
115
140
|
|
|
141
|
+
app_delete = app_subparsers.add_parser("delete", help="删除应用")
|
|
142
|
+
app_delete.add_argument("--app-key", required=True)
|
|
143
|
+
app_delete.set_defaults(handler=_handle_app_delete, format_hint="builder_summary", force_json_output=True)
|
|
144
|
+
|
|
116
145
|
app_get = app_subparsers.add_parser("get", help="读取应用配置(字段请使用: builder app get --app-key APP fields)")
|
|
117
146
|
app_get.add_argument(
|
|
118
147
|
"builder_app_get_section",
|
|
@@ -202,34 +231,6 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
202
231
|
portal_delete.add_argument("--dash-key", required=True)
|
|
203
232
|
portal_delete.set_defaults(handler=_handle_portal_delete, format_hint="builder_summary", force_json_output=True)
|
|
204
233
|
|
|
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
234
|
views_apply = builder_subparsers.add_parser("views", help="视图")
|
|
234
235
|
views_apply_subparsers = views_apply.add_subparsers(dest="builder_views_command", required=True)
|
|
235
236
|
views_apply_apply = views_apply_subparsers.add_parser("apply", help="执行视图变更")
|
|
@@ -331,6 +332,68 @@ def _handle_contract(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
331
332
|
return context.builder.builder_tool_contract(tool_name=args.tool_name)
|
|
332
333
|
|
|
333
334
|
|
|
335
|
+
def _handle_app_form_schema(args: argparse.Namespace, context: CliContext) -> dict:
|
|
336
|
+
return context.builder.app_form_schema(
|
|
337
|
+
schema_version=args.schema_version,
|
|
338
|
+
field_type=args.field_type,
|
|
339
|
+
schema_kind=args.schema_kind,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _handle_app_form_get(args: argparse.Namespace, context: CliContext) -> dict:
|
|
344
|
+
return context.builder.app_form_get(
|
|
345
|
+
profile=args.profile,
|
|
346
|
+
app_key=args.app_key,
|
|
347
|
+
schema_version=args.schema_version,
|
|
348
|
+
being_draft=bool(args.being_draft),
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _handle_app_form_apply(args: argparse.Namespace, context: CliContext) -> dict:
|
|
353
|
+
payload = load_json_value(
|
|
354
|
+
args.file,
|
|
355
|
+
option_name="--file",
|
|
356
|
+
max_bytes=2 * 1024 * 1024,
|
|
357
|
+
max_depth=64,
|
|
358
|
+
)
|
|
359
|
+
if not isinstance(payload, dict):
|
|
360
|
+
raise_config_error("builder app-form apply --file must contain a JSON object")
|
|
361
|
+
return context.builder.app_form_apply(profile=args.profile, spec=payload)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _handle_app_form_validate(args: argparse.Namespace, _context: CliContext) -> dict:
|
|
365
|
+
payload = load_json_value(
|
|
366
|
+
args.file,
|
|
367
|
+
option_name="--file",
|
|
368
|
+
max_bytes=2 * 1024 * 1024,
|
|
369
|
+
max_depth=64,
|
|
370
|
+
)
|
|
371
|
+
if not isinstance(payload, dict):
|
|
372
|
+
raise_config_error("builder app-form validate --file must contain a JSON object")
|
|
373
|
+
try:
|
|
374
|
+
result = validate_declaration(payload, schema_version=args.schema_version)
|
|
375
|
+
except AppFormValidationError as error:
|
|
376
|
+
return {
|
|
377
|
+
"category": "validation",
|
|
378
|
+
"status": "failed",
|
|
379
|
+
"ok": False,
|
|
380
|
+
"error_code": "INVALID_SPEC",
|
|
381
|
+
"message": "AppForm declaration failed validation",
|
|
382
|
+
"details": {
|
|
383
|
+
"schemaVersion": error.schema_version,
|
|
384
|
+
"apiVersion": error.api_version,
|
|
385
|
+
"issues": error.issues,
|
|
386
|
+
},
|
|
387
|
+
}
|
|
388
|
+
except (ValueError, RuntimeError) as error:
|
|
389
|
+
raise_config_error(str(error), error_code="VALIDATION_ERROR")
|
|
390
|
+
return {"category": "validation", "status": "success", "ok": True, **result, "message": "AppForm declaration is valid"}
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _handle_app_delete(args: argparse.Namespace, context: CliContext) -> dict:
|
|
394
|
+
return context.builder.app_delete(profile=args.profile, app_key=args.app_key)
|
|
395
|
+
|
|
396
|
+
|
|
334
397
|
def _handle_icon_catalog(args: argparse.Namespace, context: CliContext) -> dict:
|
|
335
398
|
return context.builder.workspace_icon_catalog_get(profile=args.profile)
|
|
336
399
|
|
|
@@ -554,6 +617,8 @@ def _handle_chart_get(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
554
617
|
return context.builder.chart_get(profile=args.profile, chart_id=args.chart_id)
|
|
555
618
|
|
|
556
619
|
|
|
620
|
+
# TODO(deprecation): The legacy schema/layout CLI routes are removed. Keep the implementation below only
|
|
621
|
+
# while internal migration and focused regression coverage still depend on the imperative adapters.
|
|
557
622
|
def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
|
|
558
623
|
apps_payload = _load_apps_file_arg(args.apps_file)
|
|
559
624
|
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",
|
|
@@ -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 (
|
|
@@ -83,6 +85,8 @@ def trim_success_response(tool_name: str | None, payload: dict[str, Any]) -> dic
|
|
|
83
85
|
drop_keys = COMMON_SUCCESS_DROP_TOP - {"output_profile"}
|
|
84
86
|
if tool_name in {"user:record_insert", "user:record_update", "user:record_delete"} and payload.get("ok") is False:
|
|
85
87
|
drop_keys = drop_keys - {"ok"}
|
|
88
|
+
if tool_name == "builder:app_form_apply" and payload.get("mustReadCurrentStateBeforeRepair") is True:
|
|
89
|
+
drop_keys = drop_keys - {"suggested_next_call"}
|
|
86
90
|
_drop_top_keys(trimmed, drop_keys)
|
|
87
91
|
transformer = SUCCESS_POLICY_BY_TOOL.get(tool_name or "")
|
|
88
92
|
if transformer is not None:
|
|
@@ -1086,6 +1090,44 @@ def _trim_builder_list_like(payload: JSONObject) -> None:
|
|
|
1086
1090
|
_trim_builder_envelope(payload)
|
|
1087
1091
|
|
|
1088
1092
|
|
|
1093
|
+
def _trim_app_form_apply(payload: JSONObject) -> None:
|
|
1094
|
+
details = payload.get("details")
|
|
1095
|
+
details_diff = deepcopy(details.get("diff")) if isinstance(details, dict) and isinstance(details.get("diff"), dict) else None
|
|
1096
|
+
if isinstance(details, dict):
|
|
1097
|
+
_drop_deep_keys(details, {"transportError"})
|
|
1098
|
+
_trim_builder_envelope(payload)
|
|
1099
|
+
_trim_form_diff(payload)
|
|
1100
|
+
if details_diff is not None:
|
|
1101
|
+
payload.setdefault("details", {})["diff"] = _summarize_form_diff(details_diff)
|
|
1102
|
+
|
|
1103
|
+
|
|
1104
|
+
def _trim_form_diff(payload: JSONObject) -> None:
|
|
1105
|
+
diff = payload.get("diff")
|
|
1106
|
+
if isinstance(diff, dict):
|
|
1107
|
+
payload["diff"] = _summarize_form_diff(diff)
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
def _summarize_form_diff(diff: dict[str, Any]) -> dict[str, Any]:
|
|
1111
|
+
summary: dict[str, Any] = {}
|
|
1112
|
+
for key in ("added", "updated", "removed"):
|
|
1113
|
+
items = diff.get(key)
|
|
1114
|
+
if not isinstance(items, list):
|
|
1115
|
+
continue
|
|
1116
|
+
names = []
|
|
1117
|
+
for item in items:
|
|
1118
|
+
if isinstance(item, str):
|
|
1119
|
+
names.append(item)
|
|
1120
|
+
elif isinstance(item, dict):
|
|
1121
|
+
name = item.get("name") or item.get("queTitle")
|
|
1122
|
+
if name:
|
|
1123
|
+
names.append(name)
|
|
1124
|
+
summary[key] = names
|
|
1125
|
+
for key in ("layoutChanged", "configChanged"):
|
|
1126
|
+
if key in diff:
|
|
1127
|
+
summary[key] = bool(diff[key])
|
|
1128
|
+
return summary
|
|
1129
|
+
|
|
1130
|
+
|
|
1089
1131
|
def _register_policy(domains: tuple[str, ...], names: tuple[str, ...], transform: TransformFn) -> None:
|
|
1090
1132
|
for domain in domains:
|
|
1091
1133
|
for name in names:
|
|
@@ -1190,6 +1232,9 @@ _register_policy(
|
|
|
1190
1232
|
"role_create",
|
|
1191
1233
|
"app_release_edit_lock_if_mine",
|
|
1192
1234
|
"app_resolve",
|
|
1235
|
+
"app_form_schema",
|
|
1236
|
+
"app_form_get",
|
|
1237
|
+
"app_delete",
|
|
1193
1238
|
"button_style_catalog_get",
|
|
1194
1239
|
"app_custom_buttons_apply",
|
|
1195
1240
|
"app_associated_resources_apply",
|
|
@@ -1198,7 +1243,11 @@ _register_policy(
|
|
|
1198
1243
|
"app_get_layout",
|
|
1199
1244
|
"app_get_views",
|
|
1200
1245
|
"app_get_flow",
|
|
1246
|
+
"app_flow_get_schema",
|
|
1247
|
+
"app_flow_get",
|
|
1201
1248
|
"app_get_charts",
|
|
1249
|
+
"app_get_buttons",
|
|
1250
|
+
"app_get_associated_resources",
|
|
1202
1251
|
"app_schema_apply",
|
|
1203
1252
|
"app_layout_apply",
|
|
1204
1253
|
"app_flow_apply",
|
|
@@ -1210,3 +1259,4 @@ _register_policy(
|
|
|
1210
1259
|
),
|
|
1211
1260
|
_trim_builder_list_like,
|
|
1212
1261
|
)
|
|
1262
|
+
_register_policy((BUILDER_DOMAIN,), ("app_form_apply",), _trim_app_form_apply)
|
|
@@ -26,12 +26,32 @@ from .tools.workflow_tools import WorkflowTools
|
|
|
26
26
|
from .tools.workspace_tools import WorkspaceTools
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
def _register_missing_builder_tools(server: FastMCP) -> None:
|
|
30
|
+
"""Add current high-level builder tools to the unified MCP without duplicate names."""
|
|
31
|
+
from .server_app_builder import build_builder_server
|
|
32
|
+
|
|
33
|
+
builder_server = build_builder_server()
|
|
34
|
+
registered = server._tool_manager._tools
|
|
35
|
+
for tool in builder_server._tool_manager._tools.values():
|
|
36
|
+
if tool.name in registered:
|
|
37
|
+
continue
|
|
38
|
+
server.add_tool(
|
|
39
|
+
tool.fn,
|
|
40
|
+
name=tool.name,
|
|
41
|
+
title=tool.title,
|
|
42
|
+
description=tool.description,
|
|
43
|
+
annotations=tool.annotations,
|
|
44
|
+
icons=tool.icons,
|
|
45
|
+
meta=tool.meta,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
29
49
|
def build_server() -> FastMCP:
|
|
30
50
|
today = date.today()
|
|
31
51
|
current_year = today.year
|
|
32
52
|
server = FastMCP(
|
|
33
53
|
"Qingflow MCP",
|
|
34
|
-
instructions=f"""Use this server for Qingflow operational workflows. Current date: `{today.isoformat()}`.
|
|
54
|
+
instructions=f"""Use this unified server for Qingflow operational and builder workflows. Current date: `{today.isoformat()}`.
|
|
35
55
|
|
|
36
56
|
## Authentication
|
|
37
57
|
|
|
@@ -203,9 +223,9 @@ Normalize relative dates before building DSL.
|
|
|
203
223
|
|
|
204
224
|
Default to `prod` unless the user explicitly specifies `test`.
|
|
205
225
|
|
|
206
|
-
##
|
|
226
|
+
## Builder Path
|
|
207
227
|
|
|
208
|
-
|
|
228
|
+
Use the public builder tools for package, AppForm, workflow, view, chart, portal, and application configuration work. Pin the AppForm Schema version, preserve complete form declarations and existing IDs from draft reads, and use `app_form_apply` for reconciliation. Do not fall back to legacy raw builder write routes.
|
|
209
229
|
|
|
210
230
|
## Feedback Path
|
|
211
231
|
|
|
@@ -235,6 +255,7 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
235
255
|
WorkflowTools(sessions, backend).register(server)
|
|
236
256
|
ViewTools(sessions, backend).register(server)
|
|
237
257
|
SolutionTools(sessions, backend).register(server)
|
|
258
|
+
_register_missing_builder_tools(server)
|
|
238
259
|
return server
|
|
239
260
|
|
|
240
261
|
|