@josephyan/qingflow-cli 1.1.4 → 1.1.6

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 +282 -102
  93. package/src/qingflow_mcp/builder_facade/service.py +4192 -935
  94. package/src/qingflow_mcp/cli/commands/builder.py +316 -298
  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 +1780 -406
  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,68 +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
8
+ from .common import load_list_arg, load_object_arg, parse_bool_text, raise_config_error, require_list_arg
66
9
 
67
10
 
68
11
  def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
@@ -79,7 +22,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
79
22
  file_upload_local.add_argument("--bucket-type")
80
23
  file_upload_local.add_argument("--path-id", type=int)
81
24
  file_upload_local.add_argument("--file-related-url")
82
- 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")
83
26
 
84
27
  feedback = builder_subparsers.add_parser("feedback", help="builder 侧反馈提交")
85
28
  feedback_subparsers = feedback.add_subparsers(dest="builder_feedback_command", required=True)
@@ -95,7 +38,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
95
38
  feedback_submit.add_argument("--record-id")
96
39
  feedback_submit.add_argument("--workflow-node-id")
97
40
  feedback_submit.add_argument("--note")
98
- feedback_submit.set_defaults(handler=_handle_feedback_submit, format_hint="generic")
41
+ feedback_submit.set_defaults(handler=_handle_feedback_submit, format_hint="feedback_submit")
99
42
 
100
43
  contract = builder_subparsers.add_parser("contract", help="读取 builder tool 合约")
101
44
  contract.add_argument("--tool-name", required=True)
@@ -109,7 +52,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
109
52
  member = builder_subparsers.add_parser("member", help="成员目录")
110
53
  member_subparsers = member.add_subparsers(dest="builder_member_command", required=True)
111
54
  member_search = member_subparsers.add_parser("search", help="搜索成员")
112
- member_search.add_argument("--query", default="")
55
+ member_search.add_argument("--query", required=True)
113
56
  member_search.add_argument("--page-num", type=int, default=1)
114
57
  member_search.add_argument("--page-size", type=int, default=20)
115
58
  member_search.add_argument("--contain-disable", action=argparse.BooleanOptionalAction, default=False)
@@ -118,7 +61,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
118
61
  role = builder_subparsers.add_parser("role", help="角色目录")
119
62
  role_subparsers = role.add_subparsers(dest="builder_role_command", required=True)
120
63
  role_search = role_subparsers.add_parser("search", help="搜索角色")
121
- role_search.add_argument("--keyword", default="")
64
+ role_search.add_argument("--keyword", required=True)
122
65
  role_search.add_argument("--page-num", type=int, default=1)
123
66
  role_search.add_argument("--page-size", type=int, default=20)
124
67
  role_search.set_defaults(handler=_handle_role_search, format_hint="builder_summary")
@@ -178,7 +121,8 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
178
121
  default="summary",
179
122
  )
180
123
  app_get.add_argument("--app-key", default="")
181
- 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")
182
126
  app_get.set_defaults(handler=_handle_app_get, format_hint="builder_summary")
183
127
 
184
128
  app_repair_code_blocks = app_subparsers.add_parser("repair-code-blocks", help="扫描或修复现有代码块配置")
@@ -195,34 +139,35 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
195
139
 
196
140
  button_get = button_subparsers.add_parser("get", help="读取应用自定义按钮配置")
197
141
  button_get.add_argument("--app-key", default="")
198
- 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 字符串数组;用于批量读取按钮配置")
199
144
  button_get.set_defaults(handler=_handle_button_get, format_hint="builder_summary")
200
145
 
201
146
  button_apply = button_subparsers.add_parser("apply", help="声明式创建、更新或删除自定义按钮")
202
147
  button_apply.add_argument("--app-key", default="")
148
+ button_apply.add_argument("--apps-file", help="多应用按钮配置 JSON 数组;每项包含 app_key 和按钮 payload")
203
149
  button_apply.add_argument("--upsert-buttons-file")
