@josephyan/qingflow-app-builder-mcp 1.1.24 → 1.1.26

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 (98) hide show
  1. package/README.md +5 -6
  2. package/docs/local-agent-install.md +6 -57
  3. package/entry_point.py +1 -1
  4. package/npm/bin/qingflow-app-builder-mcp.mjs +33 -2
  5. package/npm/lib/runtime.mjs +101 -21
  6. package/npm/scripts/postinstall.mjs +10 -1
  7. package/package.json +2 -3
  8. package/pyproject.toml +1 -1
  9. package/skills/qingflow-app-builder/SKILL.md +42 -104
  10. package/skills/qingflow-app-builder/references/build-complete-system.md +428 -0
  11. package/skills/qingflow-app-builder/references/build-single-app.md +530 -0
  12. package/skills/qingflow-app-builder/references/create-app.md +11 -30
  13. package/skills/qingflow-app-builder/references/environments.md +1 -1
  14. package/skills/qingflow-app-builder/references/gotchas.md +9 -31
  15. package/skills/qingflow-app-builder/references/match-rules.md +0 -15
  16. package/skills/qingflow-app-builder/references/solution-playbooks.md +0 -10
  17. package/skills/qingflow-app-builder/references/tool-selection.md +23 -38
  18. package/skills/qingflow-app-builder/references/update-flow.md +38 -22
  19. package/skills/qingflow-app-builder/references/update-schema.md +0 -14
  20. package/skills/qingflow-app-builder/references/update-views.md +14 -83
  21. package/skills/qingflow-app-builder-code-integrations/SKILL.md +3 -5
  22. package/skills/qingflow-app-builder-code-integrations/references/code-block.md +1 -1
  23. package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +1 -1
  24. package/skills/qingflow-mcp-setup/SKILL.md +111 -0
  25. package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
  26. package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
  27. package/skills/qingflow-mcp-setup/references/environments.md +61 -0
  28. package/skills/qingflow-mcp-setup/references/generic-stdio.md +31 -0
  29. package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
  30. package/skills/qingflow-workflow-builder/SKILL.md +96 -0
  31. package/skills/qingflow-workflow-builder/manifest.yaml +8 -0
  32. package/skills/qingflow-workflow-builder/references/01-overview.md +45 -0
  33. package/skills/qingflow-workflow-builder/references/02-update-mode.md +53 -0
  34. package/skills/qingflow-workflow-builder/references/03-flow-patterns.md +57 -0
  35. package/skills/qingflow-workflow-builder/references/04-stage1-business-modeling.md +131 -0
  36. package/skills/qingflow-workflow-builder/references/05-stage2-members-roles.md +29 -0
  37. package/skills/qingflow-workflow-builder/references/06-stage3-build-spec.md +165 -0
  38. package/skills/qingflow-workflow-builder/references/07-stage4-validate-spec.md +33 -0
  39. package/skills/qingflow-workflow-builder/references/08-stage5-apply-verify.md +51 -0
  40. package/skills/qingflow-workflow-builder/references/09-stage6-summary.md +88 -0
  41. package/skills/qingflow-workflow-builder/references/10-node-config-reference.md +93 -0
  42. package/skills/qingflow-workflow-builder/references/11-troubleshooting.md +15 -0
  43. package/skills/qingflow-workflow-builder/scripts/diff_flow_spec.py +275 -0
  44. package/skills/qingflow-workflow-builder/scripts/validate_flow_spec.py +605 -0
  45. package/src/qingflow_mcp/__init__.py +1 -1
  46. package/src/qingflow_mcp/__main__.py +2 -6
  47. package/src/qingflow_mcp/builder_facade/models.py +108 -485
  48. package/src/qingflow_mcp/builder_facade/service.py +1686 -6674
  49. package/src/qingflow_mcp/cli/commands/builder.py +364 -322
  50. package/src/qingflow_mcp/cli/commands/chart.py +1 -1
  51. package/src/qingflow_mcp/cli/commands/common.py +3 -12
  52. package/src/qingflow_mcp/cli/commands/exports.py +2 -2
  53. package/src/qingflow_mcp/cli/commands/imports.py +3 -3
  54. package/src/qingflow_mcp/cli/commands/portal.py +2 -2
  55. package/src/qingflow_mcp/cli/commands/record.py +27 -101
  56. package/src/qingflow_mcp/cli/commands/task.py +47 -28
  57. package/src/qingflow_mcp/cli/commands/view.py +1 -1
  58. package/src/qingflow_mcp/cli/context.py +3 -0
  59. package/src/qingflow_mcp/cli/formatters.py +16 -784
  60. package/src/qingflow_mcp/cli/main.py +33 -117
  61. package/src/qingflow_mcp/errors.py +2 -43
  62. package/src/qingflow_mcp/public_surface.py +17 -26
  63. package/src/qingflow_mcp/response_trim.py +17 -81
  64. package/src/qingflow_mcp/server.py +12 -14
  65. package/src/qingflow_mcp/server_app_builder.py +36 -79
  66. package/src/qingflow_mcp/server_app_user.py +16 -22
  67. package/src/qingflow_mcp/session_store.py +7 -11
  68. package/src/qingflow_mcp/solution/compiler/__init__.py +1 -3
  69. package/src/qingflow_mcp/solution/executor.py +18 -245
  70. package/src/qingflow_mcp/tools/ai_builder_tools.py +1396 -3100
  71. package/src/qingflow_mcp/tools/app_tools.py +43 -184
  72. package/src/qingflow_mcp/tools/approval_tools.py +35 -197
  73. package/src/qingflow_mcp/tools/auth_tools.py +16 -92
  74. package/src/qingflow_mcp/tools/code_block_tools.py +40 -298
  75. package/src/qingflow_mcp/tools/custom_button_tools.py +10 -64
  76. package/src/qingflow_mcp/tools/directory_tools.py +72 -236
  77. package/src/qingflow_mcp/tools/export_tools.py +34 -244
  78. package/src/qingflow_mcp/tools/feedback_tools.py +0 -9
  79. package/src/qingflow_mcp/tools/file_tools.py +3 -9
  80. package/src/qingflow_mcp/tools/import_tools.py +49 -336
  81. package/src/qingflow_mcp/tools/navigation_tools.py +12 -91
  82. package/src/qingflow_mcp/tools/package_tools.py +6 -118
  83. package/src/qingflow_mcp/tools/portal_tools.py +3 -39
  84. package/src/qingflow_mcp/tools/qingbi_report_tools.py +7 -116
  85. package/src/qingflow_mcp/tools/record_tools.py +360 -1145
  86. package/src/qingflow_mcp/tools/resource_read_tools.py +39 -188
  87. package/src/qingflow_mcp/tools/role_tools.py +9 -80
  88. package/src/qingflow_mcp/tools/solution_tools.py +45 -59
  89. package/src/qingflow_mcp/tools/task_context_tools.py +158 -662
  90. package/src/qingflow_mcp/tools/task_tools.py +29 -113
  91. package/src/qingflow_mcp/tools/view_tools.py +3 -106
  92. package/src/qingflow_mcp/tools/workflow_tools.py +4 -48
  93. package/src/qingflow_mcp/tools/workspace_tools.py +3 -71
  94. package/npm/bin/qingflow-skills.mjs +0 -5
  95. package/skills/qingflow-app-builder/references/complete-system-development-guide.md +0 -172
  96. package/skills/qingflow-app-builder/references/single-app-development-guide.md +0 -87
  97. package/skills/qingflow-app-builder/scripts/validate_system_build_summary.py +0 -124
  98. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +0 -173
@@ -1,11 +1,94 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import argparse
4
- from copy import deepcopy
4
+ from typing import Any
5
5
 
6
6
  from ..context import CliContext
7
7
  from ..json_io import load_json_value
