@qingflow-tech/qingflow-cli 1.1.2 → 1.1.34

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 (103) hide show
  1. package/README.md +2 -2
  2. package/docs/local-agent-install.md +10 -0
  3. package/package.json +1 -1
  4. package/pyproject.toml +1 -1
  5. package/skills/qingflow-cli/SKILL.md +61 -47
  6. package/skills/qingflow-cli/manifest.yaml +1 -1
  7. package/skills/qingflow-cli/reference/00-INDEX.md +35 -0
  8. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md +21 -477
  9. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md +44 -290
  10. package/skills/qingflow-cli/reference/QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md +11 -102
  11. package/skills/qingflow-cli/reference/builder/10-build-single-app.md +57 -0
  12. package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +84 -0
  13. package/skills/qingflow-cli/reference/builder/30-schema-fields.md +317 -0
  14. package/skills/qingflow-cli/reference/builder/40-layout.md +70 -0
  15. package/skills/qingflow-cli/reference/builder/50-views.md +540 -0
  16. package/skills/qingflow-cli/reference/builder/60-charts.md +328 -0
  17. package/skills/qingflow-cli/reference/builder/70-portal.md +372 -0
  18. package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +148 -0
  19. package/skills/qingflow-cli/reference/builder/90-workflow.md +35 -0
  20. package/skills/qingflow-cli/reference/builder/99-publish-verify.md +64 -0
  21. package/skills/qingflow-cli/reference/builder/README.md +62 -0
  22. package/skills/qingflow-cli/reference/builder/code-integrations/README.md +130 -0
  23. package/skills/qingflow-cli/reference/builder/code-integrations/code-block.md +66 -0
  24. package/skills/qingflow-cli/reference/builder/code-integrations/q-linker.md +77 -0
  25. package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +508 -0
  26. package/skills/qingflow-cli/reference/builder/reference/match-rules.md +229 -0
  27. package/skills/qingflow-cli/reference/builder/reference/workspace-icons.md +89 -0
  28. package/skills/qingflow-cli/reference/builder/workflow/01-overview.md +48 -0
  29. package/skills/qingflow-cli/reference/builder/workflow/02-update-mode.md +63 -0
  30. package/skills/qingflow-cli/reference/builder/workflow/03-flow-patterns.md +59 -0
  31. package/skills/qingflow-cli/reference/builder/workflow/04-stage1-business-modeling.md +131 -0
  32. package/skills/qingflow-cli/reference/builder/workflow/05-stage2-members-roles.md +29 -0
  33. package/skills/qingflow-cli/reference/builder/workflow/06-stage3-build-spec.md +211 -0
  34. package/skills/qingflow-cli/reference/builder/workflow/07-stage4-validate-spec.md +36 -0
  35. package/skills/qingflow-cli/reference/builder/workflow/08-stage5-apply-verify.md +66 -0
  36. package/skills/qingflow-cli/reference/builder/workflow/09-stage6-summary.md +88 -0
  37. package/skills/qingflow-cli/reference/builder/workflow/10-node-config-reference.md +93 -0
  38. package/skills/qingflow-cli/reference/builder/workflow/11-troubleshooting.md +18 -0
  39. package/skills/qingflow-cli/reference/builder/workflow/README.md +88 -0
  40. package/skills/qingflow-cli/reference/builder/workflow/workflow-schema.json +1754 -0
  41. package/skills/qingflow-cli/reference/charts_upsert_dashboard_starter.example.json +81 -0
  42. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +94 -0
  43. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +139 -0
  44. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_EXPLORATION_REPORT.md +84 -0
  45. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_FIELD_DATA_TYPES.md +129 -0
  46. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +195 -0
  47. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +159 -0
  48. package/skills/qingflow-cli/reference/examples/README.md +17 -0
  49. package/skills/qingflow-cli/reference/examples/associated/associated_resources_semantic_full.example.json +50 -0
  50. package/skills/qingflow-cli/reference/examples/buttons/custom_buttons_advanced.example.json +56 -0
  51. package/skills/qingflow-cli/reference/examples/charts/charts_remove.example.json +1 -0
  52. package/skills/qingflow-cli/reference/examples/charts/charts_reorder.example.json +1 -0
  53. package/skills/qingflow-cli/reference/examples/charts/charts_upsert_bar.example.json +8 -0
  54. package/skills/qingflow-cli/reference/examples/charts/charts_upsert_dashboard_starter.example.json +118 -0
  55. package/skills/qingflow-cli/reference/examples/charts/charts_upsert_minimal.example.json +13 -0
  56. package/skills/qingflow-cli/reference/examples/layout/layout_sections_full.example.json +22 -0
  57. package/skills/qingflow-cli/reference/examples/portal/portal_sections_all_types.example.json +145 -0
  58. package/skills/qingflow-cli/reference/examples/portal/portal_sections_five_types.example.json +126 -0
  59. package/skills/qingflow-cli/reference/examples/portal/portal_sections_standard_workbench.example.json +178 -0
  60. package/skills/qingflow-cli/reference/examples/record/record_insert_all_field_types.example.json +35 -0
  61. package/skills/qingflow-cli/reference/examples/schema/_batch_schema_complex.json +18 -0
  62. package/skills/qingflow-cli/reference/examples/schema/_batch_schema_scalar.json +17 -0
  63. package/skills/qingflow-cli/reference/examples/schema/schema_add_fields_minimal.example.json +7 -0
  64. package/skills/qingflow-cli/reference/examples/schema/schema_apply_add_fields_all_types.json +78 -0
  65. package/skills/qingflow-cli/reference/examples/schema/schema_multi_app_form_relation_layout.example.json +166 -0
  66. package/skills/qingflow-cli/reference/examples/schema/schema_single_app_form_standard.example.json +95 -0
  67. package/skills/qingflow-cli/reference/examples/views/views_batch_full.example.json +159 -0
  68. package/skills/qingflow-cli/reference/examples/views/views_upsert_table_minimal.example.json +42 -0
  69. package/skills/qingflow-cli/reference/portal_sections_all_types.example.json +18 -4
  70. package/skills/qingflow-cli/reference/portal_sections_five_types.example.json +3 -3
  71. package/skills/qingflow-cli/reference/portal_sections_standard_workbench.example.json +57 -7
  72. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md +22 -0
  73. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_DELETE_WORKFLOW.md +31 -0
  74. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md +176 -0
  75. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +167 -0
  76. package/skills/qingflow-cli/reference/record/analysis/README.md +130 -0
  77. package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +91 -0
  78. package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +112 -0
  79. package/skills/qingflow-cli/reference/record/analysis/business-context.md +74 -0
  80. package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +69 -0
  81. package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +106 -0
  82. package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +172 -0
  83. package/skills/qingflow-cli/reference/record/analysis/report-format.md +76 -0
  84. package/skills/qingflow-cli/reference/record/insert/README.md +125 -0
  85. package/skills/qingflow-cli/reference/schema_apply_add_fields_all_types.json +1 -1
  86. package/skills/qingflow-cli/reference/task/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md +153 -0
  87. package/skills/qingflow-cli/reference/task/ops/README.md +134 -0
  88. package/skills/qingflow-cli/reference/task/ops/environments.md +43 -0
  89. package/skills/qingflow-cli/reference/task/ops/workflow-usage.md +29 -0
  90. package/skills/qingflow-cli/reference/views_upsert_table_minimal.example.json +42 -7
  91. package/skills/qingflow-cli/scripts/validate_system_build_summary.py +124 -0
  92. package/skills/qingflow-cli/scripts/workflow/diff_flow_spec.py +275 -0
  93. package/skills/qingflow-cli/scripts/workflow/validate_flow_spec.py +627 -0
  94. package/src/qingflow_mcp/builder_facade/models.py +413 -119
  95. package/src/qingflow_mcp/builder_facade/service.py +4238 -666
  96. package/src/qingflow_mcp/cli/commands/builder.py +232 -75
  97. package/src/qingflow_mcp/cli/main.py +34 -0
  98. package/src/qingflow_mcp/public_surface.py +10 -0
  99. package/src/qingflow_mcp/server_app_builder.py +62 -34
  100. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
  101. package/src/qingflow_mcp/solution/executor.py +2 -0
  102. package/src/qingflow_mcp/tools/ai_builder_tools.py +2649 -457
  103. package/src/qingflow_mcp/tools/record_tools.py +23 -11