204
150
  button_apply.add_argument("--patch-buttons-file")
205
151
  button_apply.add_argument("--remove-buttons-file")
206
152
  button_apply.add_argument("--view-configs-file")
207
- button_apply.add_argument("--apps-file", help="多应用批量按钮 JSON 数组,每项含 app_key + upsert_buttons/patch_buttons/remove_buttons/view_configs")
208
153
  button_apply.set_defaults(handler=_handle_button_apply, format_hint="builder_summary", force_json_output=True)
209
154
 
210
155
  associated_resource = builder_subparsers.add_parser("associated-resource", aliases=["associated-resources"], help="关联视图/报表")
211
156
  associated_resource_subparsers = associated_resource.add_subparsers(dest="builder_associated_resource_command", required=True)
212
-
213
157
  associated_resource_get = associated_resource_subparsers.add_parser("get", help="读取应用关联资源配置")
214
158
  associated_resource_get.add_argument("--app-key", default="")
215
- 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 字符串数组;用于批量读取关联资源配置")
216
161
  associated_resource_get.set_defaults(handler=_handle_associated_resource_get, format_hint="builder_summary")
217
162
 
218
163
  associated_resource_apply = associated_resource_subparsers.add_parser("apply", help="声明式管理应用关联资源池和视图展示配置")
219
164
  associated_resource_apply.add_argument("--app-key", default="")
165
+ associated_resource_apply.add_argument("--apps-file", help="多应用关联资源配置 JSON 数组;每项包含 app_key 和资源 payload")
220
166
  associated_resource_apply.add_argument("--upsert-resources-file")
221
167
  associated_resource_apply.add_argument("--patch-resources-file")
222
168
  associated_resource_apply.add_argument("--remove-associated-item-ids-file")
223
169
  associated_resource_apply.add_argument("--reorder-associated-item-ids-file")
224
170
  associated_resource_apply.add_argument("--view-configs-file")
225
- 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")
226
171
  associated_resource_apply.set_defaults(handler=_handle_associated_resource_apply, format_hint="builder_summary", force_json_output=True)
227
172
 
228
173
  portal = builder_subparsers.add_parser("portal", help="门户")
@@ -253,6 +198,10 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
253
198
  portal_apply.add_argument("--patch-sections-file", help="门户组件局部更新 JSON 数组,每项含 chart_ref/view_ref/order + set/unset")
254
199
  portal_apply.set_defaults(handler=_handle_portal_apply, format_hint="builder_summary", force_json_output=True)
255
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
+
256
205
  schema_apply = builder_subparsers.add_parser("schema", help="字段搭建")
257
206
  schema_apply_subparsers = schema_apply.add_subparsers(dest="builder_schema_command", required=True)
258
207
  schema_apply_apply = schema_apply_subparsers.add_parser("apply", help="执行字段变更")
@@ -264,8 +213,8 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
264
213
  schema_apply_apply.add_argument("--color")
265
214
  schema_apply_apply.add_argument("--visibility-file")
266
215
  schema_apply_apply.add_argument("--create-if-missing", action="store_true")
267
- schema_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
268
- schema_apply_apply.add_argument("--apps-file", help="多应用 schema JSON;推荐对象 {package_id, apps},兼容 raw apps 数组;支持 relation target_app_ref")
216
+ schema_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=None)
217
+ schema_apply_apply.add_argument("--apps-file", help="多应用 schema JSON 数组;每项可带 client_key/app_name/add_fields,支持 relation target_app_ref")
269
218
  schema_apply_apply.add_argument("--add-fields-file", help="字段 JSON 数组;字段可用 as_data_title/as_data_cover 标记数据标题/封面")
270
219
  schema_apply_apply.add_argument("--update-fields-file", help="字段更新 JSON 数组;set 内可用 as_data_title/as_data_cover 标记数据标题/封面")
271
220
  schema_apply_apply.add_argument("--remove-fields-file")