8
- from .common import load_list_arg, load_object_arg, parse_bool_text, raise_config_error, require_list_arg
8
+ from .common import load_list_arg, load_object_arg, raise_config_error, require_list_arg
9
+
10
+
11
+ def _parse_app_keys_arg(raw: Any) -> list[str] | None:
12
+ """Parse comma-separated --app-keys argument into a list, or return None if empty."""
13
+ if not raw:
14
+ return None
15
+ keys = [k.strip() for k in str(raw).split(",") if k.strip()]
16
+ return keys if keys else None
17
+
18
+
19
+ def _load_schema_apps_payload(path: str | None, *, cli_package_id: int | None) -> tuple[list[Any], int | None]:
20
+ """Load schema --apps-file.
21
+
22
+ Public docs recommend {"package_id": 123, "apps": [...]}; older payloads used
23
+ a raw apps array, and some agents accidentally wrapped the object in a
24
+ singleton array. Keep all three shapes on the same multi-app path.
25
+ """
26
+ if not path:
27
+ return [], cli_package_id
28
+ payload = load_json_value(path, option_name="--apps-file")
29
+ package_id = cli_package_id
30
+ if isinstance(payload, dict):
31
+ if package_id is None:
32
+ raw_package_id = payload.get("package_id")
33
+ if isinstance(raw_package_id, int):
34
+ package_id = raw_package_id
35
+ elif raw_package_id is not None and str(raw_package_id).strip().isdigit():
36
+ package_id = int(str(raw_package_id).strip())
37
+ apps = payload.get("apps")
38
+ elif isinstance(payload, list):
39
+ if len(payload) == 1 and isinstance(payload[0], dict) and "apps" in payload[0]:
40
+ wrapper = payload[0]
41
+ if package_id is None:
42
+ raw_package_id = wrapper.get("package_id")
43
+ if isinstance(raw_package_id, int):
44
+ package_id = raw_package_id
45
+ elif raw_package_id is not None and str(raw_package_id).strip().isdigit():
46
+ package_id = int(str(raw_package_id).strip())
47
+ apps = wrapper.get("apps")
48
+ elif any(isinstance(item, dict) and "apps" in item for item in payload):
49
+ raise_config_error(
50
+ "APPS_FILE_SHAPE_INVALID: --apps-file may be {package_id, apps}, a raw apps array, or a singleton wrapper array only.",
51
+ fix_hint='Use {"package_id": 123, "apps": [{"client_key": "main", "app_name": "主表", "icon": "database", "color": "blue", "add_fields": []}]}',
52
+ )
53
+ else:
54
+ apps = payload
55
+ else:
56
+ raise_config_error(
57
+ "APPS_FILE_SHAPE_INVALID: --apps-file must be {package_id, apps} or a JSON array of app items.",
58
+ fix_hint='Use {"package_id": 123, "apps": [{"client_key": "main", "app_name": "主表", "icon": "database", "color": "blue", "add_fields": []}]}',
59
+ )
60
+ if not isinstance(apps, list):
61
+ raise_config_error(
62
+ "APPS_FILE_SHAPE_INVALID: --apps-file apps must be a JSON array.",
63
+ fix_hint='Use {"package_id": 123, "apps": [{"client_key": "main", "app_name": "主表", "icon": "database", "color": "blue", "add_fields": []}]}',
64
+ )
65
+ return apps, package_id
66
+
67
+
68
+ def _load_views_payload(path: str | None) -> list[Any]:
69
+ """Load views --views-file.
70
+
71
+ Public docs recommend {"views": [...]}; accept a raw array as a small
72
+ compatibility convenience because most other builder file flags are arrays.
73
+ """
74
+ if not path:
75
+ return []
76
+ payload = load_json_value(path, option_name="--views-file")
77
+ if isinstance(payload, dict):
78
+ views = payload.get("views")
79
+ elif isinstance(payload, list):
80
+ views = payload
81
+ else:
82
+ raise_config_error(
83
+ "VIEWS_FILE_SHAPE_INVALID: --views-file must be {views: [...]} or a JSON array.",
84
+ fix_hint='Use {"views": [{"operation": "upsert", "app_key": "APP_KEY", "name": "业务视图", "type": "table", "columns": ["字段A"]}]}',
85
+ )
86
+ if not isinstance(views, list):
87
+ raise_config_error(
88
+ "VIEWS_FILE_SHAPE_INVALID: --views-file views must be a JSON array.",
89
+ fix_hint='Use {"views": [{"operation": "upsert", "app_key": "APP_KEY", "name": "业务视图", "type": "table", "columns": ["字段A"]}]}',
90
+ )
91
+ return views
9
92
 
10
93
 
11
94
  def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
@@ -22,7 +105,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
22
105
  file_upload_local.add_argument("--bucket-type")
23
106
  file_upload_local.add_argument("--path-id", type=int)
24
107
  file_upload_local.add_argument("--file-related-url")
25
- file_upload_local.set_defaults(handler=_handle_file_upload_local, format_hint="file_upload_local")
108
+ file_upload_local.set_defaults(handler=_handle_file_upload_local, format_hint="generic")
26
109
 
27
110
  feedback = builder_subparsers.add_parser("feedback", help="builder 侧反馈提交")
28
111
  feedback_subparsers = feedback.add_subparsers(dest="builder_feedback_command", required=True)
@@ -38,7 +121,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
38
121
  feedback_submit.add_argument("--record-id")
39
122
  feedback_submit.add_argument("--workflow-node-id")
40
123
  feedback_submit.add_argument("--note")
41
- feedback_submit.set_defaults(handler=_handle_feedback_submit, format_hint="feedback_submit")
124
+ feedback_submit.set_defaults(handler=_handle_feedback_submit, format_hint="generic")
42
125
 
43
126
  contract = builder_subparsers.add_parser("contract", help="读取 builder tool 合约")
44
127
  contract.add_argument("--tool-name", required=True)
@@ -52,7 +135,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
52
135
  member = builder_subparsers.add_parser("member", help="成员目录")
53
136
  member_subparsers = member.add_subparsers(dest="builder_member_command", required=True)
54
137
  member_search = member_subparsers.add_parser("search", help="搜索成员")
55
- member_search.add_argument("--query", required=True)
138
+ member_search.add_argument("--query", default="")
56
139
  member_search.add_argument("--page-num", type=int, default=1)
57
140
  member_search.add_argument("--page-size", type=int, default=20)
58
141
  member_search.add_argument("--contain-disable", action=argparse.BooleanOptionalAction, default=False)
@@ -61,7 +144,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
61
144
  role = builder_subparsers.add_parser("role", help="角色目录")
62
145
  role_subparsers = role.add_subparsers(dest="builder_role_command", required=True)
63
146
  role_search = role_subparsers.add_parser("search", help="搜索角色")
64
- role_search.add_argument("--keyword", required=True)
147
+ role_search.add_argument("--keyword", default="")
65
148
  role_search.add_argument("--page-num", type=int, default=1)
66
149
  role_search.add_argument("--page-size", type=int, default=20)
67
150
  role_search.set_defaults(handler=_handle_role_search, format_hint="builder_summary")
@@ -121,8 +204,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
121
204
  default="summary",
122
205
  )
123
206
  app_get.add_argument("--app-key", default="")
124
- app_get.add_argument("--app-keys", default="", help="逗号分隔的多应用 app_key;用于批量读取同一 section")
125
- app_get.add_argument("--app-keys-file", help="JSON 字符串数组;用于批量读取同一 section")
207
+ app_get.add_argument("--app-keys", help="逗号分隔的多应用批量读取,例如: KEY1,KEY2,KEY3")
126
208
  app_get.set_defaults(handler=_handle_app_get, format_hint="builder_summary")
127
209
 
128
210
  app_repair_code_blocks = app_subparsers.add_parser("repair-code-blocks", help="扫描或修复现有代码块配置")
@@ -139,35 +221,34 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
139
221
 
140
222
  button_get = button_subparsers.add_parser("get", help="读取应用自定义按钮配置")
141
223
  button_get.add_argument("--app-key", default="")
142
- button_get.add_argument("--app-keys", default="", help="逗号分隔的多应用 app_key;用于批量读取按钮配置")
143
- button_get.add_argument("--app-keys-file", help="JSON 字符串数组;用于批量读取按钮配置")
224
+ button_get.add_argument("--app-keys", help="逗号分隔的多应用批量读取,例如: KEY1,KEY2,KEY3")
144
225
  button_get.set_defaults(handler=_handle_button_get, format_hint="builder_summary")
145
226
 
146
227
  button_apply = button_subparsers.add_parser("apply", help="声明式创建、更新或删除自定义按钮")
147
228
  button_apply.add_argument("--app-key", default="")
148
- button_apply.add_argument("--apps-file", help="多应用按钮配置 JSON 数组;每项包含 app_key 和按钮 payload")
149
229
  button_apply.add_argument("--upsert-buttons-file")
150
230
  button_apply.add_argument("--patch-buttons-file")
151
231
  button_apply.add_argument("--remove-buttons-file")
152
232
  button_apply.add_argument("--view-configs-file")
