@josephyan/qingflow-cli 1.1.3 → 1.1.5

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