@@ -275,60 +224,63 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
275
224
  layout_apply_subparsers = layout_apply.add_subparsers(dest="builder_layout_command", required=True)
276
225
  layout_apply_apply = layout_apply_subparsers.add_parser("apply", help="执行布局变更")
277
226
  layout_apply_apply.add_argument("--app-key", default="")
227
+ layout_apply_apply.add_argument("--apps-file", help="多应用布局 JSON 数组;每项包含 app_key/mode?/publish?/sections")
278
228
  layout_apply_apply.add_argument("--mode", choices=["merge", "replace"], default="merge")
279
229
  layout_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
280
230
  layout_apply_apply.add_argument("--sections-file")
281
- layout_apply_apply.add_argument("--apps-file", help="多应用批量布局 JSON 数组,每项含 app_key + sections")
282
231
  layout_apply_apply.set_defaults(handler=_handle_layout_apply, format_hint="builder_summary", force_json_output=True)
283
232
 
284
233
  views_apply = builder_subparsers.add_parser("views", help="视图")
285
234
  views_apply_subparsers = views_apply.add_subparsers(dest="builder_views_command", required=True)
286
235
  views_apply_apply = views_apply_subparsers.add_parser("apply", help="执行视图变更")
287
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")
288
238
  views_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
289
239
  views_apply_apply.add_argument("--upsert-views-file")
290
240
  views_apply_apply.add_argument("--patch-views-file")
291
241
  views_apply_apply.add_argument("--remove-views-file")
292
- views_apply_apply.add_argument("--apps-file", help="多应用批量视图 JSON 数组,每项含 app_key + upsert_views/patch_views/remove_views")
293
242
  views_apply_apply.set_defaults(handler=_handle_views_apply, format_hint="builder_summary", force_json_output=True)
294
243
 
295
- flow_parser = builder_subparsers.add_parser("flow", help="流程(WorkflowSpec)")
296
- flow_subparsers = flow_parser.add_subparsers(dest="builder_flow_command", required=True)
297
-
298
- 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")
299
247
  flow_schema.add_argument("--schema-version", default="")
300
248
  flow_schema.set_defaults(handler=_handle_flow_schema, format_hint="builder_summary")
301
249
 
302
- flow_get = flow_subparsers.add_parser("get", help="读取 WorkflowSpec")
250
+ flow_get = flow_apply_subparsers.add_parser("get", help="读取 WorkflowSpec")
303
251
  flow_get.add_argument("--app-key", required=True)
304
252
  flow_get.add_argument("--version-id", default="")
305
253
  flow_get.set_defaults(handler=_handle_flow_get, format_hint="builder_summary")
306
254
 
307
- flow_apply = flow_subparsers.add_parser("apply", help="应用 WorkflowSpec")
308
- flow_apply.add_argument("--app-key", required=True)
309
- flow_apply.add_argument("--spec-file")
310
- flow_apply.add_argument("--patch-nodes-file", help="节点局部更新 JSON 数组,每项含 id + set/unset")
311
- flow_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
312
- flow_apply.add_argument("--idempotency-key", default="")
313
- flow_apply.add_argument("--schema-version", default="")
314
- 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)
315
266
 
316
267
  charts_apply = builder_subparsers.add_parser("charts", help="报表")
317
268
  charts_apply_subparsers = charts_apply.add_subparsers(dest="builder_charts_command", required=True)
318
269
  charts_apply_apply = charts_apply_subparsers.add_parser("apply", help="执行报表变更")
319
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")
320
272
  charts_apply_apply.add_argument("--upsert-file")
321
273
  charts_apply_apply.add_argument("--patch-file")
322
274
  charts_apply_apply.add_argument("--remove-chart-ids-file")
323
275
  charts_apply_apply.add_argument("--reorder-chart-ids-file")
324
- charts_apply_apply.add_argument("--apps-file", help="多应用批量报表 JSON 数组,每项含 app_key + upsert_charts/patch_charts/remove_chart_ids/reorder_chart_ids")
325
276
  charts_apply_apply.set_defaults(handler=_handle_charts_apply, format_hint="builder_summary", force_json_output=True)