233
+ button_apply.add_argument("--apps-file", help="多应用批量按钮 JSON 数组,每项含 app_key + upsert_buttons/patch_buttons/remove_buttons/view_configs")
153
234
  button_apply.set_defaults(handler=_handle_button_apply, format_hint="builder_summary", force_json_output=True)
154
235
 
155
236
  associated_resource = builder_subparsers.add_parser("associated-resource", aliases=["associated-resources"], help="关联视图/报表")
156
237
  associated_resource_subparsers = associated_resource.add_subparsers(dest="builder_associated_resource_command", required=True)
238
+
157
239
  associated_resource_get = associated_resource_subparsers.add_parser("get", help="读取应用关联资源配置")
158
240
  associated_resource_get.add_argument("--app-key", default="")
159
- associated_resource_get.add_argument("--app-keys", default="", help="逗号分隔的多应用 app_key;用于批量读取关联资源配置")
160
- associated_resource_get.add_argument("--app-keys-file", help="JSON 字符串数组;用于批量读取关联资源配置")
241
+ associated_resource_get.add_argument("--app-keys", help="逗号分隔的多应用批量读取,例如: KEY1,KEY2,KEY3")
161
242
  associated_resource_get.set_defaults(handler=_handle_associated_resource_get, format_hint="builder_summary")
162
243
 
163
244
  associated_resource_apply = associated_resource_subparsers.add_parser("apply", help="声明式管理应用关联资源池和视图展示配置")
164
245
  associated_resource_apply.add_argument("--app-key", default="")
165
- associated_resource_apply.add_argument("--apps-file", help="多应用关联资源配置 JSON 数组;每项包含 app_key 和资源 payload")
166
246
  associated_resource_apply.add_argument("--upsert-resources-file")
167
247
  associated_resource_apply.add_argument("--patch-resources-file")
168
248
  associated_resource_apply.add_argument("--remove-associated-item-ids-file")
169
249
  associated_resource_apply.add_argument("--reorder-associated-item-ids-file")
170
250
  associated_resource_apply.add_argument("--view-configs-file")
251
+ associated_resource_apply.add_argument("--apps-file", help="多应用批量关联资源 JSON 数组,每项含 app_key + upsert_resources/patch_resources/remove_associated_item_ids/reorder_associated_item_ids/view_configs")
171
252
  associated_resource_apply.set_defaults(handler=_handle_associated_resource_apply, format_hint="builder_summary", force_json_output=True)
172
253
 
173
254
  portal = builder_subparsers.add_parser("portal", help="门户")
@@ -195,13 +276,9 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
195
276
  portal_apply.add_argument("--hide-copyright", action=argparse.BooleanOptionalAction, default=None)
196
277
  portal_apply.add_argument("--dash-global-config-file")
197
278
  portal_apply.add_argument("--config-file")
198
- portal_apply.add_argument("--patch-sections-file", help="门户组件局部更新 JSON 数组,每项含 chart_ref/view_ref/order + set/unset")
279
+ portal_apply.add_argument("--patch-sections-file", help="门户组件局部更新 JSON 数组,每项含 chart 或 chart_ref/view_ref/order + set/unset")
199
280
  portal_apply.set_defaults(handler=_handle_portal_apply, format_hint="builder_summary", force_json_output=True)
200
281
 
201
- portal_delete = portal_subparsers.add_parser("delete", help="删除门户")
202
- portal_delete.add_argument("--dash-key", required=True)
203
- portal_delete.set_defaults(handler=_handle_portal_delete, format_hint="builder_summary", force_json_output=True)
204
-
205
282
  schema_apply = builder_subparsers.add_parser("schema", help="字段搭建")
206
283
  schema_apply_subparsers = schema_apply.add_subparsers(dest="builder_schema_command", required=True)
207
284
  schema_apply_apply = schema_apply_subparsers.add_parser("apply", help="执行字段变更")
@@ -212,9 +289,9 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
212
289
  schema_apply_apply.add_argument("--icon")
213
290
  schema_apply_apply.add_argument("--color")
214
291
  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[][] 同时声明字段和表单分组布局")
292
+ schema_apply_apply.add_argument("--create-if-missing", action="store_true")
293
+ schema_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
294
+ schema_apply_apply.add_argument("--apps-file", help="多应用 schema JSON;推荐对象 {package_id, apps},兼容 raw apps 数组;支持 relation target_app_ref")
218
295
  schema_apply_apply.add_argument("--add-fields-file", help="字段 JSON 数组;字段可用 as_data_title/as_data_cover 标记数据标题/封面")
219
296
  schema_apply_apply.add_argument("--update-fields-file", help="字段更新 JSON 数组;set 内可用 as_data_title/as_data_cover 标记数据标题/封面")
220
297
  schema_apply_apply.add_argument("--remove-fields-file")
@@ -224,63 +301,61 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
224
301
  layout_apply_subparsers = layout_apply.add_subparsers(dest="builder_layout_command", required=True)
225
302
  layout_apply_apply = layout_apply_subparsers.add_parser("apply", help="执行布局变更")
226
303
  layout_apply_apply.add_argument("--app-key", default="")
227
- layout_apply_apply.add_argument("--apps-file", help="多应用布局 JSON 数组;每项包含 app_key/mode?/publish?/sections")
228
304
  layout_apply_apply.add_argument("--mode", choices=["merge", "replace"], default="merge")
229
305
  layout_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
230
306
  layout_apply_apply.add_argument("--sections-file")
307
+ layout_apply_apply.add_argument("--apps-file", help="多应用批量布局 JSON 数组,每项含 app_key + sections")
231
308
  layout_apply_apply.set_defaults(handler=_handle_layout_apply, format_hint="builder_summary", force_json_output=True)
232
309
 
233
310
  views_apply = builder_subparsers.add_parser("views", help="视图")
234
311
  views_apply_subparsers = views_apply.add_subparsers(dest="builder_views_command", required=True)
235
312
  views_apply_apply = views_apply_subparsers.add_parser("apply", help="执行视图变更")
236
313
  views_apply_apply.add_argument("--app-key", default="")
237
- views_apply_apply.add_argument("--apps-file", help="多应用视图 JSON 数组;每项包含 app_key 和 upsert/patch/remove payload")
238
314
  views_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
239
315
  views_apply_apply.add_argument("--upsert-views-file")
240
316
  views_apply_apply.add_argument("--patch-views-file")
241
317
  views_apply_apply.add_argument("--remove-views-file")
318
+ views_apply_apply.add_argument("--views-file", help="推荐:扁平批量视图 JSON;对象 {views:[{operation, app_key, ...}]},同 app/跨 app 均可")
319
+ views_apply_apply.add_argument("--apps-file", help="多应用批量视图 JSON 数组,每项含 app_key + upsert_views/patch_views/remove_views")
242
320
  views_apply_apply.set_defaults(handler=_handle_views_apply, format_hint="builder_summary", force_json_output=True)
243
321
 
244
- flow_apply = builder_subparsers.add_parser("flow", help="流程")
245
- flow_apply_subparsers = flow_apply.add_subparsers(dest="builder_flow_command", required=True)
246
- flow_schema = flow_apply_subparsers.add_parser("schema", help="读取 WorkflowSpec JSON Schema")
322
+ flow_parser = builder_subparsers.add_parser("flow", help="流程(WorkflowSpec)")
323
+ flow_subparsers = flow_parser.add_subparsers(dest="builder_flow_command", required=True)
324
+
325
+ flow_schema = flow_subparsers.add_parser("schema", help="读取 WorkflowSpec JSON Schema")
247
326
  flow_schema.add_argument("--schema-version", default="")
248
327
  flow_schema.set_defaults(handler=_handle_flow_schema, format_hint="builder_summary")
249
328
 
250
- flow_get = flow_apply_subparsers.add_parser("get", help="读取 WorkflowSpec")
329
+ flow_get = flow_subparsers.add_parser("get", help="读取 WorkflowSpec")
251
330
  flow_get.add_argument("--app-key", required=True)
252
331
  flow_get.add_argument("--version-id", default="")
253
332
  flow_get.set_defaults(handler=_handle_flow_get, format_hint="builder_summary")
254
333
 
