@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.
- package/README.md +3 -3
- package/npm/bin/qingflow.mjs +40 -2
- package/npm/lib/runtime.mjs +386 -15
- package/npm/scripts/postinstall.mjs +7 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-cli/SKILL.md +440 -0
- package/skills/qingflow-cli/manifest.yaml +10 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +94 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md +485 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_CHARTS_WORKFLOW.md +237 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_MATCH_RULES.md +137 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_PORTAL_WORKFLOW.md +263 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md +304 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_WORKSPACE_ICONS.md +41 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +139 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +84 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_FIELD_DATA_TYPES.md +129 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +195 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +159 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md +20 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md +176 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +163 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md +107 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md +151 -0
- package/skills/qingflow-cli/reference/_batch_schema_complex.json +18 -0
- package/skills/qingflow-cli/reference/_batch_schema_scalar.json +17 -0
- package/skills/qingflow-cli/reference/charts_remove.example.json +1 -0
- package/skills/qingflow-cli/reference/charts_reorder.example.json +1 -0
- package/skills/qingflow-cli/reference/charts_upsert_bar.example.json +8 -0
- package/skills/qingflow-cli/reference/charts_upsert_dashboard_starter.example.json +37 -0
- package/skills/qingflow-cli/reference/charts_upsert_minimal.example.json +13 -0
- package/skills/qingflow-cli/reference/portal_sections_all_types.example.json +131 -0
- package/skills/qingflow-cli/reference/portal_sections_five_types.example.json +126 -0
- package/skills/qingflow-cli/reference/portal_sections_standard_workbench.example.json +128 -0
- package/skills/qingflow-cli/reference/schema_add_fields_minimal.example.json +7 -0
- package/skills/qingflow-cli/reference/schema_apply_add_fields_all_types.json +78 -0
- package/skills/qingflow-cli/reference/views_upsert_table_minimal.example.json +7 -0
- package/skills/qingflow-cli/scripts/builder-package-from-app-list.py +140 -0
- package/skills/qingflow-cli/scripts/find-app-by-keyword.py +132 -0
- package/skills/qingflow-cli/scripts/validate_qingflow_output_files.py +87 -0
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/builder_facade/models.py +532 -48
- package/src/qingflow_mcp/builder_facade/service.py +9194 -2384
- package/src/qingflow_mcp/builder_facade/workflow_spec.py +111 -0
- package/src/qingflow_mcp/cli/commands/app.py +3 -16
- package/src/qingflow_mcp/cli/commands/builder.py +354 -56
- package/src/qingflow_mcp/cli/commands/record.py +89 -2
- package/src/qingflow_mcp/cli/formatters.py +32 -1
- package/src/qingflow_mcp/cli/main.py +245 -3
- package/src/qingflow_mcp/public_surface.py +11 -8
- package/src/qingflow_mcp/response_trim.py +143 -14
- package/src/qingflow_mcp/server.py +15 -12
- package/src/qingflow_mcp/server_app_builder.py +108 -30
- package/src/qingflow_mcp/server_app_user.py +17 -18
- package/src/qingflow_mcp/solution/compiler/__init__.py +1 -3
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +294 -0
- package/src/qingflow_mcp/solution/executor.py +3 -133
- package/src/qingflow_mcp/tools/ai_builder_tools.py +2617 -440
- package/src/qingflow_mcp/tools/app_tools.py +53 -8
- package/src/qingflow_mcp/tools/package_tools.py +16 -2
- package/src/qingflow_mcp/tools/record_tools.py +2095 -176
- package/src/qingflow_mcp/tools/resource_read_tools.py +3 -0
- package/src/qingflow_mcp/tools/solution_tools.py +30 -2
- package/src/qingflow_mcp/tools/workflow_tools.py +3 -31
- package/src/qingflow_mcp/version.py +110 -0
- 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`
|
|
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
|
-
|
|
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 `
|
|
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 `
|
|
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`
|
|
120
|
-
- `record_update` uses a field-title keyed `fields` map
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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)
|