326
277
 
327
278
  publish_verify = builder_subparsers.add_parser("publish", help="发布校验")
328
279
  publish_verify_subparsers = publish_verify.add_subparsers(dest="builder_publish_command", required=True)
329
280
  publish_verify_verify = publish_verify_subparsers.add_parser("verify", help="校验应用发布")
330
281
  publish_verify_verify.add_argument("--app-key", default="")
331
- 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 字符串数组;用于批量发布校验")
332
284
  publish_verify_verify.add_argument("--expected-package-id", type=int)
333
285
  publish_verify_verify.set_defaults(handler=_handle_publish_verify, format_hint="builder_summary", force_json_output=True)
334
286
 
@@ -476,26 +428,9 @@ def _handle_button_catalog(args: argparse.Namespace, context: CliContext) -> dic
476
428
 
477
429
 
478
430
  def _handle_button_apply(args: argparse.Namespace, context: CliContext) -> dict:
479
- apps = load_list_arg(getattr(args, "apps_file", None), option_name="--apps-file")
480
- if apps:
481
- 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]
482
- if single_app_args:
483
- raise_config_error(
484
- f"button apply --apps-file cannot be combined with {', '.join(single_app_args)}.",
485
- fix_hint="Use --apps-file for batch mode (each item contains app_key + per-app params), or remove --apps-file for single-app mode.",
486
- )
487
- if apps:
488
- return context.builder.app_custom_buttons_apply(
489
- profile=args.profile,
490
- app_key="",
491
- upsert_buttons=[],
492
- patch_buttons=[],
493
- remove_buttons=[],
494
- view_configs=[],
495
- apps=apps,
496
- )
497
- if not args.app_key:
498
- 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")
499
434
  return context.builder.app_custom_buttons_apply(
500
435
  profile=args.profile,
501
436
  app_key=args.app_key,
@@ -503,40 +438,14 @@ def _handle_button_apply(args: argparse.Namespace, context: CliContext) -> dict:
503
438
  patch_buttons=load_list_arg(args.patch_buttons_file, option_name="--patch-buttons-file"),
504
439
  remove_buttons=load_list_arg(args.remove_buttons_file, option_name="--remove-buttons-file"),
505
440
  view_configs=load_list_arg(args.view_configs_file, option_name="--view-configs-file"),
441
+ apps=apps,
506
442
  )
507
443
 
508
444
 
509
445
  def _handle_associated_resource_apply(args: argparse.Namespace, context: CliContext) -> dict:
510
- apps = load_list_arg(getattr(args, "apps_file", None), option_name="--apps-file")
511
- if apps:
512
- single_app_args = [
513
- a for a in [
514
- "--app-key" if args.app_key else None,
515
- "--upsert-resources-file" if getattr(args, "upsert_resources_file", None) else None,
516
- "--patch-resources-file" if getattr(args, "patch_resources_file", None) else None,
517
- "--remove-associated-item-ids-file" if getattr(args, "remove_associated_item_ids_file", None) else None,
518
- "--reorder-associated-item-ids-file" if getattr(args, "reorder_associated_item_ids_file", None) else None,
519
- "--view-configs-file" if getattr(args, "view_configs_file", None) else None,
520
- ] if a
521
- ]
522
- if single_app_args:
523
- raise_config_error(
524
- f"associated-resource apply --apps-file cannot be combined with {', '.join(single_app_args)}.",
525
- fix_hint="Use --apps-file for batch mode (each item contains app_key + per-app params), or remove --apps-file for single-app mode.",
526
- )
527
- if apps:
528
- return context.builder.app_associated_resources_apply(
529
- profile=args.profile,
530
- app_key="",
531
- upsert_resources=[],
532
- patch_resources=[],
533
- remove_associated_item_ids=[],
534
- reorder_associated_item_ids=[],
535
- view_configs=[],
536
- apps=apps,
537
- )
538
- if not args.app_key:
539
- 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")
540
449
  return context.builder.app_associated_resources_apply(
541
450
  profile=args.profile,
542
451
  app_key=args.app_key,
@@ -545,27 +454,26 @@ def _handle_associated_resource_apply(args: argparse.Namespace, context: CliCont
545
454
  remove_associated_item_ids=load_list_arg(args.remove_associated_item_ids_file, option_name="--remove-associated-item-ids-file"),
546
455
  reorder_associated_item_ids=load_list_arg(args.reorder_associated_item_ids_file, option_name="--reorder-associated-item-ids-file"),
547
456
  view_configs=load_list_arg(args.view_configs_file, option_name="--view-configs-file"),
457
+ apps=apps,
548
458
  )
549
459
 
550
460
 
551
461
  def _handle_button_get(args: argparse.Namespace, context: CliContext) -> dict:
552
- app_keys = _parse_app_keys_arg(getattr(args, "app_keys", None))
553
- if app_keys:
554
- return context.builder.app_get_buttons(profile=args.profile, app_keys=app_keys)
555
- app_key = str(getattr(args, "app_key", "") or "").strip()
556
- if not app_key:
557
- raise_config_error("button get requires --app-key or --app-keys", fix_hint="Pass --app-key APP_KEY or --app-keys KEY1,KEY2")
558
- 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)
559
468
 