255
- flow_apply_apply = flow_apply_subparsers.add_parser("apply", help="执行流程变更")
256
- flow_apply_apply.add_argument("--app-key", required=True)
257
- flow_apply_apply.add_argument("--mode", choices=["replace"], default="replace")
258
- flow_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
259
- flow_apply_apply.add_argument("--nodes-file")
260
- flow_apply_apply.add_argument("--transitions-file")
261
- flow_apply_apply.add_argument("--spec-file")
262
- flow_apply_apply.add_argument("--patch-nodes-file", help="节点局部更新 JSON 数组,每项含 id + set/unset")
263
- flow_apply_apply.add_argument("--idempotency-key", default="")
264
- flow_apply_apply.add_argument("--schema-version", default="")
265
- flow_apply_apply.set_defaults(handler=_handle_flow_apply, format_hint="builder_summary", force_json_output=True)
334
+ flow_apply = flow_subparsers.add_parser("apply", help="应用 WorkflowSpec")
335
+ flow_apply.add_argument("--app-key", required=True)
336
+ flow_apply.add_argument("--spec-file")
337
+ flow_apply.add_argument("--patch-nodes-file", help="节点局部更新 JSON 数组,每项含 id + set/unset")
338
+ flow_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
339
+ flow_apply.add_argument("--idempotency-key", default="")
340
+ flow_apply.add_argument("--schema-version", default="")
341
+ flow_apply.set_defaults(handler=_handle_flow_apply, format_hint="builder_summary", force_json_output=True)
266
342
 
267
343
  charts_apply = builder_subparsers.add_parser("charts", help="报表")
268
344
  charts_apply_subparsers = charts_apply.add_subparsers(dest="builder_charts_command", required=True)
269
345
  charts_apply_apply = charts_apply_subparsers.add_parser("apply", help="执行报表变更")
270
346
  charts_apply_apply.add_argument("--app-key", default="")
271
- charts_apply_apply.add_argument("--apps-file", help="多应用报表 JSON 数组;每项包含 app_key 和 upsert/patch/remove/reorder payload")
272
347
  charts_apply_apply.add_argument("--upsert-file")
273
348
  charts_apply_apply.add_argument("--patch-file")
274
349
  charts_apply_apply.add_argument("--remove-chart-ids-file")
275
350
  charts_apply_apply.add_argument("--reorder-chart-ids-file")
351
+ charts_apply_apply.add_argument("--apps-file", help="多应用批量报表 JSON 数组,每项含 app_key + upsert_charts/patch_charts/remove_chart_ids/reorder_chart_ids")
276
352
  charts_apply_apply.set_defaults(handler=_handle_charts_apply, format_hint="builder_summary", force_json_output=True)
277
353
 
278
354
  publish_verify = builder_subparsers.add_parser("publish", help="发布校验")
279
355
  publish_verify_subparsers = publish_verify.add_subparsers(dest="builder_publish_command", required=True)
280
356
  publish_verify_verify = publish_verify_subparsers.add_parser("verify", help="校验应用发布")
281
357
  publish_verify_verify.add_argument("--app-key", default="")
282
- publish_verify_verify.add_argument("--app-keys", default="", help="逗号分隔的多应用 app_key;用于批量发布校验")
283
- publish_verify_verify.add_argument("--app-keys-file", help="JSON 字符串数组;用于批量发布校验")
358
+ publish_verify_verify.add_argument("--app-keys", help="逗号分隔的多应用批量校验,例如: KEY1,KEY2,KEY3")
284
359
  publish_verify_verify.add_argument("--expected-package-id", type=int)
285
360
  publish_verify_verify.set_defaults(handler=_handle_publish_verify, format_hint="builder_summary", force_json_output=True)
286
361
 
@@ -385,7 +460,6 @@ def _handle_package_apply(args: argparse.Namespace, context: CliContext) -> dict
385
460
  config = load_object_arg(args.config_file, option_name="--config-file")
386
461
  if not isinstance(config, dict):
387
462
  raise_config_error("package apply --config-file must contain a JSON object.")
388
- config.pop("create_if_missing", None)
389
463
  return context.builder.package_apply(profile=args.profile, **config)
390
464
 
391
465
 
@@ -429,9 +503,26 @@ def _handle_button_catalog(args: argparse.Namespace, context: CliContext) -> dic
429
503
 
430
504
 
431
505
  def _handle_button_apply(args: argparse.Namespace, context: CliContext) -> dict:
432
- apps = load_list_arg(args.apps_file, option_name="--apps-file") if getattr(args, "apps_file", None) else None
433
- if apps is None and not (args.app_key or "").strip():
434
- raise_config_error("builder button apply requires --app-key unless --apps-file is provided")
506
+ apps = load_list_arg(getattr(args, "apps_file", None), option_name="--apps-file")
507
+ if apps:
508
+ single_app_args = [a for a in ["--app-key" if args.app_key else None, "--upsert-buttons-file" if getattr(args, "upsert_buttons_file", None) else None, "--patch-buttons-file" if getattr(args, "patch_buttons_file", None) else None, "--remove-buttons-file" if getattr(args, "remove_buttons_file", None) else None, "--view-configs-file" if getattr(args, "view_configs_file", None) else None] if a]
509
+ if single_app_args:
510
+ raise_config_error(
511
+ f"button apply --apps-file cannot be combined with {', '.join(single_app_args)}.",
512
+ fix_hint="Use --apps-file for batch mode (each item contains app_key + per-app params), or remove --apps-file for single-app mode.",
513
+ )
514
+ if apps:
515
+ return context.builder.app_custom_buttons_apply(
516
+ profile=args.profile,
517
+ app_key="",
518
+ upsert_buttons=[],
519
+ patch_buttons=[],
520
+ remove_buttons=[],
521
+ view_configs=[],
522
+ apps=apps,
523
+ )
524
+ if not args.app_key:
525
+ raise_config_error("button apply requires --app-key or --apps-file", fix_hint="Pass --app-key APP_KEY for single-app mode, or --apps-file for batch mode.")
435
526
  return context.builder.app_custom_buttons_apply(
436
527
  profile=args.profile,
437
528
  app_key=args.app_key,
@@ -439,14 +530,40 @@ def _handle_button_apply(args: argparse.Namespace, context: CliContext) -> dict:
439
530
  patch_buttons=load_list_arg(args.patch_buttons_file, option_name="--patch-buttons-file"),
440
531
  remove_buttons=load_list_arg(args.remove_buttons_file, option_name="--remove-buttons-file"),
441
532
  view_configs=load_list_arg(args.view_configs_file, option_name="--view-configs-file"),
442
- apps=apps,
443
533
  )
444
534
 
445
535
 
446
536
  def _handle_associated_resource_apply(args: argparse.Namespace, context: CliContext) -> dict:
