@josephyan/qingflow-app-builder-mcp 1.1.30 → 1.1.33

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