@@ -152,6 +152,8 @@ BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
152
152
  PublicToolSpec(BUILDER_DOMAIN, "app_get_views", ("app_get_views",), ("builder", "app", "get", "views"), has_contract=True, cli_show_effective_context=True),
153
153
  PublicToolSpec(BUILDER_DOMAIN, "app_get_flow", ("app_get_flow",), ("builder", "app", "get", "flow"), has_contract=True, cli_show_effective_context=True),
154
154
  PublicToolSpec(BUILDER_DOMAIN, "app_get_charts", ("app_get_charts",), ("builder", "app", "get", "charts"), has_contract=True, cli_show_effective_context=True),
155
+ PublicToolSpec(BUILDER_DOMAIN, "app_get_buttons", ("app_get_buttons",), ("builder", "app", "get", "buttons"), has_contract=True, cli_show_effective_context=True),
156
+ PublicToolSpec(BUILDER_DOMAIN, "app_get_associated_resources", ("app_get_associated_resources",), ("builder", "app", "get", "associated-resources"), has_contract=True, cli_show_effective_context=True),
155
157
  PublicToolSpec(BUILDER_DOMAIN, "portal_list", ("portal_list",), ("builder", "portal", "list"), has_contract=True, cli_show_effective_context=True),
156
158
  PublicToolSpec(BUILDER_DOMAIN, "portal_get", ("portal_get",), ("builder", "portal", "get"), has_contract=True, cli_show_effective_context=True),
157
159
  PublicToolSpec(BUILDER_DOMAIN, "view_get", ("view_get",), ("builder", "view", "get"), has_contract=True, cli_show_effective_context=True),
@@ -176,6 +178,14 @@ PUBLIC_TOOL_BY_CLI_ROUTE: dict[tuple[str, ...], PublicToolSpec] = {
176
178
  for spec in ALL_PUBLIC_TOOL_SPECS
177
179
  if spec.cli_public and spec.cli_route is not None
178
180
  }
181
+ PUBLIC_TOOL_BY_CLI_ROUTE.update(
182
+ {
183
+ ("builder", "button", "get"): PUBLIC_TOOL_BY_KEY[tool_key(BUILDER_DOMAIN, "app_get_buttons")],
184
+ ("builder", "associated-resource", "get"): PUBLIC_TOOL_BY_KEY[
185
+ tool_key(BUILDER_DOMAIN, "app_get_associated_resources")
186
+ ],
187
+ }
188
+ )
179
189
 
