@josephyan/qingflow-cli 1.0.11 → 1.1.2

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 (67) hide show
  1. package/README.md +3 -3
  2. package/npm/bin/qingflow.mjs +40 -2
  3. package/npm/lib/runtime.mjs +386 -15
  4. package/npm/scripts/postinstall.mjs +7 -2
  5. package/package.json +1 -1
  6. package/pyproject.toml +1 -1
  7. package/skills/qingflow-cli/SKILL.md +440 -0
  8. package/skills/qingflow-cli/manifest.yaml +10 -0
  9. package/skills/qingflow-cli/reference/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +94 -0
  10. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md +485 -0
  11. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_CHARTS_WORKFLOW.md +237 -0
  12. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_MATCH_RULES.md +137 -0
  13. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_PORTAL_WORKFLOW.md +263 -0
  14. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md +304 -0
  15. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_WORKSPACE_ICONS.md +41 -0
  16. package/skills/qingflow-cli/reference/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +139 -0
  17. package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +84 -0
  18. package/skills/qingflow-cli/reference/QINGFLOW_CLI_FIELD_DATA_TYPES.md +129 -0
  19. package/skills/qingflow-cli/reference/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +195 -0
  20. package/skills/qingflow-cli/reference/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +159 -0
  21. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md +20 -0
  22. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md +176 -0
  23. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +163 -0
  24. package/skills/qingflow-cli/reference/QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md +107 -0
  25. package/skills/qingflow-cli/reference/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md +151 -0
  26. package/skills/qingflow-cli/reference/_batch_schema_complex.json +18 -0
  27. package/skills/qingflow-cli/reference/_batch_schema_scalar.json +17 -0
  28. package/skills/qingflow-cli/reference/charts_remove.example.json +1 -0
  29. package/skills/qingflow-cli/reference/charts_reorder.example.json +1 -0
  30. package/skills/qingflow-cli/reference/charts_upsert_bar.example.json +8 -0
  31. package/skills/qingflow-cli/reference/charts_upsert_dashboard_starter.example.json +37 -0
  32. package/skills/qingflow-cli/reference/charts_upsert_minimal.example.json +13 -0
  33. package/skills/qingflow-cli/reference/portal_sections_all_types.example.json +131 -0
  34. package/skills/qingflow-cli/reference/portal_sections_five_types.example.json +126 -0
  35. package/skills/qingflow-cli/reference/portal_sections_standard_workbench.example.json +128 -0
  36. package/skills/qingflow-cli/reference/schema_add_fields_minimal.example.json +7 -0
  37. package/skills/qingflow-cli/reference/schema_apply_add_fields_all_types.json +78 -0
  38. package/skills/qingflow-cli/reference/views_upsert_table_minimal.example.json +7 -0
  39. package/skills/qingflow-cli/scripts/builder-package-from-app-list.py +140 -0
  40. package/skills/qingflow-cli/scripts/find-app-by-keyword.py +132 -0
  41. package/skills/qingflow-cli/scripts/validate_qingflow_output_files.py +87 -0
  42. package/src/qingflow_mcp/__init__.py +1 -1
  43. package/src/qingflow_mcp/builder_facade/models.py +532 -48
  44. package/src/qingflow_mcp/builder_facade/service.py +9194 -2384
  45. package/src/qingflow_mcp/builder_facade/workflow_spec.py +111 -0
  46. package/src/qingflow_mcp/cli/commands/app.py +3 -16
  47. package/src/qingflow_mcp/cli/commands/builder.py +354 -56
  48. package/src/qingflow_mcp/cli/commands/record.py +89 -2
  49. package/src/qingflow_mcp/cli/formatters.py +32 -1
  50. package/src/qingflow_mcp/cli/main.py +245 -3
  51. package/src/qingflow_mcp/public_surface.py +11 -8
  52. package/src/qingflow_mcp/response_trim.py +143 -14
  53. package/src/qingflow_mcp/server.py +15 -12
  54. package/src/qingflow_mcp/server_app_builder.py +108 -30
  55. package/src/qingflow_mcp/server_app_user.py +17 -18
  56. package/src/qingflow_mcp/solution/compiler/__init__.py +1 -3
  57. package/src/qingflow_mcp/solution/compiler/icon_utils.py +294 -0
  58. package/src/qingflow_mcp/solution/executor.py +3 -133
  59. package/src/qingflow_mcp/tools/ai_builder_tools.py +2617 -440
  60. package/src/qingflow_mcp/tools/app_tools.py +53 -8
  61. package/src/qingflow_mcp/tools/package_tools.py +16 -2
  62. package/src/qingflow_mcp/tools/record_tools.py +2095 -176
  63. package/src/qingflow_mcp/tools/resource_read_tools.py +3 -0
  64. package/src/qingflow_mcp/tools/solution_tools.py +30 -2
  65. package/src/qingflow_mcp/tools/workflow_tools.py +3 -31
  66. package/src/qingflow_mcp/version.py +110 -0
  67. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +0 -173