560
469
 
561
470
  def _handle_associated_resource_get(args: argparse.Namespace, context: CliContext) -> dict:
562
- app_keys = _parse_app_keys_arg(getattr(args, "app_keys", None))
563
- if app_keys:
564
- return context.builder.app_get_associated_resources(profile=args.profile, app_keys=app_keys)
565
- app_key = str(getattr(args, "app_key", "") or "").strip()
566
- if not app_key:
567
- raise_config_error("associated-resource get requires --app-key or --app-keys", fix_hint="Pass --app-key APP_KEY or --app-keys KEY1,KEY2")
568
- 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)
569
477
 
570
478
 
571
479
  def _handle_button_create(args: argparse.Namespace, context: CliContext) -> dict:
@@ -589,32 +497,18 @@ def _handle_button_delete(args: argparse.Namespace, context: CliContext) -> dict
589
497
  return context.builder.app_custom_button_delete(profile=args.profile, app_key=args.app_key, button_id=args.button_id)
590
498
 
591
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
+
592
511
  def _handle_app_get(args: argparse.Namespace, context: CliContext) -> dict:
593
- section = args.builder_app_get_section
594
- app_keys = _parse_app_keys_arg(getattr(args, "app_keys", None))
595
- app_key = str(getattr(args, "app_key", "") or "").strip()
596
- if not app_keys and not app_key:
597
- raise_config_error("app get requires --app-key or --app-keys", fix_hint="Pass --app-key APP_KEY or --app-keys KEY1,KEY2")
598
- if section == "buttons":
599
- if app_keys:
600
- return context.builder.app_get_buttons(profile=args.profile, app_keys=app_keys)
601
- return context.builder.app_get_buttons(profile=args.profile, app_key=app_key)
602
- if section == "associated-resources":
603
- if app_keys:
604
- return context.builder.app_get_associated_resources(profile=args.profile, app_keys=app_keys)
605
- return context.builder.app_get_associated_resources(profile=args.profile, app_key=app_key)
606
- if app_keys:
607
- batch_handlers = {
608
- "summary": context.builder.app_get,
609
- "fields": context.builder.app_get_fields,
610
- "layout": context.builder.app_get_layout,
611
- "views": context.builder.app_get_views,
612
- "flow": context.builder.app_get_flow,
613
- "charts": context.builder.app_get_charts,
614
- }
615
- if section not in batch_handlers:
616
- 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")
617
- return batch_handlers[section](profile=args.profile, app_keys=app_keys)
618
512
  handlers = {
619
513
  "summary": context.builder.app_get,
620
514
  "fields": context.builder.app_get_fields,
@@ -622,8 +516,15 @@ def _handle_app_get(args: argparse.Namespace, context: CliContext) -> dict:
622
516
  "views": context.builder.app_get_views,
623
517
  "flow": context.builder.app_get_flow,
624
518
  "charts": context.builder.app_get_charts,
519
+ "buttons": context.builder.app_get_buttons,
520
+ "associated-resources": context.builder.app_get_associated_resources,
625
521
  }