180
190
 
181
191
  def public_tool_specs(domain: str | None = None) -> tuple[PublicToolSpec, ...]:
@@ -37,22 +37,22 @@ def build_builder_server() -> FastMCP:
37
37
  "Use solution_install when the user explicitly wants to install a packaged solution/template by solution_key, optionally copying bundled demo data. "
38
38
  "Use package_list to find visible app packages by keyword and package_get to read package detail before editing; if creating or updating an app package may be appropriate, use package_apply with explicit user intent; otherwise use app_resolve to locate app resources, "
39
39
  "Use workspace_icon_catalog_get before creating app packages, apps, or portals when supported icon/color candidates are needed; new workspace resources require explicit non-template icon + color, and the CLI validates choices without inferring business defaults. "
40
- "app_get as the default app map read, then app_get_fields/app_repair_code_blocks/app_get_layout/app_get_views/app_get_flow/app_flow_get/app_flow_get_schema/app_get_charts/portal_list/portal_get/view_get/chart_get for focused configuration reads, "
40
+ "app_get as the default app map read, then app_get_fields/app_repair_code_blocks/app_get_layout/app_get_views/app_get_flow/app_get_charts/portal_list/portal_get/view_get/chart_get for focused configuration reads, "
41
41
  "member_search/role_search/role_create when workflow assignees must come from the directory or role catalog, preferring roles over explicit members unless the user explicitly names members, "
42
- "then app_schema_apply/app_layout_apply/app_flow_apply/app_views_apply/app_custom_buttons_apply/app_associated_resources_apply/app_charts_apply/portal_apply/portal_delete to execute normalized patches; these apply/delete tools perform planning, normalization, and dependency checks internally where applicable. Schema/layout/views noop requests skip publish, app_custom_buttons_apply and app_associated_resources_apply publish after at least one write succeeds and expose no draft-only parameter, charts are immediate-live without publish and resolve targets by chart_id first then exact unique chart name, portal updates use replace semantics only when sections are supplied and edit-mode base-info-only updates may omit sections, portal delete separates DELETE execution from readback verification, portal pc layout is a 24-column grid and mobile is a 6-column grid so omit position or use layout_preset when unsure, publish=false only guarantees draft/base-info updates for tools that still expose that parameter, and flow should use publish=false whenever you only want draft/precheck behavior. "
42
+ "then app_schema_apply/app_layout_apply/app_flow_apply/app_views_apply/app_associated_resources_apply/app_charts_apply/portal_apply/portal_delete to execute normalized patches; app_custom_buttons_apply is maintenance-only for explicit standalone button work outside normal view design. These apply/delete tools perform basic validation, normalization, writes, and readback verification where applicable. Schema/layout/views noop requests skip publish, app_custom_buttons_apply and app_associated_resources_apply publish after at least one write succeeds and expose no draft-only parameter, charts are immediate-live without publish and resolve targets by chart_id first then exact unique chart name, portal updates use replace semantics only when sections are supplied and edit-mode base-info-only updates may omit sections, portal delete separates DELETE execution from readback verification, portal pc layout is a 24-column grid and mobile is a 6-column grid so omit position or use layout_preset when unsure, publish=false only guarantees draft/base-info updates for tools that still expose that parameter, and flow should use publish=false whenever you only want draft/precheck behavior. "
43
43
  "Builder apply/write outputs include schema_version, operation, summary, and resources[]; use resources[].id/key/name/ids/parent as the stable UI and agent display entry, and keep legacy fields such as field_diff/views_diff/chart_results only for compatibility or troubleshooting. "
44
44
  "For existing object parameter replacement, prefer patch_views, patch_buttons, patch_resources, and patch_charts with set/unset; the tool reads current config and full-saves internally, while upsert_* is for creation or full target configuration and should not be used as an incomplete partial update. "
45
45
  "For builder delete/remove apply results, separate delete execution from readback verification: after DELETE is sent, resources expose delete_executed, readback_status, and safe_to_retry_delete=false. If readback_status is unavailable or still_exists, do not blindly repeat the delete; confirm later with app_get/view_get/chart_get or the relevant apply readback. Views/buttons use single-item readback; associated resources use one app-level resource-pool readback because there is no confirmed single-item GET. "
46
46
  "For builder write/apply results, separate write execution from final readback verification: status=partial_success with write_executed=true and safe_to_retry=false means the write was sent or succeeded, while final readback, publish verification, or metadata confirmation is pending or permission-restricted. Report it as written but unverified; do not repeat the same write blindly. "
47
47
  "For app_schema_apply, configure data title and data cover directly in field JSON with as_data_title=true and as_data_cover=true; data title is required and exactly one field may be marked, while data cover is optional and must be a top-level attachment field. For multi-app creation, pass apps[]/--apps-file on app_schema_apply; each item may have client_key, and relation fields may use target_app_ref to point at another same-call client_key. "
48
48
  "For app_views_apply, keep fixed saved filters in filters and configure the frontend query panel separately with query_conditions; query_conditions.rows is a matrix of field names compiled to backend queryCondition queIds. New views default associated report/view display to visible with limit_type=all; existing views preserve their current associated display unless associated_resources is explicitly patched. View writes use ViewManagementAuth (beingViewManageStatus), falling back to DataManageAuth when advanced app permissions are not enabled. "