447
- apps = load_list_arg(args.apps_file, option_name="--apps-file") if getattr(args, "apps_file", None) else None
448
- if apps is None and not (args.app_key or "").strip():
449
- raise_config_error("builder associated-resource apply requires --app-key unless --apps-file is provided")
537
+ apps = load_list_arg(getattr(args, "apps_file", None), option_name="--apps-file")
538
+ if apps:
539
+ single_app_args = [
540
+ a for a in [
541
+ "--app-key" if args.app_key else None,
542
+ "--upsert-resources-file" if getattr(args, "upsert_resources_file", None) else None,
543
+ "--patch-resources-file" if getattr(args, "patch_resources_file", None) else None,
544
+ "--remove-associated-item-ids-file" if getattr(args, "remove_associated_item_ids_file", None) else None,
545
+ "--reorder-associated-item-ids-file" if getattr(args, "reorder_associated_item_ids_file", None) else None,
546
+ "--view-configs-file" if getattr(args, "view_configs_file", None) else None,
547
+ ] if a
548
+ ]
549
+ if single_app_args:
550
+ raise_config_error(
551
+ f"associated-resource apply --apps-file cannot be combined with {', '.join(single_app_args)}.",
552
+ fix_hint="Use --apps-file for batch mode (each item contains app_key + per-app params), or remove --apps-file for single-app mode.",
553
+ )
554
+ if apps:
555
+ return context.builder.app_associated_resources_apply(
556
+ profile=args.profile,
557
+ app_key="",
558
+ upsert_resources=[],
559
+ patch_resources=[],
560
+ remove_associated_item_ids=[],
561
+ reorder_associated_item_ids=[],
562
+ view_configs=[],
563
+ apps=apps,
564
+ )
565
+ if not args.app_key:
566
+ raise_config_error("associated-resource apply requires --app-key or --apps-file", fix_hint="Pass --app-key APP_KEY for single-app mode, or --apps-file for batch mode.")
450
567
  return context.builder.app_associated_resources_apply(
451
568
  profile=args.profile,
452
569
  app_key=args.app_key,
@@ -455,26 +572,27 @@ def _handle_associated_resource_apply(args: argparse.Namespace, context: CliCont
455
572
  remove_associated_item_ids=load_list_arg(args.remove_associated_item_ids_file, option_name="--remove-associated-item-ids-file"),
456
573
  reorder_associated_item_ids=load_list_arg(args.reorder_associated_item_ids_file, option_name="--reorder-associated-item-ids-file"),
457
574
  view_configs=load_list_arg(args.view_configs_file, option_name="--view-configs-file"),
458
- apps=apps,
459
575
  )
460
576
 
461
577
 
462
578
  def _handle_button_get(args: argparse.Namespace, context: CliContext) -> dict:
463
- app_keys = _app_keys_from_args(args)
464
- if app_keys is not None:
465
- return context.builder.app_get_buttons(profile=args.profile, app_key="", app_keys=app_keys)
466
- if not (args.app_key or "").strip():
467
- raise_config_error("builder button get requires --app-key unless --app-keys or --app-keys-file is provided")
468
- return context.builder.app_get_buttons(profile=args.profile, app_key=args.app_key)
579
+ app_keys = _parse_app_keys_arg(getattr(args, "app_keys", None))
580
+ if app_keys:
581
+ return context.builder.app_get_buttons(profile=args.profile, app_keys=app_keys)
582
+ app_key = str(getattr(args, "app_key", "") or "").strip()
583
+ if not app_key:
584
+ raise_config_error("button get requires --app-key or --app-keys", fix_hint="Pass --app-key APP_KEY or --app-keys KEY1,KEY2")
585
+ return context.builder.app_get_buttons(profile=args.profile, app_key=app_key)
469
586
 
470
587
 
471
588
  def _handle_associated_resource_get(args: argparse.Namespace, context: CliContext) -> dict:
472
- app_keys = _app_keys_from_args(args)
473
- if app_keys is not None:
474
- return context.builder.app_get_associated_resources(profile=args.profile, app_key="", app_keys=app_keys)
475
- if not (args.app_key or "").strip():
476
- raise_config_error("builder associated-resource get requires --app-key unless --app-keys or --app-keys-file is provided")
477
- return context.builder.app_get_associated_resources(profile=args.profile, app_key=args.app_key)
589
+ app_keys = _parse_app_keys_arg(getattr(args, "app_keys", None))
590
+ if app_keys:
591
+ return context.builder.app_get_associated_resources(profile=args.profile, app_keys=app_keys)
592
+ app_key = str(getattr(args, "app_key", "") or "").strip()
593
+ if not app_key:
594
+ raise_config_error("associated-resource get requires --app-key or --app-keys", fix_hint="Pass --app-key APP_KEY or --app-keys KEY1,KEY2")
595
+ return context.builder.app_get_associated_resources(profile=args.profile, app_key=app_key)
478
596
 
479
597
 
480
598
  def _handle_button_create(args: argparse.Namespace, context: CliContext) -> dict:
@@ -498,18 +616,32 @@ def _handle_button_delete(args: argparse.Namespace, context: CliContext) -> dict
498
616
  return context.builder.app_custom_button_delete(profile=args.profile, app_key=args.app_key, button_id=args.button_id)
499
617
 
500
618
 
501
- def _app_keys_from_args(args: argparse.Namespace) -> list[str] | None:
502
- keys: list[str] = []
503
- raw = str(getattr(args, "app_keys", "") or "").strip()
504
- if raw:
505
- keys.extend(part.strip() for part in raw.split(",") if part.strip())
506
- keys_file = getattr(args, "app_keys_file", None)
507
- if keys_file:
508
- keys.extend(str(item).strip() for item in require_list_arg(keys_file, option_name="--app-keys-file") if str(item).strip())
509
- return keys or None
510
-
511
-
512
619
  def _handle_app_get(args: argparse.Namespace, context: CliContext) -> dict:
620
+ section = args.builder_app_get_section
621
+ app_keys = _parse_app_keys_arg(getattr(args, "app_keys", None))
622
+ app_key = str(getattr(args, "app_key", "") or "").strip()
623
+ if not app_keys and not app_key:
624
+ raise_config_error("app get requires --app-key or --app-keys", fix_hint="Pass --app-key APP_KEY or --app-keys KEY1,KEY2")
625
+ if section == "buttons":
626
+ if app_keys:
627
+ return context.builder.app_get_buttons(profile=args.profile, app_keys=app_keys)
628
+ return context.builder.app_get_buttons(profile=args.profile, app_key=app_key)
629
+ if section == "associated-resources":
630
+ if app_keys:
631
+ return context.builder.app_get_associated_resources(profile=args.profile, app_keys=app_keys)
632
+ return context.builder.app_get_associated_resources(profile=args.profile, app_key=app_key)
633
+ if app_keys:
634
+ batch_handlers = {
635
+ "summary": context.builder.app_get,
636
+ "fields": context.builder.app_get_fields,
637
+ "layout": context.builder.app_get_layout,
638
+ "views": context.builder.app_get_views,
639
+ "flow": context.builder.app_get_flow,
640
+ "charts": context.builder.app_get_charts,
641
+ }
642
+ if section not in batch_handlers:
643
+ raise_config_error(f"app get --app-keys does not support section '{section}'", fix_hint="Batch reads support: summary, fields, layout, views, flow, charts, buttons, associated-resources")
644
+ return batch_handlers[section](profile=args.profile, app_keys=app_keys)
513
645
  handlers = {
514
646
  "summary": context.builder.app_get,
515
647
  "fields": context.builder.app_get_fields,
@@ -517,15 +649,8 @@ def _handle_app_get(args: argparse.Namespace, context: CliContext) -> dict:
517
649
  "views": context.builder.app_get_views,
518
650
  "flow": context.builder.app_get_flow,
519
651
  "charts": context.builder.app_get_charts,
520
- "buttons": context.builder.app_get_buttons,
521
- "associated-resources": context.builder.app_get_associated_resources,
522
652
  }
523
- app_keys = _app_keys_from_args(args)
524
- if app_keys is not None:
525
- return handlers[args.builder_app_get_section](profile=args.profile, app_key="", app_keys=app_keys)
526
- if not (args.app_key or "").strip():
527
- raise_config_error("builder app get requires --app-key unless --app-keys or --app-keys-file is provided")
528
- return handlers[args.builder_app_get_section](profile=args.profile, app_key=args.app_key)
653
+ return handlers[section](profile=args.profile, app_key=app_key)
529
654
 
530
655
 
531
656
  def _handle_app_repair_code_blocks(args: argparse.Namespace, context: CliContext) -> dict:
@@ -554,66 +679,49 @@ def _handle_chart_get(args: argparse.Namespace, context: CliContext) -> dict:
554
679
 
555
680
 
556
681
  def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
557
- apps_payload = _load_apps_file_arg(args.apps_file)
558
- apps = apps_payload["apps"]
559
- apps_warnings = apps_payload.get("warnings") or []
560
- package_id = args.package_id
561
- if package_id is None and apps_payload.get("package_id") is not None:
562
- package_id = _coerce_apps_file_package_id(apps_payload["package_id"])
682
+ apps, apps_package_id = _load_schema_apps_payload(args.apps_file, cli_package_id=args.package_id)
563
683
  if args.apps_file:
564
- file_publish = _coerce_apps_file_bool(
565
- apps_payload.get("publish"),
566
- field_name="publish",
567
- default=True,
568
- )
569
- effective_publish = bool(args.publish if args.publish is not None else file_publish)
570
684
  if not apps:
571
685
  raise_config_error(
572
686
  "schema apply multi-app mode requires a non-empty --apps-file.",
573
- fix_hint="Pass a JSON array, or a JSON object like {\"package_id\":1001,\"apps\":[...]} with at least one app item.",
574
- error_code="APPS_FILE_EMPTY",
687
+ fix_hint='Pass {"package_id": 123, "apps": [{"client_key": "main", "app_name": "主表", "icon": "database", "color": "blue", "add_fields": []}]}',
575
688
  )
576
- if args.app_key or args.app_name or args.app_title or args.form_file or args.add_fields_file or args.update_fields_file or args.remove_fields_file:
689
+ if args.app_key or args.app_name or args.app_title or args.add_fields_file or args.update_fields_file or args.remove_fields_file:
577
690
  raise_config_error(
578
- "schema apply multi-app mode accepts --package-id plus --apps-file only.",
579
- fix_hint="Use apps[].form inside --apps-file for batch mode, or remove --apps-file and use --form-file for a single app.",
691
+ "schema apply multi-app mode accepts --package-id/--create-if-missing plus --apps-file only.",
692
+ fix_hint="Use `--apps-file` for batch mode, or remove `--apps-file` and use the single-app arguments.",
580
693
  )
581
- if package_id is None:
694
+ if apps_package_id is None:
582
695
  raise_config_error(
583
696
  "schema apply multi-app mode requires --package-id.",
584
- fix_hint="Pass `--package-id`, or put `package_id` at the top level of --apps-file. `package_name` alone does not create the package here; run `builder package apply` first.",
697
+ fix_hint="Pass root `--package-id`, or include package_id in the --apps-file object.",
585
698
  )
586
- result = context.builder.app_schema_apply(
699
+ return context.builder.app_schema_apply(
587
700
  profile=args.profile,
588
- package_id=package_id,
701
+ package_id=apps_package_id,
589
702
  visibility=load_object_arg(args.visibility_file, option_name="--visibility-file"),
590
- publish=effective_publish,
703
+ create_if_missing=bool(args.create_if_missing),
704
+ publish=bool(args.publish),
591
705
  apps=apps,
592
- form=None,
593
706
  add_fields=[],
594
707
  update_fields=[],
595
708
  remove_fields=[],
596
709
  )
597
- if apps_warnings and isinstance(result, dict):
598
- result_warnings = list(result.get("warnings") or [])
599
- result_warnings.extend(deepcopy(apps_warnings))
600
- result["warnings"] = result_warnings
601
- return result
602
710
  has_app_key = bool((args.app_key or "").strip())
603
711
  has_app_name = bool((args.app_name or "").strip())
604
712
  has_app_title = bool((args.app_title or "").strip())
605
713
  has_package_id = args.package_id is not None
606
714
  if has_app_key:
607
- if has_package_id:
715
+ if args.create_if_missing or has_package_id:
608
716
  raise_config_error(
609
717
  "schema apply edit mode accepts --app-key and optional --app-name only.",
610
- fix_hint="For existing apps, use `--app-key` and optionally `--app-name`. For create mode, use `--package-id --app-name`.",
718
+ fix_hint="For existing apps, use `--app-key` and optionally `--app-name`. For create mode, use `--package-id --app-name --create-if-missing`.",
611
719
  )
612
720
  else:
613
- if not has_package_id or not (has_app_name or has_app_title):
721
+ if not args.create_if_missing or not has_package_id or not (has_app_name or has_app_title):
614
722
  raise_config_error(
615
- "schema apply create mode requires --package-id and --app-name.",
616
- fix_hint="Use `--app-key` for existing apps, or pass `--package-id --app-name` to create a new app.",
723
+ "schema apply create mode requires --package-id, --app-name, and --create-if-missing.",
724
+ fix_hint="Use `--app-key` for existing apps, or pass `--package-id --app-name --create-if-missing` to create a new app.",
617
725
  )
618
726
  return context.builder.app_schema_apply(
619
727
  profile=args.profile,
@@ -624,175 +732,92 @@ def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
624
732
  icon=args.icon,
625
733
  color=args.color,
626
734
  visibility=load_object_arg(args.visibility_file, option_name="--visibility-file"),
627
- publish=True if args.publish is None else bool(args.publish),
628
- form=load_list_arg(args.form_file, option_name="--form-file") if args.form_file else [],
735
+ create_if_missing=bool(args.create_if_missing),
736
+ publish=bool(args.publish),
629
737
  add_fields=load_list_arg(args.add_fields_file, option_name="--add-fields-file"),
630
738
  update_fields=load_list_arg(args.update_fields_file, option_name="--update-fields-file"),
631
739
  remove_fields=load_list_arg(args.remove_fields_file, option_name="--remove-fields-file"),
632
740
  )
633
741
 
634
742
 
635
- def _load_apps_file_arg(path: str | None) -> dict[str, object]:
636
- if not path:
637
- return {"apps": []}
638
- expected_shape = {
639
- "package_id": 1001,
640
- "apps": [
641
- {
642
- "client_key": "employee",
643
- "app_name": "员工花名册",
644
- "icon": "business-personalcard",
645
- "color": "emerald",
646
- "add_fields": [],
647
- }
648
- ],
649
- }
650
- expected_shape_details = {
651
- "expected_shape": expected_shape,
652
- "expected_shape_json": (
653
- '{"package_id":1001,"apps":[{"client_key":"employee","app_name":"员工花名册",'
654
- '"icon":"business-personalcard","color":"emerald","add_fields":[]}]}'
655
- ),
656
- }
657
- payload = load_json_value(path, option_name="--apps-file")
658
- if isinstance(payload, list):
659
- if len(payload) == 1 and isinstance(payload[0], dict) and "apps" in payload[0]:
660
- wrapper = payload[0]
661
- apps = wrapper.get("apps")
662
- if not isinstance(apps, list):
663
- raise_config_error(
664
- "--apps-file wrapper object requires an apps array.",
665
- fix_hint="Use {\"package_id\":1001,\"apps\":[...]} or pass a raw apps array like [{\"app_name\":\"...\",\"icon\":\"...\",\"color\":\"...\",\"add_fields\":[...]}].",
666
- error_code="APPS_FILE_SHAPE_INVALID",
667
- details=expected_shape_details,
668
- )
669
- result: dict[str, object] = {
670
- "apps": apps,
671
- "warnings": [
672
- {
673
- "code": "APPS_FILE_WRAPPER_ARRAY_UNWRAPPED",
674
- "message": "--apps-file was a singleton wrapper array; normalized it to {package_id, apps}.",
675
- }
676
- ],
677
- }
678
- if wrapper.get("package_id") is not None:
679
- result["package_id"] = wrapper.get("package_id")
680
- if wrapper.get("publish") is not None:
681
- result["publish"] = wrapper.get("publish")
682
- return result
683
- if any(isinstance(item, dict) and "apps" in item for item in payload):
684
- raise_config_error(
685
- "--apps-file root array items must be app items, not package wrapper objects.",
686
- fix_hint="Use one object {\"package_id\":1001,\"apps\":[...]} or a raw app array [{\"app_name\":\"...\",\"icon\":\"...\",\"color\":\"...\",\"add_fields\":[...]}]. Do not wrap multiple {package_id, apps} objects in an array.",
687
- error_code="APPS_FILE_SHAPE_INVALID",
688
- details=expected_shape_details,
689
- )
690
- return {"apps": payload}
691
- if isinstance(payload, dict):
692
- apps = payload.get("apps")
693
- if not isinstance(apps, list):
743
+ def _handle_layout_apply(args: argparse.Namespace, context: CliContext) -> dict:
744
+ apps = load_list_arg(getattr(args, "apps_file", None), option_name="--apps-file")
745
+ if apps:
746
+ single_app_args = [a for a in ["--app-key" if args.app_key else None, "--sections-file" if getattr(args, "sections_file", None) else None] if a]
747
+ if single_app_args:
694
748
  raise_config_error(
695
- "--apps-file JSON object requires an apps array.",
696
- fix_hint="Use {\"package_id\":1001,\"apps\":[...]} or pass a raw JSON array.",
697
- error_code="APPS_FILE_SHAPE_INVALID",
698
- details=expected_shape_details,
749
+ f"layout apply --apps-file cannot be combined with {', '.join(single_app_args)}.",
750
+ fix_hint="Use --apps-file for batch mode (each item contains app_key + sections), or remove --apps-file for single-app mode.",
699
751
  )
700
- result: dict[str, object] = {"apps": apps}
701
- if payload.get("package_id") is not None:
702
- result["package_id"] = payload.get("package_id")
703
- if payload.get("publish") is not None:
704
- result["publish"] = payload.get("publish")
705
- return result
706
- raise_config_error(
707
- "--apps-file must be a JSON array or an object containing apps.",
708
- fix_hint="Use [{...}] or {\"package_id\":1001,\"apps\":[...]}",
709
- error_code="APPS_FILE_SHAPE_INVALID",
710
- details=expected_shape_details,
711
- )
712
-
713
-
714
- def _coerce_apps_file_package_id(value: object) -> int:
715
- package_id: int
716
- if isinstance(value, bool):
717
- _raise_apps_file_package_id_invalid(value)
718
- if isinstance(value, int):
719
- package_id = value
720
- elif isinstance(value, str):
721
- stripped = value.strip()
722
- try:
723
- package_id = int(stripped)
724
- except ValueError:
725
- _raise_apps_file_package_id_invalid(value)
726
- else:
727
- _raise_apps_file_package_id_invalid(value)
728
- if package_id <= 0:
729
- _raise_apps_file_package_id_invalid(value)
730
- return package_id
731
-
732
-
733
- def _raise_apps_file_package_id_invalid(value: object) -> None:
734
- raise_config_error(
735
- "--apps-file package_id must be a positive integer.",
736
- fix_hint='Use a numeric package_id, for example {"package_id":1001,"apps":[...]}.',
737
- error_code="APPS_FILE_PACKAGE_ID_INVALID",
738
- details={
739
- "field": "package_id",
740
- "value": value,
741
- "expected": "positive integer or numeric string",
742
- },
743
- )
744
-
745
-
746
- def _coerce_apps_file_bool(value: object, *, field_name: str, default: bool) -> bool:
747
- if value is None:
748
- return default
749
- if isinstance(value, bool):
750
- return value
751
- if isinstance(value, str):
752
- try:
753
- return parse_bool_text(value)
754
- except argparse.ArgumentTypeError:
755
- pass
756
- raise_config_error(
757
- f"--apps-file {field_name} must be a boolean.",
758
- fix_hint=f'Use "{field_name}": true or "{field_name}": false in --apps-file.',
759
- error_code="APPS_FILE_BOOLEAN_INVALID",
760
- details={
761
- "field": field_name,
762
- "value": value,
763
- "expected": "boolean true/false or string true/false/1/0/yes/no",
764
- },
765
- )
766
-
767
-
768
- def _handle_layout_apply(args: argparse.Namespace, context: CliContext) -> dict:
769
- apps = load_list_arg(args.apps_file, option_name="--apps-file") if getattr(args, "apps_file", None) else None
770
- if apps is None and not (args.app_key or "").strip():
771
- raise_config_error("builder layout apply requires --app-key unless --apps-file is provided")
772
- if apps is None and not getattr(args, "sections_file", None):
773
- raise_config_error(
774
- "--sections-file is required",
775
- fix_hint="Pass --sections-file for single-app layout apply, or use --apps-file for batch layout apply.",
776
- error_code="ARGUMENT_ERROR",
777
- details={
778
- "prog": "qingflow builder layout apply",
779
- "usage": "qingflow builder layout apply --app-key APP_KEY --sections-file SECTIONS.json",
780
- },
752
+ if apps:
753
+ return context.builder.app_layout_apply(
754
+ profile=args.profile,
755
+ app_key="",
756
+ mode=args.mode,
757
+ publish=bool(args.publish),
758
+ sections=[],
759
+ apps=apps,
781
760
  )
761
+ if not args.app_key:
762
+ raise_config_error("layout apply requires --app-key or --apps-file", fix_hint="Pass --app-key + --sections-file for single-app mode, or --apps-file for batch mode.")
782
763
  return context.builder.app_layout_apply(
783
764
  profile=args.profile,
784
765
  app_key=args.app_key,
785
766
  mode=args.mode,
786
767
  publish=bool(args.publish),
787
- sections=[] if apps is not None else require_list_arg(args.sections_file, option_name="--sections-file"),
788
- apps=apps,
768
+ sections=require_list_arg(args.sections_file, option_name="--sections-file"),
789
769
  )
790
770
 
791
771
 
792
772
  def _handle_views_apply(args: argparse.Namespace, context: CliContext) -> dict:
793
- apps = load_list_arg(args.apps_file, option_name="--apps-file") if getattr(args, "apps_file", None) else None
794
- if apps is None and not (args.app_key or "").strip():
795
- raise_config_error("builder views apply requires --app-key unless --apps-file is provided")
773
+ views_file = getattr(args, "views_file", None)
774
+ views = _load_views_payload(views_file)
775
+ if views_file is not None:
776
+ legacy_args = [
777
+ a
778
+ for a in [
779
+ "--app-key" if args.app_key else None,
780
+ "--apps-file" if getattr(args, "apps_file", None) else None,
781
+ "--upsert-views-file" if getattr(args, "upsert_views_file", None) else None,
782
+ "--patch-views-file" if getattr(args, "patch_views_file", None) else None,
783
+ "--remove-views-file" if getattr(args, "remove_views_file", None) else None,
784
+ ]
785
+ if a
786
+ ]
787
+ if legacy_args:
788
+ raise_config_error(
789
+ f"views apply --views-file cannot be combined with {', '.join(legacy_args)}.",
790
+ fix_hint="Use --views-file as the single batch input, or remove --views-file and use legacy single-app/apps-file mode.",
791
+ )
792
+ return context.builder.app_views_apply(
793
+ profile=args.profile,
794
+ app_key="",
795
+ publish=bool(args.publish),
796
+ upsert_views=[],
797
+ patch_views=[],
798
+ remove_views=[],
799
+ views=views,
800
+ )
801
+ apps = load_list_arg(getattr(args, "apps_file", None), option_name="--apps-file")
802
+ if apps:
803
+ single_app_args = [a for a in ["--app-key" if args.app_key else None, "--upsert-views-file" if getattr(args, "upsert_views_file", None) else None, "--patch-views-file" if getattr(args, "patch_views_file", None) else None, "--remove-views-file" if getattr(args, "remove_views_file", None) else None] if a]
804
+ if single_app_args:
805
+ raise_config_error(
806
+ f"views apply --apps-file cannot be combined with {', '.join(single_app_args)}.",
807
+ fix_hint="Use --apps-file for batch mode (each item contains app_key + per-app params), or remove --apps-file for single-app mode.",
808
+ )
809
+ if apps:
810
+ return context.builder.app_views_apply(
811
+ profile=args.profile,
812
+ app_key="",
813
+ publish=bool(args.publish),
814
+ upsert_views=[],
815
+ patch_views=[],
816
+ remove_views=[],
817
+ apps=apps,
818
+ )
819
+ if not args.app_key:
820
+ raise_config_error("views apply requires --app-key or --apps-file", fix_hint="Pass --app-key for single-app mode, or --apps-file for batch mode.")
796
821
  return context.builder.app_views_apply(
797
822
  profile=args.profile,
798
823
  app_key=args.app_key,
@@ -800,65 +825,79 @@ def _handle_views_apply(args: argparse.Namespace, context: CliContext) -> dict:
800
825
  upsert_views=load_list_arg(args.upsert_views_file, option_name="--upsert-views-file"),
801
826
  patch_views=load_list_arg(args.patch_views_file, option_name="--patch-views-file"),
802
827
  remove_views=load_list_arg(args.remove_views_file, option_name="--remove-views-file"),
803
- apps=apps,
828
+ )
829
+
830
+
831
+ def _handle_flow_schema(args: argparse.Namespace, context: CliContext) -> dict:
832
+ return context.builder.app_flow_get_schema(
833
+ profile=args.profile,
834
+ schema_version=args.schema_version or None,
835
+ )
836
+
837
+
838
+ def _handle_flow_get(args: argparse.Namespace, context: CliContext) -> dict:
839
+ return context.builder.app_get_flow(
840
+ profile=args.profile,
841
+ app_key=args.app_key,
842
+ version_id=args.version_id or None,
804
843
  )
805
844
 
806
845
 
807
846
  def _handle_flow_apply(args: argparse.Namespace, context: CliContext) -> dict:
808
847
  patch_nodes = load_list_arg(getattr(args, "patch_nodes_file", None), option_name="--patch-nodes-file")
809
- spec = load_object_arg(getattr(args, "spec_file", None), option_name="--spec-file")
810
- has_legacy_nodes = bool(getattr(args, "nodes_file", None) or getattr(args, "transitions_file", None))
811
- selected_modes = sum(1 for selected in (bool(patch_nodes), bool(spec), has_legacy_nodes) if selected)
812
- if selected_modes != 1:
848
+ if patch_nodes and args.spec_file:
813
849
  raise_config_error(
814
- "builder flow apply requires exactly one input mode: --spec-file, --patch-nodes-file, or --nodes-file with --transitions-file.",
815
- fix_hint="Use --patch-nodes-file for local edits, --spec-file for full WorkflowSpec, or legacy --nodes-file plus --transitions-file.",
850
+ "flow apply --spec-file and --patch-nodes-file are mutually exclusive.",
851
+ fix_hint="Use --spec-file to replace the full flow spec, or --patch-nodes-file to patch specific nodes.",
816
852
  )
817
853
  if patch_nodes:
818
854
  return context.builder.app_flow_apply(
819
855
  profile=args.profile,
820
856
  app_key=args.app_key,
821
857
  publish=bool(args.publish),
858
+ spec=None,
822
859
  patch_nodes=patch_nodes,
823
- idempotency_key=args.idempotency_key,
824
- schema_version=args.schema_version,
825
- )
826
- if spec:
827
- return context.builder.app_flow_apply(
828
- profile=args.profile,
829
- app_key=args.app_key,
830
- publish=bool(args.publish),
831
- spec=spec,
832
- idempotency_key=args.idempotency_key,
833
- schema_version=args.schema_version,
834
- )
835
- if not getattr(args, "nodes_file", None) or not getattr(args, "transitions_file", None):
836
- raise_config_error(
837
- "legacy builder flow apply requires both --nodes-file and --transitions-file.",
838
- fix_hint="Pass both files, or use --spec-file / --patch-nodes-file.",
860
+ idempotency_key=args.idempotency_key or None,
861
+ schema_version=args.schema_version or None,
839
862
  )
863
+ spec_payload = load_object_arg(args.spec_file, option_name="--spec-file") if args.spec_file else None
864
+ if not isinstance(spec_payload, dict):
865
+ raise_config_error("flow apply requires either --spec-file or --patch-nodes-file.", fix_hint="Pass --spec-file to replace the full flow spec, or --patch-nodes-file to patch specific nodes.")
866
+ if "spec" in spec_payload and isinstance(spec_payload.get("spec"), dict):
867
+ spec = spec_payload["spec"]
868
+ else:
869
+ spec = spec_payload
840
870
  return context.builder.app_flow_apply(
841
871
  profile=args.profile,
842
872
  app_key=args.app_key,
843
- mode=args.mode,
844
873
  publish=bool(args.publish),
845
- nodes=require_list_arg(args.nodes_file, option_name="--nodes-file"),
846
- transitions=require_list_arg(args.transitions_file, option_name="--transitions-file"),
874
+ spec=spec,
875
+ idempotency_key=args.idempotency_key or None,
876
+ schema_version=args.schema_version or None,
847
877
  )
848
878
 
849
879
 
850
- def _handle_flow_schema(args: argparse.Namespace, context: CliContext) -> dict:
851
- return context.builder.app_flow_get_schema(profile=args.profile, schema_version=args.schema_version)
852
-
853
-
854
- def _handle_flow_get(args: argparse.Namespace, context: CliContext) -> dict:
855
- return context.builder.app_flow_get(profile=args.profile, app_key=args.app_key, version_id=args.version_id)
856
-
857
-
858
880
  def _handle_charts_apply(args: argparse.Namespace, context: CliContext) -> dict:
859
- apps = load_list_arg(args.apps_file, option_name="--apps-file") if getattr(args, "apps_file", None) else None
860
- if apps is None and not (args.app_key or "").strip():
861
- raise_config_error("builder charts apply requires --app-key unless --apps-file is provided")
881
+ apps = load_list_arg(getattr(args, "apps_file", None), option_name="--apps-file")
882
+ if apps:
883
+ single_app_args = [a for a in ["--app-key" if args.app_key else None, "--upsert-file" if getattr(args, "upsert_file", None) else None, "--patch-file" if getattr(args, "patch_file", None) else None, "--remove-chart-ids-file" if getattr(args, "remove_chart_ids_file", None) else None, "--reorder-chart-ids-file" if getattr(args, "reorder_chart_ids_file", None) else None] if a]
884
+ if single_app_args:
885
+ raise_config_error(
886
+ f"charts apply --apps-file cannot be combined with {', '.join(single_app_args)}.",
887
+ fix_hint="Use --apps-file for batch mode (each item contains app_key + per-app params), or remove --apps-file for single-app mode.",
888
+ )
889
+ if apps:
890
+ return context.builder.app_charts_apply(
891
+ profile=args.profile,
892
+ app_key="",
893
+ upsert_charts=[],
894
+ patch_charts=[],
895
+ remove_chart_ids=[],
896
+ reorder_chart_ids=[],
897
+ apps=apps,
898
+ )
899
+ if not args.app_key:
900
+ raise_config_error("charts apply requires --app-key or --apps-file", fix_hint="Pass --app-key for single-app mode, or --apps-file for batch mode.")
862
901
  return context.builder.app_charts_apply(
863
902
  profile=args.profile,
864
903
  app_key=args.app_key,
@@ -866,11 +905,30 @@ def _handle_charts_apply(args: argparse.Namespace, context: CliContext) -> dict:
866
905
  patch_charts=load_list_arg(args.patch_file, option_name="--patch-file"),
867
906
  remove_chart_ids=load_list_arg(args.remove_chart_ids_file, option_name="--remove-chart-ids-file"),
868
907
  reorder_chart_ids=load_list_arg(args.reorder_chart_ids_file, option_name="--reorder-chart-ids-file"),
869
- apps=apps,
870
908
  )
871
909
 
872
910
 
873
911
  def _handle_portal_apply(args: argparse.Namespace, context: CliContext) -> dict:
912
+ patch_sections = load_list_arg(getattr(args, "patch_sections_file", None), option_name="--patch-sections-file")
913
+ if patch_sections:
914
+ if not (args.dash_key or "").strip():
915
+ raise_config_error("portal apply --patch-sections-file requires --dash-key", fix_hint="Pass --dash-key DASH_KEY to identify the portal to patch")
916
+ has_sections_file = bool(getattr(args, "sections_file", None))
917
+ has_payload_file = bool(getattr(args, "payload_file", None))
918
+ if has_sections_file or has_payload_file:
919
+ raise_config_error(
920
+ "portal apply --patch-sections-file cannot be combined with --sections-file or --payload-file.",
921
+ fix_hint="Use --patch-sections-file alone to patch specific sections, or --sections-file to replace all sections.",
922
+ )
923
+ return context.builder.portal_apply(
924
+ profile=args.profile,
925
+ dash_key=args.dash_key,
926
+ dash_name="",
927
+ package_id=None,
928
+ publish=bool(args.publish),
929
+ sections=[],
930
+ patch_sections=patch_sections,
931
+ )
874
932
  payload = load_object_arg(args.payload_file, option_name="--payload-file") if getattr(args, "payload_file", None) else None
875
933
  payload_obj = payload if isinstance(payload, dict) else {}
876
934
  effective_dash_name = (args.dash_name or str(payload_obj.get("dash_name") or payload_obj.get("dashName") or payload_obj.get("name") or "")).strip()
@@ -889,11 +947,6 @@ def _handle_portal_apply(args: argparse.Namespace, context: CliContext) -> dict:
889
947
  fix_hint="Use `--dash-key` for an existing portal. For create mode, pass `--package-id --dash-name`.",
890
948
  )
891
949
  sections = [] if not args.sections_file else require_list_arg(args.sections_file, option_name="--sections-file")
892
- patch_sections = (
893
- load_list_arg(args.patch_sections_file, option_name="--patch-sections-file")
894
- if args.patch_sections_file
895
- else None
896
- )
897
950
  return context.builder.portal_apply(
898
951
  profile=args.profile,
899
952
  dash_key=args.dash_key,
@@ -910,28 +963,17 @@ def _handle_portal_apply(args: argparse.Namespace, context: CliContext) -> dict:
910
963
  dash_global_config=load_object_arg(args.dash_global_config_file, option_name="--dash-global-config-file"),
911
964
  config=load_object_arg(args.config_file, option_name="--config-file"),
912
965
  payload=payload,
913
- patch_sections=patch_sections,
914
- )
915
-
916
-
917
- def _handle_portal_delete(args: argparse.Namespace, context: CliContext) -> dict:
918
- return context.builder.portal_delete(
919
- profile=args.profile,
920
- dash_key=args.dash_key,
921
966
  )
922
967
 
923
968
 
924
969
  def _handle_publish_verify(args: argparse.Namespace, context: CliContext) -> dict:
925
- app_keys = _app_keys_from_args(args)
926
- if app_keys is not None:
970
+ app_keys = _parse_app_keys_arg(getattr(args, "app_keys", None))
971
+ if app_keys:
927
972
  return context.builder.app_publish_verify(
928
973
  profile=args.profile,
929
- app_key="",
930
974
  app_keys=app_keys,
931
975
  expected_package_id=args.expected_package_id,
932
976
  )
933
- if not (args.app_key or "").strip():
934
- raise_config_error("builder publish verify requires --app-key unless --app-keys or --app-keys-file is provided")
935
977
  return context.builder.app_publish_verify(
936
978
  profile=args.profile,
937
979
  app_key=args.app_key,