626
- 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)
627
528
 
628
529
 
629
530
  def _handle_app_repair_code_blocks(args: argparse.Namespace, context: CliContext) -> dict:
@@ -652,34 +553,57 @@ def _handle_chart_get(args: argparse.Namespace, context: CliContext) -> dict:
652
553
 
653
554
 
654
555
  def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
655
- apps, apps_package_id = _load_schema_apps_payload(args.apps_file, cli_package_id=args.package_id)
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"])
656
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)
657
575
  if not apps:
658
576
  raise_config_error(
659
577
  "schema apply multi-app mode requires a non-empty --apps-file.",
660
- fix_hint='Pass {"package_id": 123, "apps": [{"client_key": "main", "app_name": "主表", "icon": "database", "color": "blue", "add_fields": []}]}',
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",
661
580
  )
662
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:
663
582
  raise_config_error(
664
583
  "schema apply multi-app mode accepts --package-id/--create-if-missing plus --apps-file only.",
665
584
  fix_hint="Use `--apps-file` for batch mode, or remove `--apps-file` and use the single-app arguments.",
666
585
  )
667
- if apps_package_id is None:
586
+ if package_id is None:
668
587
  raise_config_error(
669
588
  "schema apply multi-app mode requires --package-id.",
670
- fix_hint="Pass root `--package-id`, or include package_id in the --apps-file object.",
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.",
671
590
  )
672
- return context.builder.app_schema_apply(
591
+ result = context.builder.app_schema_apply(
673
592
  profile=args.profile,
674
- package_id=apps_package_id,
593
+ package_id=package_id,
675
594
  visibility=load_object_arg(args.visibility_file, option_name="--visibility-file"),
676
- create_if_missing=bool(args.create_if_missing),
677
- publish=bool(args.publish),
595
+ create_if_missing=effective_create_if_missing,
596
+ publish=effective_publish,
678
597
  apps=apps,
679
598
  add_fields=[],
680
599
  update_fields=[],
681
600
  remove_fields=[],
682
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
683
607
  has_app_key = bool((args.app_key or "").strip())
684
608
  has_app_name = bool((args.app_name or "").strip())
685
609
  has_app_title = bool((args.app_title or "").strip())
@@ -706,63 +630,178 @@ def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
706
630
  color=args.color,
707
631
  visibility=load_object_arg(args.visibility_file, option_name="--visibility-file"),
708
632
  create_if_missing=bool(args.create_if_missing),
709
- publish=bool(args.publish),
633
+ publish=True if args.publish is None else bool(args.publish),
710
634
  add_fields=load_list_arg(args.add_fields_file, option_name="--add-fields-file"),
711
635
  update_fields=load_list_arg(args.update_fields_file, option_name="--update-fields-file"),
712
636
  remove_fields=load_list_arg(args.remove_fields_file, option_name="--remove-fields-file"),
713
637
  )
714
638
 
715
639
 
716
- def _handle_layout_apply(args: argparse.Namespace, context: CliContext) -> dict:
717
- apps = load_list_arg(getattr(args, "apps_file", None), option_name="--apps-file")
718
- if apps:
719
- 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]
720
- 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):
721
691
  raise_config_error(
722
- f"layout apply --apps-file cannot be combined with {', '.join(single_app_args)}.",
723
- 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,
724
696
  )
725
- if apps:
726
- return context.builder.app_layout_apply(
727
- profile=args.profile,
728
- app_key="",
729
- mode=args.mode,
730
- publish=bool(args.publish),
731
- sections=[],
732
- 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
+ },
733
790
  )