49
- "For custom button body create/update/delete and view placement, use app_custom_buttons_apply. Button body writes require EditAppAuth; view_configs placement writes require ViewManagementAuth, matching the backend viewConfig route. For addData buttons, prefer trigger_add_data_config.target_app_key + field_mappings/default_values; do not ask agents to write raw que_relation unless maintaining a legacy config. field_mappings.source_field accepts source schema fields and supported system fields: 数据ID/row_record_id/apply_id/_id means current record id (-17), 编号/record_number means visible record number (0). To fill a target relation with the current record, map {'source_field': '数据ID', 'target_field': '目标引用字段'}; default_values is only for static constants. View button bindings merge by default and merge-mode view_configs must include buttons; use view_configs[].mode=replace or explicit buttons=[] only when clearing/replacing existing bindings is intended. Builder view_key arguments are raw keys from app_get.views[].view_key and must not be prefixed with custom:. "
49
+ "For ordinary business buttons, declare them on the target view with app_views_apply.views[].action_buttons; app_views_apply writes the view first, then creates/binds the button. Use app_custom_buttons_apply only when the user explicitly asks to maintain existing standalone buttons outside view design, such as custom style/icon changes, cross-view reuse, deleting button bodies, bulk binding reorder, or exact qRobot/wings config. Button body writes require EditAppAuth; view_configs placement writes require ViewManagementAuth, matching the backend viewConfig route. For addData buttons, prefer action_buttons.target_app_key + field_mappings/default_values on the view path; do not ask agents to write raw que_relation unless maintaining a legacy config. field_mappings.source_field accepts source schema fields and supported system fields: 数据ID/row_record_id/apply_id/_id means current record id (-17), 编号/record_number means visible record number (0). To fill a target relation with the current record, map {'source_field': '数据ID', 'target_field': '目标引用字段'}; default_values is only for static constants. View button bindings merge by default; standalone view_configs are only for explicit maintenance. Builder view_key arguments are raw keys from app_get.views[].view_key and must not be prefixed with custom:. "
50
50
  "For BI reports, keep report-body development separate from Qingflow in-app display: use app_charts_apply to create, update, remove, or reorder app-source QingBI chart bodies/configs with dataSourceType=qingflow; chart dimension/metric/filter/query fields must come from app_get_fields.chart_fields, not record schema or form-only fields; dataset BI reports are not created or edited by app_charts_apply yet and should be created in QingBI first, then attached with app_associated_resources_apply using report_source=dataset. "
51
51
  "For associated views/reports, use app_associated_resources_apply. Use match_mappings for filtering associated resources: dynamic current-record conditions use source_field, static conditions use value. match_mappings also supports 数据ID(-17) and 编号(0). Do not ask agents to write raw match_rules unless preserving a legacy backend config. "
52
52
  "For associated reports/views, use app_associated_resources_apply for both the app-level associated_resources pool and per-view display config; associated_item_id is the app-level form_asos_chart.id, and view_configs/remove/reorder may also pass an existing resource's chart_id/chart_key/view_key because the tool resolves those to the internal id. Before creating an associated resource, read app_get.associated_resources and reuse an existing matching target_app_key + view_key/chart_key through patch_resources; client_key only works inside one apply call and is not persisted. Do not ask agents to pass backend raw sourceType: views infer the internal Qingflow view source, reports default to BI app reports, and dataset reports use report_source=dataset. "
53
53
  "For code_block fields with output bindings, always use qf_output assignment rather than const/let qf_output, and use app_repair_code_blocks when an existing form hangs because output-bound fields stay loading. "
54
54
  "Use package_apply to manage package metadata, visibility, grouping, and ordering, and app_publish_verify for explicit final publish verification. "
55
- "For workflow edits, use app_flow_get_schema and app_flow_get (GET-first), author a complete WorkflowSpecDTO in spec, then app_flow_apply; CLI equivalent is qingflow builder flow schema|get|apply --spec-file. Do not use general-server legacy workflow config reads (auditNodes list/detail, global settings, qsource config reads); they are removed. "
55
+ "For workflow edits, use app_flow_get_schema/app_flow_get when raw WorkflowSpec shape is needed, then app_flow_apply with spec for full replacement or patch_nodes for targeted node edits. Declare node assignees and editable fields explicitly. "
56
56
  "If builder writes are blocked by the current user's own edit lock, use app_release_edit_lock_if_mine with the lock owner details from the failed result. "
57
57
  "Do not handcraft internal solution payloads or rely on build_id/stage/repair. "
58
58
  "If the current MCP capability is unsupported, the workflow is awkward, or the user's need still cannot be satisfied after reasonable use, first summarize the gap, ask whether to submit feedback, and call feedback_submit only after explicit user confirmation."
@@ -197,7 +197,6 @@ def build_builder_server() -> FastMCP:
197
197
  profile: str = DEFAULT_PROFILE,
198
198
  package_id: int | None = None,
199
199
  package_name: str | None = None,
200
- create_if_missing: bool = False,
201
200
  icon: str | None = None,
202
201
  color: str | None = None,
203
202
  visibility: dict | None = None,
@@ -208,7 +207,6 @@ def build_builder_server() -> FastMCP:
208
207
  profile=profile,
209
208
  package_id=package_id,
210
209
  package_name=package_name,
211
- create_if_missing=create_if_missing,
212
210
  icon=icon,