@@ -31,7 +31,7 @@ def build_user_server() -> FastMCP:
31
31
 
32
32
  ## App Discovery
33
33
 
34
- If `app_key` is unknown, use `app_list` or `app_search` first.
34
+ If `app_key` is unknown, use `app_list` first. Pass `query` to filter visible apps by keyword.
35
35
  If the app is known but the data range is not, use `app_get` first and choose from `accessible_views`.
36
36
  If an accessible view has `analysis_supported=false`, do not use it for `record_access` or `record_list`. `boardView` and `ganttView` are special UI views, not data-access targets.
37
37
  `view_get(view_id=...)` also returns `export_capability`; it only means there is a supported export route, not that export permission has been verified.
@@ -47,9 +47,9 @@ If an accessible view has `analysis_supported=false`, do not use it for `record_
47
47
  ## Schema-First Rule
48
48
 
49
49
  Call `record_insert_schema_get` before `record_insert`.
50
- Call `record_update_schema_get` before `record_update`.
50
+ For simple field changes after the target record is clear, call `record_update` directly. Use `record_update_schema_get` for diagnostics, ambiguous fields, or complex writable-scope inspection.
51
51
  Call `record_code_block_schema_get` before `record_code_block_run`.
52
- Call `app_get` first when the data range is unclear, then use `record_browse_schema_get(view_id=...)` before `record_access`, `record_list`, or `record_get`.
52
+ Call `app_get` first when the data range is unclear, then use `record_browse_schema_get(view_id=...)` before `record_access`, `record_list`, `record_get`, or `record_logs_get`.
53
53
  Call `record_import_schema_get` when the import field mapping is unclear before template download or verify.
54
54
 
55
55
  - All `field_id` values must come from the schema response.
@@ -59,7 +59,7 @@ Call `record_import_schema_get` when the import field mapping is unclear before
59
59
 
60
60
  `record_insert_schema_get` returns the current user's insert-ready applicant schema; read `required_fields`, `optional_fields`, `runtime_linked_required_fields`, and `payload_template`.
61
61
  Inside `optional_fields`, any field with `may_become_required=true` is still writable, but may become required when linked visibility or option-driven runtime rules activate.
62
- `record_update_schema_get` returns the current record's overall update-ready writable field set across matched accessible views; read `writable_fields` and `payload_template`.
62
+ `record_update_schema_get` returns the current record's overall update-ready writable field set and route diagnostics across matched accessible views; read `writable_fields`, `payload_template`, `available_update_routes`, and `recommended_update_route`.
63
63
  `record_browse_schema_get(view_id=...)` returns the same readable fields shown in the selected Qingflow table view header.
64
64
  `record_access.fields` / CSV columns and `record_list.columns / where / order_by / query_fields` use that exact same view schema; a missing field means it is not readable in that view.
65
65
  `searchQueIds` is a backend full-text search scope, not an output-column/projection mechanism.
@@ -102,9 +102,9 @@ Analysis answers must include concrete numbers. When applicable, include percent
102
102
 
103
103
  ## Record CRUD Path
104
104
 
105
- `app_get -> record_browse_schema_get(view_id=...) -> record_list / record_get`
106
- `record_insert_schema_get -> record_insert`
107
- `record_update_schema_get -> record_update`
105
+ `app_get -> record_browse_schema_get(view_id=...) -> record_list / record_get / record_logs_get`
106
+ `record_insert_schema_get -> record_insert(items)`
107
+ `record_update` for simple updates; `record_update_schema_get -> record_update` when the writable field scope is unclear.
108
108
  `record_list / record_get -> record_delete`
109
109
  `record_code_block_schema_get -> record_code_block_run`
110
110
  `portal_list -> portal_get -> chart_get / view_get`
@@ -116,22 +116,25 @@ Analysis answers must include concrete numbers. When applicable, include percent
116
116
  - Use `order_by` items as `{{field_id, direction}}`
117
117
  - Legacy forms such as bare integer `field_id`, `fieldId`, `operator`, `values`, or `order` may still parse, but they are compatibility-only and not the canonical DSL
118
118
 
119
- - `record_insert` uses an applicant-node `fields` map keyed by field title.
120
- - `record_update` uses a field-title keyed `fields` map and internally selects the first accessible view that can execute the current payload.
119
+ - `record_insert` defaults to an applicant-node `items` array; each item contains a field-title keyed `fields` map. A single insert is one item.
120
+ - `record_update` uses a field-title keyed `fields` map. It first tries the data-manager direct update route, then falls back to the frontend custom-view detail edit route when the selected view can cover the payload; if a unique current-user todo task for the same record exposes editable fields, it can finally use the workflow save-only route. Read `update_route` and `tried_routes` after execution.
121
121
  - For insert, `runtime_linked_required_fields` means required-but-not-directly-writable fields that are usually supplied by runtime linkage or upstream context.
122
122
  - For insert, fields marked `may_become_required=true` stay in `optional_fields`; they are still directly writable, but linked visibility or option-driven rules can make them required at runtime.
123
123
  - Read field-level `linkage` whenever present on `record_insert_schema_get` or `record_update_schema_get`; it is the static hint for linked visibility, reference-driven auto fill, and formula/default auto-fill behavior.
124
124
  - `linkage.sources` lists upstream field titles that influence the current field; `linkage.affects_fields` lists downstream fields that may change when the current field changes.
125
125
  - `linkage.kind=logic_visibility` means linked visibility or option-driven rules are involved; `linkage.kind=reference_fill` means reference/default matching logic is involved; `linkage.kind=formula_fill` means formula/default auto-fill logic is involved.
126
- - `record_update_schema_get` exposes the overall writable field set for the record, but not every field combination is guaranteed; `record_update` still needs one single matched accessible view that can cover the payload.
126
+ - `record_update_schema_get` exposes the overall writable field set and update route candidates for the record, but not every field combination is guaranteed; `record_update` still needs data-manager permission, one single matched custom view that can cover the payload, or one unique editable current-user todo task.
127
127
  - `record_delete` deletes by `record_id` or `record_ids`.
128
128
  - `record_get` is the single-record frontend detail context tool. It returns detail-page visible fields, one-level relation targets, first-page data/workflow logs, associated views/reports, local readable image assets, local downloadable file assets, unavailable context, and `semantic_context`.
129
+ - Use `record_logs_get` only when the user needs the full visible data/workflow log history for a specific record. It writes JSONL files locally and returns file paths plus completeness metadata; do not expect full log arrays in the response.
129
130
  - Read record images from `record_get.media_assets.items[].local_path` when `readable_by_agent=true`; read attachments/documents/tables from `record_get.file_assets.items[].local_path` and `extraction.text_path` when present. `record_get` follows the frontend storage cookie redirect path for Qingflow attachments, and remote file URLs should not be treated as directly readable.
130
131
  - `record_get.columns` are focus hints only; they do not project the detail fields. Read facts from top-level `fields[]`.
131
132
  - When readback shape matters after insert or update, prefer `record_get` for human/detail-page context, or `record_list(..., output_profile="normalized")` for batch-shaped normalized rows.
132
133
 
133
134
  - Read relation targets from `record_insert_schema_get` / `record_update_schema_get` relation metadata before preparing relation writes.
134
- - Member and department fields may be written with natural strings directly on `record_insert` / `record_update`; only fall back to `record_member_candidates` or `record_department_candidates` when the user wants explicit candidate browsing or the write returns ambiguity that needs confirmation.
135
+ - Member, department, and relation fields may be written with natural strings directly on `record_insert` / `record_update`; only fall back to candidate tools when the user wants explicit candidate browsing or the write returns ambiguity that needs confirmation.
136
+ - CLI-only agents can use `qingflow record member-candidates --app-key APP_KEY --field-id FIELD_ID --keyword NAME --json` or `qingflow record department-candidates --app-key APP_KEY --field-id FIELD_ID --keyword DEPT --json` for that fallback.
137
+ - For batch insert `partial_success`, read `created_record_ids`, failed `items[].row_number`, and `failed_fields`; repair only failed rows and never retry the whole batch after any row has `write_executed=true`.
135
138
  - When candidate browsing must match a real update/write scope, pass `record_id`, `workflow_node_id`, and any pending `fields` context to the candidate tool; otherwise the candidate result is only a static applicant-node preview.
136
139
  - If explicit candidate browsing is needed for default-all member or department fields, prefer those field candidate tools instead of starting with `directory_*`.
137
140
 
@@ -188,7 +191,7 @@ Use export only when the user explicitly asks to export/download/generate an Exc
188
191
  - `task_action_execute(task_id=..., action=...)` is also supported; MCP resolves the current todo locator internally before calling the real action route.
189
192
  - `task_workflow_log_get(task_id=...)` and `task_associated_report_detail_get(task_id=...)` are also supported for the current todo context.
190
193
  - Use `task_associated_report_detail_get` for associated view or report details.
191
- - Use `task_workflow_log_get` for full workflow log history.
194
+ - Use `task_workflow_log_get` for the current task context workflow log page. For full record-level data/workflow logs, use `record_logs_get(app_key, record_id, view_id?)`.
192
195
  - Task actions operate on `app_key + record_id + workflow_node_id`, not `task_id`.
193
196
  - Treat `task_action_execute` as the tool-level action enum surface; the current task's real actions are only the ones listed in `task_get.capabilities.available_actions`.
194
197
  - Use `task_action_execute(action="save_only", fields=...)` when the user wants to save editable field changes on the current node without advancing the workflow.
@@ -358,12 +361,8 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
358
361
  )
359
362
 
360
363
  @server.tool()
361
- def app_list(profile: str = DEFAULT_PROFILE) -> dict:
362
- return apps.app_list(profile=profile)
363
-
364
- @server.tool()
365
- def app_search(profile: str = DEFAULT_PROFILE, keyword: str = "", page_num: int = 1, page_size: int = 50) -> dict:
366
- return apps.app_search(profile=profile, keyword=keyword, page_num=page_num, page_size=page_size)
364
+ def app_list(profile: str = DEFAULT_PROFILE, query: str = "", keyword: str = "") -> dict:
365
+ return apps.app_list(profile=profile, query=query, keyword=keyword)
367
366
 
368
367
  @server.tool()
369
368
  def app_get(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
@@ -10,8 +10,6 @@ from .navigation_compiler import compile_navigation
10
10
  from .package_compiler import compile_package
11
11
  from .portal_compiler import compile_portal
12
12
  from .view_compiler import compile_views
13
- from .workflow_compiler import compile_workflow
14
-
15
13
 
16
14
  @dataclass(slots=True)
17
15
  class ExecutionStep:
@@ -101,7 +99,7 @@ def compile_solution(spec: SolutionSpec) -> CompiledSolution:
101
99
 
102
100
  def compile_entity(entity: EntitySpec, *, include_package: bool) -> CompiledEntity:
103
101
  app_create_payload, form_base_payload, form_relation_payload, field_specs, field_labels = compile_entity_form(entity, include_package=include_package)
104
- workflow_plan = compile_workflow(entity)
102
+ workflow_plan = None
105
103
  view_plans = compile_views(entity)
106
104
  chart_plans = compile_charts(entity)
107
105
  return CompiledEntity(
@@ -5,6 +5,215 @@ import re
5
5
 
6
6
 
7
7
  DEFAULT_ICON_COLOR = "qing-orange"
8
+ WORKSPACE_ICON_COLORS: tuple[str, ...] = (
9
+ "qing-orange",
10
+ "yellow",
11
+ "green",
12
+ "emerald",
13
+ "blue",
14
+ "azure",
15
+ "indigo",
16
+ "qing-purple",
17
+ "purple",
18
+ "pink",
19
+ "red",
20
+ "orange",
21
+ )
22
+ WORKSPACE_ICON_NAMES: tuple[str, ...] = (
23
+ "user",
24
+ "user-group",
25
+ "user-remove",
26
+ "user-add",
27
+ "user-circle",
28
+ "base-camera",
29
+ "view-grid",
30
+ "inbox",
31
+ "inbox-in",
32
+ "share",
33
+ "sitemap",
34
+ "airplane",
35
+ "template",
36
+ "music-note",
37
+ "movie-play",
38
+ "clock",
39
+ "document",
40
+ "document-search",
41
+ "clipboard-check",
42
+ "document-download",
43
+ "document-text",
44
+ "clipboard-copy",
45
+ "presentation-chart-bar",
46
+ "chart-square-bar",
47
+ "database",
48
+ "server",
49
+ "calendar",
50
+ "mail",
51
+ "annotation",
52
+ "chat",
53
+ "bell",
54
+ "key",
55
+ "shopping-bag",
56
+ "download",
57
+ "eye",
58
+ "eye-off",
59
+ "emoji-happy",
60
+ "emoji-sad",
61
+ "sun",
62
+ "moon",
63
+ "cloud",
64
+ "lightning-bolt",
65
+ "fire",
66
+ "star",
67
+ "sparkles",
68
+ "heart",
69
+ "cake",
70
+ "gift",
71
+ "light-bulb",
72
+ "exclamation",
73
+ "cog",
74
+ "thumb-up",
75
+ "thumb-down",
76
+ "cloud-download",
77
+ "cloud-upload",
78
+ "printer",
79
+ "phone-incoming",
80
+ "phone-missed-call",
81
+ "terminal",
82
+ "search-circle",
83
+ "x-circle",
84
+ "check-circle",
85
+ "exclamation-circle",
86
+ "question-mark-circle",
87
+ "information-circle",
88
+ "academic-cap",
89
+ "briefcase",
90
+ "home",
91
+ "phone",
92
+ "photograph",
93
+ "puzzle",
94
+ "color-swatch",
95
+ "lock-open",
96
+ "lock-closed",
97
+ "shield-check",
98
+ "shield-exclamation",
99
+ "currency-dollar",
100
+ "currency-yen",
101
+ "globe",
102
+ "at-symbol",
103
+ "slack",
104
+ "microphone",
105
+ "speakerphone",
106
+ "trash",
107
+ "book-open",
108
+ "truck",
109
+ "filter",
110
+ "essetional-filter-search",
111
+ "essetional-filter-tick",
112
+ "table",
113
+ "calculator",
114
+ "location-radar",
115
+ "essetional-weight",
116
+ "school-award",
117
+ "comp-cloud-connection",
118
+ "comp-cloud-remove",
119
+ "comp-cpu-charge",
120
+ "comp-cpu-setting",
121
+ "comp-cpu",
122
+ "comp-devices",
123
+ "comp-driver-2",
124
+ "comp-driver-refresh",
125
+ "location-global",
126
+ "location-location",
127
+ "location-map",
128
+ "location-gps",
129
+ "essetional-ranking",
130
+ "chart-bar",
131
+ "business-graph",
132
+ "business-status-up",
133
+ "business-trend-down",
134
+ "business-trend-up",
135
+ "business-presention-chart",
136
+ "business-favorite-chart",
137
+ "business-health",
138
+ "receipt-refund",
139
+ "receipt-tax",
140
+ "money-receipt-2-1",
141
+ "money-transaction-minus",
142
+ "action-hourglass-full",
143
+ "action-work",
144
+ "bug-f",
145
+ "essetional-pet",
146
+ "files-folder",
147
+ "badge-check",
148
+ "money-wallet-1",
149
+ "money-ticket",
150
+ "money-money",
151
+ "money-tag",
152
+ "money-wallet-2",
153
+ "business-personalcard",
154
+ "car-airplane",
155
+ "car-bus",
156
+ "car-car",
157
+ "car-driving",
158
+ "car-gas-station",
159
+ "car-smart-car",
160
+ "car-ship",
161
+ "location-map-1",
162
+ "location-route-square",
163
+ "cone",
164
+ "design-brush-4",
165
+ "paint-roll",
166
+ "wrench-f",
167
+ "essetional-reserve",
168
+ "essetional-broom",
169
+ "design-brush-2",
170
+ "essetional-judge",
171
+ "design-bucket",
172
+ "palette",
173
+ "comp-electricity",
174
+ "vial",
175
+ "beaker",
176
+ "leaf-f",
177
+ "cursor-click",
178
+ "solid-search-alt-2",
179
+ "md-library",
180
+ "building-3",
181
+ "office-building",
182
+ "building-hospital",
183
+ "school",
184
+ "store",
185
+ "video-camera-vintage-f",
186
+ "comp-monitor",
187
+ "delivery-truck",
188
+ "delivery-box-1",
189
+ "delivery-box-add",
190
+ "delivery-box-remove",
191
+ "settings-setting-3",
192
+ "document-duplicate",
193
+ "essetional-flag-2",
194
+ "flag",
195
+ "icon-currency-dollar",
196
+ "clipboard-list",
197
+ "save-as",
198
+ "wifi",
199
+ "status-online",
200
+ "scissors",
201
+ "globe-alt",
202
+ "ban",
203
+ "finger-print",
204
+ "qrcode",
205
+ "paper-clip",
206
+ "translate",
207
+ "cube-transparent",
208
+ "variable",
209
+ "switch-vertical",
210
+ "sports-baseball",
211
+ "sports-basketball",
212
+ "sports-soccer",
213
+ "sports-football",
214
+ "sports-volleyball",
215
+ )
216
+ GENERIC_WORKSPACE_ICON_NAMES: tuple[str, ...] = ("template",)
8
217
  DEFAULT_ICON_STYLE_POOL: tuple[tuple[str, str], ...] = (
9
218
  ("briefcase", "qing-orange"),
10
219
  ("calendar", "emerald"),
@@ -115,6 +324,91 @@ def parse_workspace_icon(value: str | None) -> tuple[str | None, str | None, str
115
324
  return normalize_workspace_icon_name(stripped), None, None
116
325
 
117
326
 
327
+ def workspace_icon_config(value: str | None) -> dict[str, str | None]:
328
+ icon_name, icon_color, icon_text = parse_workspace_icon(value)
329
+ raw = str(value).strip() if value not in (None, "") else None
330
+ return {
331
+ "icon_name": icon_name,
332
+ "icon_color": icon_color,
333
+ "icon_text": icon_text,
334
+ "raw": raw,
335
+ }
336
+
337
+
338
+ def workspace_icon_catalog_payload() -> dict[str, object]:
339
+ return {
340
+ "icon_names": list(WORKSPACE_ICON_NAMES),
341
+ "icon_colors": list(WORKSPACE_ICON_COLORS),
342
+ "generic_icon_names": list(GENERIC_WORKSPACE_ICON_NAMES),
343
+ "notes": [
344
+ "Use explicit icon + color for app/package/portal creation.",
345
+ "Do not use template for newly created workspace resources.",
346
+ "The CLI validates candidates only; it does not infer an icon from business names.",
347
+ ],
348
+ "common_examples": {
349
+ "employee": ["business-personalcard", "user-group", "user"],
350
+ "task": ["clipboard-check", "action-work"],
351
+ "worklog": ["clock", "action-hourglass-full"],
352
+ "order": ["delivery-box-1", "shopping-bag"],
353
+ "payment": ["money-receipt-2-1", "money-wallet-1"],
354
+ "opportunity": ["business-graph", "business-trend-up"],
355
+ "dashboard": ["view-grid", "chart-square-bar", "presentation-chart-bar"],
356
+ },
357
+ }
358
+
359
+
360
+ def validate_workspace_icon_choice(
361
+ *,
362
+ icon: str | None,
363
+ color: str | None,
364
+ require_explicit: bool,
365
+ disallow_generic: bool,
366
+ ) -> tuple[bool, str | None, str | None, dict[str, object]]:
367
+ normalized_icon = _normalize_workspace_icon_candidate(icon)
368
+ normalized_color = str(color or "").strip() or None
369
+ details: dict[str, object] = {
370
+ "icon": icon,
371
+ "normalized_icon": normalized_icon,
372
+ "color": color,
373
+ "icon_catalog_command": "qingflow --json builder icon catalog",
374
+ }
375
+ if require_explicit and not normalized_icon:
376
+ return False, "WORKSPACE_ICON_REQUIRED", "icon is required when creating a workspace resource", details
377
+ if require_explicit and not normalized_color:
378
+ return False, "WORKSPACE_ICON_COLOR_REQUIRED", "color is required when creating a workspace resource", details
379
+ if normalized_icon and normalized_icon not in WORKSPACE_ICON_NAMES:
380
+ details["allowed_icon_names"] = list(WORKSPACE_ICON_NAMES)
381
+ return False, "WORKSPACE_ICON_NOT_FOUND", "icon is not in the workspace icon catalog", details
382
+ if normalized_color and normalized_color not in WORKSPACE_ICON_COLORS:
383
+ details["allowed_icon_colors"] = list(WORKSPACE_ICON_COLORS)
384
+ return False, "WORKSPACE_ICON_COLOR_NOT_FOUND", "color is not in the workspace icon color catalog", details
385
+ if disallow_generic and normalized_icon in GENERIC_WORKSPACE_ICON_NAMES:
386
+ details["generic_icon_names"] = list(GENERIC_WORKSPACE_ICON_NAMES)
387
+ return False, "GENERIC_WORKSPACE_ICON_NOT_ALLOWED", "template is a generic icon and is not allowed for new workspace resources", details
388
+ return True, None, None, details
389
+
390
+
391
+ def _normalize_workspace_icon_candidate(icon: str | None) -> str | None:
392
+ if not icon:
393
+ return None
394
+ raw = str(icon).strip()
395
+ if not raw:
396
+ return None
397
+ if _looks_like_icon_json(raw):
398
+ try:
399
+ payload = json.loads(raw)
400
+ except Exception:
401
+ return None
402
+ return _normalize_workspace_icon_candidate(payload.get("iconName"))
403
+ normalized = raw.lower()
404
+ if normalized in WORKSPACE_ICON_NAMES:
405
+ return normalized
406
+ legacy = LEGACY_EX_ICON_MAP.get(normalized)
407
+ if legacy in WORKSPACE_ICON_NAMES:
408
+ return legacy
409
+ return normalized
410
+
411
+
118
412
  def encode_workspace_icon_with_defaults(
119
413
  *,
120
414
  icon: str | None,
@@ -14,7 +14,6 @@ from ..tools.qingbi_report_tools import QingbiReportTools
14
14
  from ..tools.record_tools import RecordTools
15
15
  from ..tools.role_tools import RoleTools
16
16
  from ..tools.view_tools import ViewTools
17
- from ..tools.workflow_tools import WorkflowTools
18
17
  from ..tools.workspace_tools import WorkspaceTools
19
18
  from .compiler import CompiledEntity, CompiledRole, CompiledSolution
20
19
  from .compiler.form_compiler import QUESTION_TYPE_MAP
@@ -36,7 +35,6 @@ class SolutionExecutor:
36
35
  role_tools: RoleTools,
37
36
  app_tools: AppTools,
38
37
  record_tools: RecordTools,
39
- workflow_tools: WorkflowTools,
40
38
  view_tools: ViewTools,
41
39
  chart_tools: QingbiReportTools,
42
40
  portal_tools: PortalTools,
@@ -47,7 +45,6 @@ class SolutionExecutor:
47
45
  self.role_tools = role_tools
48
46
  self.app_tools = app_tools
49
47
  self.record_tools = record_tools
50
- self.workflow_tools = workflow_tools
51
48
  self.view_tools = view_tools
52
49
  self.chart_tools = chart_tools
53
50
  self.portal_tools = portal_tools
@@ -419,123 +416,10 @@ class SolutionExecutor:
419
416
  def _build_workflow(self, profile: str, entity: CompiledEntity, store: RunArtifactStore) -> None:
420
417
  if entity.workflow_plan is None:
421
418
  return
422
- app_key = self._get_app_key(store, entity.entity_id)
423
- workflow_edit_version_no = self._ensure_edit_version(
424
- profile,
425
- entity.entity_id,
426
- store,
427
- app_key=app_key,
428
- force_new=True,
429
- )
430
- node_artifacts = store.get_artifact("apps", entity.entity_id, {}).get("workflow_nodes", {})
431
- existing_nodes = self.workflow_tools.workflow_list_nodes(profile=profile, app_key=app_key).get("result") or {}
432
- current_nodes = _coerce_workflow_nodes(existing_nodes)
433
- existing_nodes_by_name = {
434
- node.get("auditNodeName"): int(node_id)
435
- for node_id, node in current_nodes.items()
436
- if isinstance(node, dict) and node.get("auditNodeName")
437
- }
438
- applicant_node_id = next(
439
- (
440
- int(node_id)
441
- for node_id, node in current_nodes.items()
442
- if isinstance(node, dict) and node.get("type") == 0 and node.get("dealType") == 3
443
- ),
444
- None,
419
+ raise RuntimeError(
420
+ "Legacy auditNode workflow execution was removed. "
421
+ "Pass {app_key, spec} to solution_build_flow or use qingflow builder flow apply."
445
422
  )
446
- if applicant_node_id is not None:
447
- node_artifacts.setdefault("__applicant__", applicant_node_id)
448
-
449
- desired_global_settings = deepcopy(entity.workflow_plan["global_settings"])
450
- explicit_global_settings = _has_explicit_workflow_global_settings(desired_global_settings)
451
- current_global_settings: dict[str, Any] = {}
452
- if explicit_global_settings:
453
- current_global_settings = self.workflow_tools.workflow_get_global_settings(profile=profile, app_key=app_key).get("result") or {}
454
- else:
455
- try:
456
- current_global_settings = self.workflow_tools.workflow_get_global_settings(profile=profile, app_key=app_key).get("result") or {}
457
- except (QingflowApiError, RuntimeError) as error:
458
- api_error = QingflowApiError(**_coerce_nested_error_payload(error))
459
- if api_error.http_status != 404:
460
- raise
461
- current_global_settings = {}
462
- if explicit_global_settings:
463
- global_settings = deepcopy(current_global_settings if isinstance(current_global_settings, dict) else {})
464
- global_settings.update(desired_global_settings)
465
- global_settings["editVersionNo"] = workflow_edit_version_no or global_settings.get("editVersionNo") or 1
466
- self.workflow_tools.workflow_update_global_settings(profile=profile, app_key=app_key, payload=global_settings)
467
- for action in entity.workflow_plan["actions"]:
468
- if action["action"] == "create_sub_branch" and node_artifacts.get(action["node_id"]) is not None:
469
- continue
470
- if action["action"] == "add_node":
471
- if action.get("node_type") == "branch":
472
- existing_branch_id = node_artifacts.get(action["node_id"])
473
- if existing_branch_id is not None and not _workflow_node_is_branch(current_nodes, existing_branch_id):
474
- existing_branch_id = None
475
- if existing_branch_id is not None:
476
- for branch_index, lane_id in enumerate(_find_branch_lane_ids(current_nodes, existing_branch_id), start=1):
477
- node_artifacts[_branch_lane_ref(action["node_id"], branch_index)] = lane_id
478
- apps_artifact = store.get_artifact("apps", entity.entity_id, {})
479
- apps_artifact["workflow_nodes"] = node_artifacts
480
- store.set_artifact("apps", entity.entity_id, apps_artifact)
481
- continue
482
- existing_node_id = node_artifacts.get(action["node_id"]) or existing_nodes_by_name.get(action.get("node_name"))
483
- if existing_node_id is not None:
484
- node_artifacts[action["node_id"]] = existing_node_id
485
- apps_artifact = store.get_artifact("apps", entity.entity_id, {})
486
- apps_artifact["workflow_nodes"] = node_artifacts
487
- store.set_artifact("apps", entity.entity_id, apps_artifact)
488
- continue
489
- before_node_ids = set(current_nodes)
490
- payload = self._resolve_workflow_payload(action["payload"], node_artifacts)
491
- if workflow_edit_version_no is not None:
492
- payload["editVersionNo"] = int(workflow_edit_version_no)
493
- if action["action"] == "create_sub_branch":
494
- result = self.workflow_tools.workflow_create_sub_branch(profile=profile, app_key=app_key, payload=payload)
495
- elif action["action"] == "update_node":
496
- target_node_id = node_artifacts.get(action["node_id"])
497
- if target_node_id is None:
498
- raise RuntimeError(f"workflow lane '{action['node_id']}' could not be resolved before update")
499
- result = self.workflow_tools.workflow_update_node(
500
- profile=profile,
501
- app_key=app_key,
502
- audit_node_id=target_node_id,
503
- payload=payload,
504
- )
505
- else:
506
- result = self.workflow_tools.workflow_add_node(profile=profile, app_key=app_key, payload=payload)
507
- expected_type = 1 if action.get("node_type") == "branch" else None
508
- audit_node_id = _extract_workflow_node_id(result.get("result"), expected_type=expected_type)
509
- if action.get("node_type") == "branch" or action["action"] == "create_sub_branch":
510
- current_nodes = _coerce_workflow_nodes(
511
- self.workflow_tools.workflow_list_nodes(profile=profile, app_key=app_key).get("result") or {}
512
- )
513
- if audit_node_id is not None:
514
- node_artifacts[action["node_id"]] = audit_node_id
515
- if action.get("node_type") == "branch":
516
- branch_node_id = node_artifacts.get(action["node_id"]) or _find_created_branch_node_id(
517
- current_nodes,
518
- before_node_ids=before_node_ids,
519
- prev_id=payload.get("prevId"),
520
- )
521
- if branch_node_id is not None:
522
- node_artifacts[action["node_id"]] = branch_node_id
523
- for branch_index, lane_id in enumerate(_find_branch_lane_ids(current_nodes, branch_node_id), start=1):
524
- node_artifacts[_branch_lane_ref(action["node_id"], branch_index)] = lane_id
525
- if action["action"] == "create_sub_branch" and node_artifacts.get(action["node_id"]) is None:
526
- created_lane_id = audit_node_id or _find_created_sub_branch_lane_id(
527
- current_nodes,
528
- before_node_ids=before_node_ids,
529
- branch_node_id=payload.get("auditNodeId"),
530
- )
531
- if created_lane_id is not None:
532
- node_artifacts[action["node_id"]] = created_lane_id
533
- apps_artifact = store.get_artifact("apps", entity.entity_id, {})
534
- apps_artifact["workflow_nodes"] = node_artifacts
535
- store.set_artifact("apps", entity.entity_id, apps_artifact)
536
- apps_artifact = store.get_artifact("apps", entity.entity_id, {})
537
- apps_artifact["workflow_nodes"] = node_artifacts
538
- store.set_artifact("apps", entity.entity_id, apps_artifact)
539
423
 
540
424
  def _build_views(self, profile: str, entity: CompiledEntity, store: RunArtifactStore) -> None:
541
425
  app_key = self._get_app_key(store, entity.entity_id)
@@ -2097,20 +1981,6 @@ def _find_created_sub_branch_lane_id(
2097
1981
  return candidates[0] if candidates else None
2098
1982
 
2099
1983
 
2100
- def _has_explicit_workflow_global_settings(global_settings: dict[str, Any] | None) -> bool:
2101
- if not isinstance(global_settings, dict):
2102
- return False
2103
- for key, value in global_settings.items():
2104
- if key == "editVersionNo":
2105
- continue
2106
- if value is None:
2107
- continue
2108
- if isinstance(value, (list, dict)) and not value:
2109
- continue
2110
- return True
2111
- return False
2112
-
2113
-
2114
1984
  def _is_navigation_plugin_unavailable(error: QingflowApiError) -> bool:
2115
1985
  try:
2116
1986
  backend_code = int(error.backend_code)