734
- if not args.app_key:
735
- 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.")
736
791
  return context.builder.app_layout_apply(
737
792
  profile=args.profile,
738
793
  app_key=args.app_key,
739
794
  mode=args.mode,
740
795
  publish=bool(args.publish),
741
- 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,
742
798
  )
743
799
 
744
800
 
745
801
  def _handle_views_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, "--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]
749
- if single_app_args:
750
- raise_config_error(
751
- f"views 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 + per-app params), or remove --apps-file for single-app mode.",
753
- )
754
- if apps:
755
- return context.builder.app_views_apply(
756
- profile=args.profile,
757
- app_key="",
758
- publish=bool(args.publish),
759
- upsert_views=[],
760
- patch_views=[],
761
- remove_views=[],
762
- apps=apps,
763
- )
764
- if not args.app_key:
765
- 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")
766
805
  return context.builder.app_views_apply(
767
806
  profile=args.profile,
768
807
  app_key=args.app_key,
@@ -770,79 +809,65 @@ def _handle_views_apply(args: argparse.Namespace, context: CliContext) -> dict:
770
809
  upsert_views=load_list_arg(args.upsert_views_file, option_name="--upsert-views-file"),
771
810
  patch_views=load_list_arg(args.patch_views_file, option_name="--patch-views-file"),
772
811
  remove_views=load_list_arg(args.remove_views_file, option_name="--remove-views-file"),
773
- )
774
-
775
-
776
- def _handle_flow_schema(args: argparse.Namespace, context: CliContext) -> dict:
777
- return context.builder.app_flow_get_schema(
778
- profile=args.profile,
779
- schema_version=args.schema_version or None,
780
- )
781
-
782
-
783
- def _handle_flow_get(args: argparse.Namespace, context: CliContext) -> dict:
784
- return context.builder.app_get_flow(
785
- profile=args.profile,
786
- app_key=args.app_key,
787
- version_id=args.version_id or None,
812
+ apps=apps,
788
813
  )
789
814
 
790
815
 
791
816
  def _handle_flow_apply(args: argparse.Namespace, context: CliContext) -> dict:
792
817
  patch_nodes = load_list_arg(getattr(args, "patch_nodes_file", None), option_name="--patch-nodes-file")
793
- 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:
794
822
  raise_config_error(
795
- "flow apply --spec-file and --patch-nodes-file are mutually exclusive.",
796
- 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.",
797
825
  )
798
826
  if patch_nodes:
799
827
  return context.builder.app_flow_apply(
800
828
  profile=args.profile,
801
829
  app_key=args.app_key,
802
830
  publish=bool(args.publish),
803
- spec=None,
804
831
  patch_nodes=patch_nodes,
805
- idempotency_key=args.idempotency_key or None,
806
- 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.",
807
848
  )
808
- spec_payload = load_object_arg(args.spec_file, option_name="--spec-file") if args.spec_file else None
809
- if not isinstance(spec_payload, dict):
810
- 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.")
811
- if "spec" in spec_payload and isinstance(spec_payload.get("spec"), dict):
812
- spec = spec_payload["spec"]
813
- else:
814
- spec = spec_payload
815
849
  return context.builder.app_flow_apply(
816
850
  profile=args.profile,
817
851
  app_key=args.app_key,
852
+ mode=args.mode,
818
853
  publish=bool(args.publish),
819
- spec=spec,
820
- idempotency_key=args.idempotency_key or None,
821
- 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"),
822
856
  )
823
857
 
824
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
+
825
867
  def _handle_charts_apply(args: argparse.Namespace, context: CliContext) -> dict:
826
- apps = load_list_arg(getattr(args, "apps_file", None), option_name="--apps-file")
827
- if apps:
828
- 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]
829
- if single_app_args:
830
- raise_config_error(
831
- f"charts apply --apps-file cannot be combined with {', '.join(single_app_args)}.",
832
- fix_hint="Use --apps-file for batch mode (each item contains app_key + per-app params), or remove --apps-file for single-app mode.",
833
- )
834
- if apps:
835
- return context.builder.app_charts_apply(
836
- profile=args.profile,
837
- app_key="",
838
- upsert_charts=[],
839
- patch_charts=[],
840
- remove_chart_ids=[],
841
- reorder_chart_ids=[],
842
- apps=apps,
843
- )
844
- if not args.app_key:
845
- 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")
846
871
  return context.builder.app_charts_apply(
847
872
  profile=args.profile,
848
873
  app_key=args.app_key,
@@ -850,30 +875,11 @@ def _handle_charts_apply(args: argparse.Namespace, context: CliContext) -> dict:
850
875
  patch_charts=load_list_arg(args.patch_file, option_name="--patch-file"),
851
876
  remove_chart_ids=load_list_arg(args.remove_chart_ids_file, option_name="--remove-chart-ids-file"),
852
877
  reorder_chart_ids=load_list_arg(args.reorder_chart_ids_file, option_name="--reorder-chart-ids-file"),
878
+ apps=apps,
853
879
  )
854
880
 
855
881
 
856
882
  def _handle_portal_apply(args: argparse.Namespace, context: CliContext) -> dict:
857
- patch_sections = load_list_arg(getattr(args, "patch_sections_file", None), option_name="--patch-sections-file")
858
- if patch_sections:
859
- if not (args.dash_key or "").strip():
860
- raise_config_error("portal apply --patch-sections-file requires --dash-key", fix_hint="Pass --dash-key DASH_KEY to identify the portal to patch")
861
- has_sections_file = bool(getattr(args, "sections_file", None))
862
- has_payload_file = bool(getattr(args, "payload_file", None))
863
- if has_sections_file or has_payload_file:
864
- raise_config_error(
865
- "portal apply --patch-sections-file cannot be combined with --sections-file or --payload-file.",
866
- fix_hint="Use --patch-sections-file alone to patch specific sections, or --sections-file to replace all sections.",
867
- )
868
- return context.builder.portal_apply(
869
- profile=args.profile,
870
- dash_key=args.dash_key,
871
- dash_name="",
872
- package_id=None,
873
- publish=bool(args.publish),
874
- sections=[],
875
- patch_sections=patch_sections,
876
- )
877
883
  payload = load_object_arg(args.payload_file, option_name="--payload-file") if getattr(args, "payload_file", None) else None
878
884
  payload_obj = payload if isinstance(payload, dict) else {}
879
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()
@@ -892,6 +898,7 @@ def _handle_portal_apply(args: argparse.Namespace, context: CliContext) -> dict:
892
898
  fix_hint="Use `--dash-key` for an existing portal. For create mode, pass `--package-id --dash-name`.",
893
899
  )
894
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")
895
902
  return context.builder.portal_apply(
896
903
  profile=args.profile,
897
904
  dash_key=args.dash_key,
@@ -908,17 +915,28 @@ def _handle_portal_apply(args: argparse.Namespace, context: CliContext) -> dict:
908
915
  dash_global_config=load_object_arg(args.dash_global_config_file, option_name="--dash-global-config-file"),
909
916
  config=load_object_arg(args.config_file, option_name="--config-file"),
910
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,
911
926
  )
912
927
 
913
928
 
914
929
  def _handle_publish_verify(args: argparse.Namespace, context: CliContext) -> dict:
915
- app_keys = _parse_app_keys_arg(getattr(args, "app_keys", None))
916
- if app_keys:
930
+ app_keys = _app_keys_from_args(args)
931
+ if app_keys is not None:
917
932
  return context.builder.app_publish_verify(
918
933
  profile=args.profile,
934
+ app_key="",
919
935
  app_keys=app_keys,
920
936
  expected_package_id=args.expected_package_id,
921
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")
922
940
  return context.builder.app_publish_verify(
923
941
  profile=args.profile,
924
942
  app_key=args.app_key,