213
211
  color=color,
214
212
  visibility=visibility,
@@ -321,6 +319,7 @@ def build_builder_server() -> FastMCP:
321
319
  patch_buttons: list[dict] | None = None,
322
320
  remove_buttons: list[dict] | None = None,
323
321
  view_configs: list[dict] | None = None,
322
+ apps: list[dict] | None = None,
324
323
  ) -> dict:
325
324
  return ai_builder.app_custom_buttons_apply(
326
325
  profile=profile,
@@ -329,6 +328,7 @@ def build_builder_server() -> FastMCP:
329
328
  patch_buttons=patch_buttons or [],
330
329
  remove_buttons=remove_buttons or [],
331
330
  view_configs=view_configs or [],
331
+ apps=apps,
332
332
  )
333
333
 
334
334
  @server.tool()
@@ -340,6 +340,7 @@ def build_builder_server() -> FastMCP:
340
340
  remove_associated_item_ids: list[int] | None = None,
341
341
  reorder_associated_item_ids: list[int] | None = None,
342
342
  view_configs: list[dict] | None = None,
343
+ apps: list[dict] | None = None,
343
344
  ) -> dict:
344
345
  return ai_builder.app_associated_resources_apply(
345
346
  profile=profile,
@@ -349,15 +350,16 @@ def build_builder_server() -> FastMCP:
349
350
  remove_associated_item_ids=remove_associated_item_ids or [],
350
351
  reorder_associated_item_ids=reorder_associated_item_ids or [],
351
352
  view_configs=view_configs or [],
353
+ apps=apps,
352
354
  )
353
355
 
354
356
  @server.tool()
355
- def app_get(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
356
- return ai_builder.app_get(profile=profile, app_key=app_key)
357
+ def app_get(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> dict:
358
+ return ai_builder.app_get(profile=profile, app_key=app_key, app_keys=app_keys)
357
359
 
358
360
  @server.tool()
359
- def app_get_fields(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
360
- return ai_builder.app_get_fields(profile=profile, app_key=app_key)
361
+ def app_get_fields(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> dict:
362
+ return ai_builder.app_get_fields(profile=profile, app_key=app_key, app_keys=app_keys)
361
363
 
362
364
  @server.tool()
363
365
  def app_repair_code_blocks(
@@ -369,20 +371,28 @@ def build_builder_server() -> FastMCP:
369
371
  return ai_builder.app_repair_code_blocks(profile=profile, app_key=app_key, field=field, apply=apply)
370
372
 
371
373
  @server.tool()
372
- def app_get_layout(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
373
- return ai_builder.app_get_layout(profile=profile, app_key=app_key)
374
+ def app_get_layout(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> dict:
375
+ return ai_builder.app_get_layout(profile=profile, app_key=app_key, app_keys=app_keys)
376
+
377
+ @server.tool()
378
+ def app_get_views(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> dict:
379
+ return ai_builder.app_get_views(profile=profile, app_key=app_key, app_keys=app_keys)
380
+
381
+ @server.tool()
382
+ def app_get_flow(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> dict:
383
+ return ai_builder.app_get_flow(profile=profile, app_key=app_key, app_keys=app_keys)
374
384
 
375
385
  @server.tool()
376
- def app_get_views(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
377
- return ai_builder.app_get_views(profile=profile, app_key=app_key)
386
+ def app_get_charts(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> dict:
387
+ return ai_builder.app_get_charts(profile=profile, app_key=app_key, app_keys=app_keys)
378
388
 
379
389
  @server.tool()
380
- def app_get_flow(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
381
- return ai_builder.app_get_flow(profile=profile, app_key=app_key)
390
+ def app_get_buttons(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> dict:
391
+ return ai_builder.app_get_buttons(profile=profile, app_key=app_key, app_keys=app_keys)
382
392
 
383
393
  @server.tool()
384
- def app_get_charts(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
385
- return ai_builder.app_get_charts(profile=profile, app_key=app_key)
394
+ def app_get_associated_resources(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> dict:
395
+ return ai_builder.app_get_associated_resources(profile=profile, app_key=app_key, app_keys=app_keys)
386
396
 
387
397
  @server.tool()
388
398
  def portal_list(profile: str = DEFAULT_PROFILE) -> dict:
@@ -414,18 +424,18 @@ def build_builder_server() -> FastMCP:
414
424
  icon: str = "",
415
425
  color: str = "",
416
426
  visibility: dict | None = None,
417
- create_if_missing: bool = False,
418
427
  publish: bool = True,
428
+ form: list[dict] | None = None,
419
429
  add_fields: list[dict] | None = None,
420
430
  update_fields: list[dict] | None = None,
421
431
  remove_fields: list[dict] | None = None,
422
432
  apps: list[dict] | None = None,
423
433
  ) -> dict:
424
434
  if apps:
425
- if app_key or app_name or app_title or add_fields or update_fields or remove_fields:
435
+ if app_key or app_name or app_title or form or add_fields or update_fields or remove_fields:
426
436
  return _config_failure(
427
- "app_schema_apply multi-app mode accepts package_id/create_if_missing plus apps only.",
428
- fix_hint="Use `apps` for batch mode, or use the single-app arguments without `apps`.",
437
+ "app_schema_apply multi-app mode accepts package_id plus apps only.",
438
+ fix_hint="Use apps[].form for batch mode, or use the single-app arguments without apps.",
429
439
  )
430
440
  if package_id is None:
431
441
  return _config_failure(
@@ -436,8 +446,8 @@ def build_builder_server() -> FastMCP:
436
446
  profile=profile,
437
447
  package_id=package_id,
438
448
  visibility=visibility,
439
- create_if_missing=create_if_missing,
440
449
  publish=publish,
450
+ form=None,
441
451
  add_fields=[],
442
452
  update_fields=[],
443
453
  remove_fields=[],
@@ -447,16 +457,17 @@ def build_builder_server() -> FastMCP:
447
457
  has_app_name = bool((app_name or "").strip())
448
458
  has_app_title = bool((app_title or "").strip())
449
459
  has_package_id = package_id is not None
460
+ create_by_shape = has_package_id and (has_app_name or has_app_title)
450
461
  if has_app_key:
451
- if create_if_missing or has_package_id:
462
+ if has_package_id:
452
463
  return _config_failure(
453
464
  "app_schema_apply edit mode accepts app_key and optional app_name rename only.",
454
- fix_hint="For existing apps, use `app_key` and optionally `app_name`. For create mode, use `package_id + app_name + create_if_missing=true`.",
465
+ fix_hint="For existing apps, use `app_key` and optionally `app_name`. For create mode, use `package_id + app_name`.",
455
466
  )
456
- elif not (create_if_missing and has_package_id and (has_app_name or has_app_title)):
467
+ elif not create_by_shape:
457
468
  return _config_failure(
458
- "app_schema_apply create mode requires package_id, app_name, and create_if_missing=true.",
459
- fix_hint="Use `app_key` for existing apps, or pass `package_id + app_name + create_if_missing=true` to create a new app.",
469
+ "app_schema_apply create mode requires package_id and app_name.",
470
+ fix_hint="Use `app_key` for existing apps, or pass `package_id + app_name` to create a new app.",
460
471
  )
461
472
  return ai_builder.app_schema_apply(
462
473
  profile=profile,
@@ -467,12 +478,12 @@ def build_builder_server() -> FastMCP:
467
478
  icon=icon,
468
479
  color=color,
469
480
  visibility=visibility,
470
- create_if_missing=create_if_missing,
471
481
  publish=publish,
482
+ form=form or [],
472
483
  add_fields=add_fields or [],
473
484
  update_fields=update_fields or [],
474
485
  remove_fields=remove_fields or [],
475
- apps=[],
486
+ apps=None,
476
487
  )
477
488
 
478
489
  @server.tool()
@@ -482,12 +493,17 @@ def build_builder_server() -> FastMCP:
482
493
  mode: str = "merge",
483
494
  publish: bool = True,
484
495
  sections: list[dict] | None = None,
496
+ apps: list[dict] | None = None,
485
497
  ) -> dict:
486
- return ai_builder.app_layout_apply(profile=profile, app_key=app_key, mode=mode, publish=publish, sections=sections or [])
498
+ return ai_builder.app_layout_apply(profile=profile, app_key=app_key, mode=mode, publish=publish, sections=sections or [], apps=apps)
487
499
 
488
500
  @server.tool()
489
- def app_flow_get(profile: str = DEFAULT_PROFILE, app_key: str = "", version_id: str = "") -> dict:
490
- return ai_builder.app_get_flow(profile=profile, app_key=app_key, version_id=version_id or None)
501
+ def app_flow_get(
502
+ profile: str = DEFAULT_PROFILE,
503
+ app_key: str = "",
504
+ version_id: str = "",
505
+ ) -> dict:
506
+ return ai_builder.app_flow_get(profile=profile, app_key=app_key, version_id=version_id or None)
491
507
 
492
508
  @server.tool()
493
509
  def app_flow_get_schema(profile: str = DEFAULT_PROFILE, schema_version: str = "") -> dict:
@@ -501,14 +517,16 @@ def build_builder_server() -> FastMCP:
501
517
  spec: dict | None = None,
502
518
  idempotency_key: str = "",
503
519
  schema_version: str = "",
520
+ patch_nodes: list[dict] | None = None,
504
521
  ) -> dict:
505
522
  return ai_builder.app_flow_apply(
506
523
  profile=profile,
507
524
  app_key=app_key,
508
525
  publish=publish,
509
- spec=spec or {},
526
+ spec=spec,
510
527
  idempotency_key=idempotency_key or None,
511
528
  schema_version=schema_version or None,
529
+ patch_nodes=patch_nodes,
512
530
  )
513
531
 
514
532
  @server.tool()
@@ -519,6 +537,8 @@ def build_builder_server() -> FastMCP:
519
537
  upsert_views: list[dict] | None = None,
520
538
  patch_views: list[dict] | None = None,
521
539
  remove_views: list[str] | None = None,
540
+ apps: list[dict] | None = None,
541
+ views: list[dict] | None = None,
522
542
  ) -> dict:
523
543
  return ai_builder.app_views_apply(
524
544
  profile=profile,
@@ -527,6 +547,8 @@ def build_builder_server() -> FastMCP:
527
547
  upsert_views=upsert_views or [],
528
548
  patch_views=patch_views or [],
529
549
  remove_views=remove_views or [],
550
+ apps=apps,
551
+ views=views,
530
552
  )
531
553
 
532
554
  @server.tool()
@@ -537,6 +559,7 @@ def build_builder_server() -> FastMCP:
537
559
  patch_charts: list[dict] | None = None,
538
560
  remove_chart_ids: list[str] | None = None,
539
561
  reorder_chart_ids: list[str] | None = None,
562
+ apps: list[dict] | None = None,
540
563
  ) -> dict:
541
564
  return ai_builder.app_charts_apply(
542
565
  profile=profile,
@@ -545,6 +568,7 @@ def build_builder_server() -> FastMCP:
545
568
  patch_charts=patch_charts or [],
546
569
  remove_chart_ids=remove_chart_ids or [],
547
570
  reorder_chart_ids=reorder_chart_ids or [],
571
+ apps=apps,
548
572
  )
549
573
 
550
574
  @server.tool()
@@ -566,6 +590,7 @@ def build_builder_server() -> FastMCP:
566
590
  dash_global_config: dict | None = None,
567
591
  config: dict | None = None,
568
592
  payload: dict | None = None,
593
+ patch_sections: list[dict] | None = None,
569
594
  ) -> dict:
570
595
  payload = payload if isinstance(payload, dict) else {}
571
596
  has_dash_key = bool((dash_key or "").strip())
@@ -601,6 +626,7 @@ def build_builder_server() -> FastMCP:
601
626
  dash_global_config=dash_global_config,
602
627
  config=config or {},
603
628
  payload=payload,
629
+ patch_sections=patch_sections,
604
630
  )
605
631
 
606
632
  @server.tool()
@@ -617,11 +643,13 @@ def build_builder_server() -> FastMCP:
617
643
  def app_publish_verify(
618
644
  profile: str = DEFAULT_PROFILE,
619
645
  app_key: str = "",
646
+ app_keys: list[str] | None = None,
620
647
  expected_package_id: int | None = None,
621
648
  ) -> dict:
622
649
  return ai_builder.app_publish_verify(
623
650
  profile=profile,
624
651
  app_key=app_key,
652
+ app_keys=app_keys,
625
653
  expected_package_id=expected_package_id,
626
654
  )
627
655
 
@@ -0,0 +1,173 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ..spec_models import EntitySpec, WorkflowNodeType
6
+
7
+
8
+ WORKFLOW_TYPE_MAP = {
9
+ WorkflowNodeType.start: {"type": 0, "dealType": 3},
10
+ WorkflowNodeType.branch: {"type": 1, "dealType": None},
11
+ WorkflowNodeType.audit: {"type": 0, "dealType": 0},
12
+ WorkflowNodeType.fill: {"type": 0, "dealType": 1},
13
+ WorkflowNodeType.copy: {"type": 0, "dealType": 2},
14
+ WorkflowNodeType.webhook: {"type": 3, "dealType": 10},
15
+ WorkflowNodeType.condition: {"type": 2, "dealType": None},
16
+ }
17
+
18
+
19
+ def _default_audit_user_infos() -> dict[str, Any]:
20
+ return {
21
+ "member": [],
22
+ "depart": [],
23
+ "role": [],
24
+ "dynamic": [],
25
+ "includeSubDeparts": None,
26
+ "externalMemberList": [],
27
+ "externalDepartList": [],
28
+ "role_refs": [],
29
+ }
30
+
31
+
32
+ def compile_workflow(entity: EntitySpec) -> dict[str, Any] | None:
33
+ workflow = entity.workflow
34
+ if workflow is None or not workflow.enabled:
35
+ return None
36
+ actions: list[dict[str, Any]] = []
37
+ seen_node_ids: set[str] = set()
38
+ created_extra_branch_lanes: set[str] = set()
39
+ start_node_ids = {
40
+ node.node_id
41
+ for node in workflow.nodes
42
+ if node.node_type == WorkflowNodeType.start
43
+ }
44
+ for node in workflow.nodes:
45
+ if node.node_type == WorkflowNodeType.start:
46
+ seen_node_ids.add(node.node_id)
47
+ continue
48
+ if node.parent_node_id and node.parent_node_id not in seen_node_ids:
49
+ raise ValueError(f"workflow node '{node.node_id}' must appear after parent node '{node.parent_node_id}'")
50
+ if node.branch_parent_id and node.branch_parent_id not in seen_node_ids:
51
+ raise ValueError(f"workflow node '{node.node_id}' must appear after branch node '{node.branch_parent_id}'")
52
+ branch_index = _branch_index(node)
53
+ branch_lane_ref = _branch_lane_ref(node.branch_parent_id, branch_index) if node.branch_parent_id else None
54
+ if branch_lane_ref and branch_index > 2 and branch_lane_ref not in created_extra_branch_lanes:
55
+ actions.append(
56
+ {
57
+ "action": "create_sub_branch",
58
+ "node_id": branch_lane_ref,
59
+ "payload": {
60
+ "editVersionNo": 1,
61
+ "auditNodeRef": node.branch_parent_id,
62
+ },
63
+ }
64
+ )
65
+ created_extra_branch_lanes.add(branch_lane_ref)
66
+ lane_only = bool((node.config or {}).get("__lane_only__")) and node.node_type == WorkflowNodeType.condition and branch_lane_ref
67
+ if lane_only:
68
+ lane_payload = {key: value for key, value in node.config.items() if key != "__lane_only__"}
69
+ actions.append(
70
+ {
71
+ "action": "update_node",
72
+ "node_id": branch_lane_ref,
73
+ "node_name": node.name,
74
+ "node_type": node.node_type.value,
75
+ "payload": {
76
+ "editVersionNo": 1,
77
+ "auditNodeName": node.name,
78
+ "type": WORKFLOW_TYPE_MAP[node.node_type]["type"],
79
+ "dealType": WORKFLOW_TYPE_MAP[node.node_type]["dealType"],
80
+ **lane_payload,
81
+ },
82
+ }
83
+ )
84
+ seen_node_ids.add(node.node_id)
85
+ continue
86
+ actions.append(
87
+ {
88
+ "action": "add_node",
89
+ "node_id": node.node_id,
90
+ "node_name": node.name,
91
+ "node_type": node.node_type.value,
92
+ "payload": {
93
+ "editVersionNo": 1,
94
+ "auditNodeName": node.name,
95
+ "type": WORKFLOW_TYPE_MAP[node.node_type]["type"],
96
+ "dealType": WORKFLOW_TYPE_MAP[node.node_type]["dealType"],
97
+ "prevNodeRef": _prev_node_ref(node, branch_lane_ref, start_node_ids),
98
+ "auditUserInfos": _build_audit_user_infos(node)
99
+ if node.node_type in {WorkflowNodeType.audit, WorkflowNodeType.fill, WorkflowNodeType.copy}
100
+ else None,
101
+ **node.config,
102
+ },
103
+ }
104
+ )
105
+ seen_node_ids.add(node.node_id)
106
+ return {
107
+ "global_settings": {
108
+ "editVersionNo": 1,
109
+ **workflow.global_settings,
110
+ },
111
+ "actions": actions,
112
+ }
113
+
114
+
115
+ def _build_audit_user_infos(node) -> dict[str, Any]:
116
+ audit_user_infos = _default_audit_user_infos()
117
+ assignees = node.assignees or {}
118
+ member_uids = assignees.get("member_uids") or []
119
+ if member_uids:
120
+ audit_user_infos["member"] = [
121
+ {"uid": uid, "beingFrontendConfig": True}
122
+ for uid in member_uids
123
+ if isinstance(uid, int) and uid > 0
124
+ ]
125
+ role_refs = assignees.get("role_refs") or []
126
+ if role_refs:
127
+ audit_user_infos["role_refs"] = [role_ref for role_ref in role_refs if role_ref]
128
+ role_entries = assignees.get("role_entries") or []
129
+ if role_entries:
130
+ audit_user_infos["role"] = [
131
+ {
132
+ "roleId": int(entry.get("roleId") or entry.get("role_id")),
133
+ "roleName": entry.get("roleName") or entry.get("role_name") or str(entry.get("roleId") or entry.get("role_id")),
134
+ "roleIcon": entry.get("roleIcon") or entry.get("role_icon") or "ex-user-outlined",
135
+ "beingFrontendConfig": True,
136
+ }
137
+ for entry in role_entries
138
+ if isinstance(entry, dict) and isinstance(entry.get("roleId") or entry.get("role_id"), int) and int(entry.get("roleId") or entry.get("role_id")) > 0
139
+ ]
140
+ include_sub_departs = assignees.get("include_sub_departs")
141
+ if include_sub_departs is not None:
142
+ audit_user_infos["includeSubDeparts"] = bool(include_sub_departs)
143
+ return audit_user_infos
144
+
145
+
146
+ def _prev_node_ref(node, branch_lane_ref: str | None, start_node_ids: set[str]) -> str:
147
+ if branch_lane_ref:
148
+ if node.parent_node_id and node.parent_node_id != node.branch_parent_id:
149
+ return node.parent_node_id
150
+ return branch_lane_ref
151
+ if node.parent_node_id in start_node_ids:
152
+ return "__applicant__"
153
+ return node.parent_node_id or "__applicant__"
154
+
155
+
156
+ def _branch_index(node) -> int:
157
+ config = node.config or {}
158
+ raw_value = getattr(node, "branch_index", None)
159
+ if raw_value is None:
160
+ raw_value = config.get("branch_index", config.get("branchIndex", config.get("lane_index", config.get("laneIndex", 1))))
161
+ try:
162
+ branch_index = int(raw_value)
163
+ except (TypeError, ValueError) as exc:
164
+ raise ValueError(f"workflow node '{node.node_id}' has invalid branch index '{raw_value}'") from exc
165
+ if branch_index <= 0:
166
+ raise ValueError(f"workflow node '{node.node_id}' branch index must be positive")
167
+ return branch_index
168
+
169
+
170
+ def _branch_lane_ref(branch_parent_id: str | None, branch_index: int) -> str:
171
+ if not branch_parent_id:
172
+ raise ValueError("branch_parent_id is required for branch lane references")
173
+ return f"__branch_lane__{branch_parent_id}__{branch_index}"
@@ -39,6 +39,7 @@ class SolutionExecutor:
39
39
  chart_tools: QingbiReportTools,
40
40
  portal_tools: PortalTools,
41
41
  navigation_tools: NavigationTools,
42
+ workflow_tools: Any | None = None,
42
43
  ) -> None:
43
44
  self.workspace_tools = workspace_tools
44
45
  self.package_tools = package_tools
@@ -49,6 +50,7 @@ class SolutionExecutor:
49
50
  self.chart_tools = chart_tools
50
51
  self.portal_tools = portal_tools
51
52
  self.navigation_tools = navigation_tools
53
+ self.workflow_tools = workflow_tools
52
54
 
53
55
  def execute(
54
56
  self,