@qingflow-tech/qingflow-app-builder-mcp 1.0.42 → 1.0.44
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 +2 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +5 -1
- package/skills/qingflow-app-builder/references/complete-system-development-guide.md +65 -1
- package/skills/qingflow-app-builder/references/gotchas.md +3 -0
- package/skills/qingflow-app-builder/references/single-app-development-guide.md +11 -0
- package/skills/qingflow-app-builder/references/tool-selection.md +1 -1
- package/skills/qingflow-app-builder/references/update-flow.md +21 -9
- package/skills/qingflow-app-builder/references/update-views.md +27 -2
- package/skills/qingflow-app-builder/scripts/validate_system_build_summary.py +124 -0
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +1 -1
- package/src/qingflow_mcp/builder_facade/service.py +277 -4
- package/src/qingflow_mcp/cli/main.py +4 -6
- package/src/qingflow_mcp/tools/ai_builder_tools.py +89 -12
- package/src/qingflow_mcp/version.py +71 -1
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @qingflow-tech/qingflow-app-builder-mcp@1.0.
|
|
6
|
+
npm install @qingflow-tech/qingflow-app-builder-mcp@1.0.44
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @qingflow-tech/qingflow-app-builder-mcp@1.0.
|
|
12
|
+
npx -y -p @qingflow-tech/qingflow-app-builder-mcp@1.0.44 qingflow-app-builder-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -7,7 +7,7 @@ metadata:
|
|
|
7
7
|
|
|
8
8
|
# Qingflow App Builder
|
|
9
9
|
|
|
10
|
-
> **Skill 版本**:`qingflow-skills-2026.06.
|
|
10
|
+
> **Skill 版本**:`qingflow-skills-2026.06.24.2`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
|
|
11
11
|
|
|
12
12
|
## Overview
|
|
13
13
|
|
|
@@ -93,6 +93,7 @@ Treat these as the official surface. Do not default to `package_create`, `packag
|
|
|
93
93
|
- it does not attach the report to the Qingflow app associated-resource display; use `app_associated_resources_apply` for that
|
|
94
94
|
- supported `chart_type` values include legacy public aliases `target`, `table` and QingBI types such as `summary`, `columnar`, `area`, `stacked_area`, `funnel`, `waterfall`, `gauge`, `heatmap`, `histogram`, `treemap`, `radar`, `stacked_bar`, `stacked_column`, `scatter`, `ring`, `rose`, `dualaxes`, `map`, and `timeline`
|
|
95
95
|
- chart `filters` use the unified fixed-filter DSL: `field_name + operator + value/values`; the tool compiles them to QingBI string judge types
|
|
96
|
+
- for complete systems, submit `upsert_charts` in batches of 4-8. More than 8 upserts is blocked before write with `CHART_UPSERT_BATCH_TOO_LARGE`; execute `details.suggested_batch_payloads[]` one batch at a time.
|
|
96
97
|
- use `patch_charts` for changing a chart name, visibility, filters, or one config fragment on an existing chart
|
|
97
98
|
- `visibility` is a public capability and should be treated as a base-only permission update
|
|
98
99
|
- do not model chart visibility changes as raw config rewrites
|
|
@@ -121,11 +122,13 @@ Treat these as the official surface. Do not default to `package_create`, `packag
|
|
|
121
122
|
- if field type compatibility is unclear, read [references/match-rules.md](references/match-rules.md)
|
|
122
123
|
- Views and flows:
|
|
123
124
|
- stay on `app_views_apply` / `app_flow_apply`
|
|
125
|
+
- before building approval/fill/copy flows, make sure the app schema has an explicit business status `select` field such as `状态` / `处理状态` / `审批状态`; do not create platform workflow system fields such as `当前流程状态`
|
|
124
126
|
- use `patch_views` for existing-view parameter changes such as `query_conditions`, `filters`, `columns`, or visibility
|
|
125
127
|
- builder view writes use raw `view_key` values from `app_get.views`; `custom:<viewKey>` is a record-data `view_id` form, not a builder `view_key`
|
|
126
128
|
- do not create platform default/system views named `全部数据`, `我的数据`, `我发起的`, `待办`, `已办`, or `抄送`; they are built in. New views must use business-specific names. To change a built-in view, patch by its existing raw `view_key`
|
|
127
129
|
- use `builder_tool_contract` whenever the minimal legal shape is unclear
|
|
128
130
|
- keep view `filters` and `query_conditions` separate: `filters` use `field_name + operator + value/values` and are fixed saved filters; `query_conditions` configure the frontend query panel fields and only take effect after users enter query values
|
|
131
|
+
- keep relation/attachment/subtable/address/Q-Linker/code-block fields out of `query_conditions`; member/department fields may be used as query-panel fields when readback supports them. Use `filters` for fixed filters or associated-resource `match_mappings` for current-record related report/view matching
|
|
129
132
|
- new views default the associated report/view display area to visible with `limit_type="all"`; existing views preserve their current associated-resource display unless `associated_resources` is explicitly patched
|
|
130
133
|
- configure associated reports/views through `app_associated_resources_apply`, not through `app_views_apply`
|
|
131
134
|
|
|
@@ -161,6 +164,7 @@ Treat these as the official surface. Do not default to `package_create`, `packag
|
|
|
161
164
|
- portal -> `portal_apply`
|
|
162
165
|
- package metadata/layout -> `package_apply`
|
|
163
166
|
8. Use `app_publish_verify` only when the user explicitly wants final publish/live verification or you need a dedicated verification pass
|
|
167
|
+
9. For complete-system builds, write `tmp/qingflow_system_build_summary.json` before the final response and make the final report match that file
|
|
164
168
|
|
|
165
169
|
## Safe Usage Rules
|
|
166
170
|
|
|
@@ -11,6 +11,7 @@ Required:
|
|
|
11
11
|
- core business objects are modeled as separate apps
|
|
12
12
|
- all new apps are created in one multi-app `app_schema_apply(package_id=..., apps=[...])`
|
|
13
13
|
- every app has stable `client_key`, `app_name`, `icon`, `color`, and one readable `as_data_title: true`
|
|
14
|
+
- no app creates platform system fields such as `数据ID`, `编号`, `申请人`, `申请时间`, `创建人`, `创建时间`, `提交人`, `提交时间`, `更新时间`, `更新人`, `当前流程状态`, `当前处理人`, `当前处理节点`, or `流程标题`
|
|
14
15
|
- same-call relations use `target_app_ref` to point to another `apps[].client_key`
|
|
15
16
|
- basic operating views exist for each core app
|
|
16
17
|
- package/apps/fields/relations are read back before repair or retry
|
|
@@ -22,6 +23,12 @@ Strongly recommended:
|
|
|
22
23
|
- a standard portal: business entry area, core metrics, BI charts, then concrete data views
|
|
23
24
|
- portal grid/business-entry sections contain real `config.items[]`, not empty containers
|
|
24
25
|
|
|
26
|
+
Workflow preflight:
|
|
27
|
+
|
|
28
|
+
- If an app will have approval/fill/copy workflow nodes, create an explicit business status `select` field in schema first, such as `状态`, `处理状态`, `审批状态`, `工单状态`, or `流程阶段`.
|
|
29
|
+
- Do not create platform workflow system fields such as `当前流程状态`, `当前处理人`, `当前处理节点`, or `流程标题`.
|
|
30
|
+
- If `app_flow_apply` returns `FLOW_DEPENDENCY_MISSING`, fix schema with the returned `suggested_next_call`, read fields back, then retry the flow. Do not skip the workflow silently.
|
|
31
|
+
|
|
25
32
|
Optional:
|
|
26
33
|
|
|
27
34
|
- workflow, reminders, buttons, associated resources, sample data, roles, and permissions when the user asks for them or the business process clearly depends on them
|
|
@@ -33,9 +40,66 @@ Optional:
|
|
|
33
40
|
3. Run one multi-app `app_schema_apply` with `apps[]`.
|
|
34
41
|
4. If write result is uncertain, run readback before retrying.
|
|
35
42
|
5. For each app, apply layout and views.
|
|
36
|
-
6. Create charts before portals when the portal needs metrics or BI sections.
|
|
43
|
+
6. Create charts before portals when the portal needs metrics or BI sections. Submit chart upserts in batches of 4-8; if `CHART_UPSERT_BATCH_TOO_LARGE` appears, execute `details.suggested_batch_payloads[]` one at a time.
|
|
37
44
|
7. Create portal only after referenced apps, views, and charts are known.
|
|
38
45
|
8. Add workflows/buttons/associated resources when they are part of the requested business process.
|
|
46
|
+
9. Before the final response, write the complete-system delivery summary file described below.
|
|
47
|
+
|
|
48
|
+
## Field Modeling Rules
|
|
49
|
+
|
|
50
|
+
- Use agent-friendly field types in drafts: `text`, `multiline`, `select`, `multi_select`, `number`, `amount`, `date`, `datetime`, `member`, `department`, `attachment`, `relation`. The tool normalizes aliases such as `multiline -> long_text` and `select -> single_select`.
|
|
51
|
+
- Create only business fields. Do not create platform system fields listed in Required; reference them only where a tool explicitly supports system fields, such as button `source_field: "数据ID"`.
|
|
52
|
+
- Use `number` for ratios, completion rates, scores, percentages, durations, and quantities that may need decimals. Use `amount` only for currency/money semantics. This prevents sample data from producing decimal values for a field that the backend treats as integer money.
|
|
53
|
+
- For select fields, define the option set during schema design. Later sample data must use only those option labels or ids.
|
|
54
|
+
|
|
55
|
+
## View Query Rules
|
|
56
|
+
|
|
57
|
+
- Use `filters` for fixed saved filters that apply when the view opens.
|
|
58
|
+
- Use `query_conditions` only for the frontend query panel layout. It is not another filter DSL and it does not express OR.
|
|
59
|
+
- `query_conditions.rows` should contain query-panel fields such as text, long text, number, amount, date/datetime, select, member, department, phone, email, or boolean.
|
|
60
|
+
- Do not put relation, attachment, subtable/subfield, address, Q-Linker, or code-block fields in `query_conditions`.
|
|
61
|
+
- For current-record related views/reports, use `app_associated_resources_apply.match_mappings`, not `query_conditions`.
|
|
62
|
+
|
|
63
|
+
## Delivery Summary File
|
|
64
|
+
|
|
65
|
+
For complete-system builds, create or update one local JSON file before the final response:
|
|
66
|
+
|
|
67
|
+
`tmp/qingflow_system_build_summary.json`
|
|
68
|
+
|
|
69
|
+
Use this file as the structured source of truth for the final report. Include at least:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"package_id": 0,
|
|
74
|
+
"package_name": "",
|
|
75
|
+
"portal_dash_key": "",
|
|
76
|
+
"portal_live_status": "verified | unverified | not_created",
|
|
77
|
+
"front_end_visible": true,
|
|
78
|
+
"apps": [
|
|
79
|
+
{
|
|
80
|
+
"app_key": "",
|
|
81
|
+
"app_name": "",
|
|
82
|
+
"fields_count": 0,
|
|
83
|
+
"views_count": 0,
|
|
84
|
+
"flows_count": 0,
|
|
85
|
+
"charts_count": 0,
|
|
86
|
+
"publish_verify_status": "verified | unverified | failed"
|
|
87
|
+
}
|
|
88
|
+
],
|
|
89
|
+
"warnings": [],
|
|
90
|
+
"partial_items": [],
|
|
91
|
+
"needs_followup": []
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Rules:
|
|
96
|
+
|
|
97
|
+
- Populate IDs and counts from readback, not from intended payload only.
|
|
98
|
+
- If a required resource is not verified, put it in `partial_items` or `needs_followup` instead of reporting the system as fully complete.
|
|
99
|
+
- If the user later adds a data-entry task, keep that separate from the system-build summary unless the user explicitly wants one combined report.
|
|
100
|
+
- The final response should match this file: completed resources, unverified items, frontend visibility, and follow-up needs must not contradict the JSON.
|
|
101
|
+
- When running from the CLI repo or a test harness, validate the file with:
|
|
102
|
+
`python scripts/validate_system_build_summary.py tmp/qingflow_system_build_summary.json`.
|
|
39
103
|
|
|
40
104
|
## Recovery Rules
|
|
41
105
|
|
|
@@ -97,6 +97,9 @@
|
|
|
97
97
|
- Do not bypass duplicate/conflict states by inventing `V2`, `测试`, timestamp, or random-suffix app names in a real business package
|
|
98
98
|
- For backend rejects, keep the retry narrow: retry only the failed tool, not the whole chain
|
|
99
99
|
- For `VALIDATION_ERROR`, do not keep guessing. Reuse `suggested_next_call`, `canonical_arguments`, `allowed_keys`, and `allowed_values` first.
|
|
100
|
+
- For `builder_tool_contract`, read the returned `json_paths` first. Successful contract data is under `$.contract`; `allowed_values` uses flat dotted keys such as `["field.type"]`, not nested objects. Do not read successful field enums from top-level `allowed_values`.
|
|
101
|
+
- For `workspace_icon_catalog_get`, read icon candidates from `$.icon_names` and colors from `$.icon_colors`.
|
|
102
|
+
- For builder apply responses, read returned `json_paths` first. Stable fields are `$.summary` and `$.resources[]`; use `resources[].id/key/name` before legacy top-level fields.
|
|
100
103
|
- For layout `VALIDATION_ERROR`, also inspect `section_allowed_keys`, `section_aliases`, and `minimal_section_example`. If the same layout-shape error repeats twice, stop free-form retries and re-read `builder_tool_contract(app_layout_apply)`.
|
|
101
104
|
- For flow work, do not replay internal keys from old logs or plan outputs. Public builder calls should stay on:
|
|
102
105
|
- `assignees.role_ids` / `assignees.member_uids` / `assignees.member_emails`
|
|
@@ -24,6 +24,11 @@ Optional:
|
|
|
24
24
|
|
|
25
25
|
- workflow, portal entry, sample data, roles, or visibility changes when requested by the user or clearly needed by the app's job
|
|
26
26
|
|
|
27
|
+
Workflow preflight:
|
|
28
|
+
|
|
29
|
+
- If the app will have approval/fill/copy workflow nodes, add one explicit business status `select` field first, such as `状态`, `处理状态`, `审批状态`, `工单状态`, or `流程阶段`.
|
|
30
|
+
- Do not create platform workflow system fields: `当前流程状态`, `当前处理人`, `当前处理节点`, or `流程标题`.
|
|
31
|
+
|
|
27
32
|
## Standard Path
|
|
28
33
|
|
|
29
34
|
1. Read current target: `app_resolve` or `app_get`.
|
|
@@ -40,6 +45,12 @@ Optional:
|
|
|
40
45
|
- Do not create `数据ID`, `编号`, `申请人`, `申请时间`, `创建人`, `创建时间`, `提交人`, `提交时间`, `更新时间`, `更新人`, `当前流程状态`, `当前处理人`, `当前处理节点`, or `流程标题`.
|
|
41
46
|
- Do not create built-in default views such as `全部数据` or `我的数据`; Qingflow provides system views.
|
|
42
47
|
|
|
48
|
+
## View Query Rules
|
|
49
|
+
|
|
50
|
+
- `filters` are fixed saved filters; `query_conditions` only configures frontend query-panel fields.
|
|
51
|
+
- Put only query-panel fields in `query_conditions.rows`: text, long text, number, amount, date/datetime, select, member, department, phone, email, or boolean.
|
|
52
|
+
- Do not put relation, attachment, subtable/subfield, address, Q-Linker, or code-block fields in `query_conditions`; use fixed `filters` or associated-resource `match_mappings` instead.
|
|
53
|
+
|
|
43
54
|
## Stop Conditions
|
|
44
55
|
|
|
45
56
|
- If the task actually needs multiple business objects, stop and switch to the complete-system guide.
|
|
@@ -55,7 +55,7 @@ These execute normalized patches. Some app apply tools publish by default and st
|
|
|
55
55
|
- `app_views_apply`: use `patch_views` for existing-view parameter replacement; use `upsert_views` for creation or full target config; remove views by key; new views default associated report/view display to visible with `limit_type="all"`
|
|
56
56
|
- `app_custom_buttons_apply`: use `patch_buttons` for existing-button parameter replacement; use `upsert_buttons` for creation or full target config; configure add-data field mappings/default values; bind buttons to header/detail/list view positions; `placement=list` maps to the backend `INSIDE` row/list button position; merge-mode view configs require `buttons`; use `view_configs[].mode="replace"` or `buttons=[]` to clear a view's custom button bindings. For child-record creation linked to the current source record, map `source_field: "数据ID"` to the target relation field.
|
|
57
57
|
- `app_associated_resources_apply`: attach existing BI reports/views to the Qingflow app associated-resource pool and per-view display area; it does not create or edit QingBI report bodies/configs. Permission is split like the backend: resource pool writes use EditAppAuth, while `view_configs` uses the view config/DataManageAuth path. Use `patch_resources` for existing associated-resource parameter replacement; use `upsert_resources` for creation or full target config; `view_configs`, remove, and reorder may reference existing resources by internal `associated_item_id` or by `chart_id`/`chart_key`/`view_key`; use `match_mappings` with `target_field + operator + source_field/value` for associated view/report filters; publishes after successful writes; omit raw `sourceType`, and use `report_source="dataset"` only to attach an existing BI dataset report. Before `upsert_resources`, read `app_get.associated_resources` and reuse an existing matching `target_app_key + view_key/chart_key`; repeated upsert can create duplicates because `client_key` is only valid inside one apply call.
|
|
58
|
-
- `app_charts_apply`: create/edit/remove/reorder app-source QingBI report bodies/configs with `dataSourceType=qingflow`; it does not create/edit dataset BI reports and does not attach reports to Qingflow app associated-resource display. Use `filters` with `field_name + operator + value/values`; the tool compiles them to QingBI string judge types. Use `patch_charts` for existing-chart parameter replacement; use `upsert_charts` for creation or full target config; supports `target/table` aliases plus QingBI chart types such as `summary`, `columnar`, `area`, `funnel`, `radar`, `scatter`, `dualaxes`, and `map`; charts are immediate-live and do not publish; use `chart_id` when names are not unique
|
|
58
|
+
- `app_charts_apply`: create/edit/remove/reorder app-source QingBI report bodies/configs with `dataSourceType=qingflow`; it does not create/edit dataset BI reports and does not attach reports to Qingflow app associated-resource display. Use `filters` with `field_name + operator + value/values`; the tool compiles them to QingBI string judge types. Use `patch_charts` for existing-chart parameter replacement; use `upsert_charts` for creation or full target config; supports `target/table` aliases plus QingBI chart types such as `summary`, `columnar`, `area`, `funnel`, `radar`, `scatter`, `dualaxes`, and `map`; charts are immediate-live and do not publish; use `chart_id` when names are not unique; for complete systems, submit large chart sets in batches of 4-8 upserts. More than 8 upserts is blocked before write with `CHART_UPSERT_BATCH_TOO_LARGE`; execute `details.suggested_batch_payloads[]` one batch at a time.
|
|
59
59
|
- `portal_apply`: create or replace-update portal pages; use `dash_key` for update mode or `package_id + dash_name` for create mode; create mode only prechecks package add_app, matching backend portal creation; edit mode may omit `sections` for base-info-only updates; when sections are supplied they still use replace semantics
|
|
60
60
|
|
|
61
61
|
For object-level updates, the safe partial syntax is `patch_*` with the object's real selector field plus `set` and optional `unset`. `selector` is only a concept, not a literal key. Examples: `patch_views: [{"view_key": "VIEW_KEY", "set": {...}}]`, `patch_buttons: [{"button_id": 1001, "set": {...}}]`, `patch_resources: [{"associated_item_id": 123, "set": {...}}]`, `patch_charts: [{"chart_id": 456, "set": {...}}]`. The tool reads the current backend config, merges the patch, then submits the full backend payload internally. Do not send a partial `upsert_*` and expect missing required fields to be preserved.
|
|
@@ -6,18 +6,19 @@ Use this when the app already exists and the task is only about workflow.
|
|
|
6
6
|
|
|
7
7
|
1. `builder_tool_contract(tool_name="app_flow_apply")`
|
|
8
8
|
2. `app_get_fields`
|
|
9
|
-
3. `
|
|
10
|
-
4. `
|
|
11
|
-
5. `
|
|
12
|
-
6.
|
|
13
|
-
7.
|
|
14
|
-
8.
|
|
9
|
+
3. Confirm the app has one explicit business status field, usually a `select` named `状态`, `处理状态`, `审批状态`, `工单状态`, or `流程阶段`
|
|
10
|
+
4. `app_get_flow`
|
|
11
|
+
5. `role_search` or `member_search`
|
|
12
|
+
6. `role_create` if the user wants a reusable directory role and no good role exists
|
|
13
|
+
7. start from a canonical preset when possible
|
|
14
|
+
8. patch the skeleton instead of freehanding a full graph
|
|
15
|
+
9. reuse preset node ids when patching:
|
|
15
16
|
- `basic_approval` -> patch `approve_1`
|
|
16
17
|
- `basic_fill_then_approve` -> patch `fill_1` and `approve_1`
|
|
17
18
|
Do not add a second approval/fill node with a new id unless you are intentionally replacing the skeleton.
|
|
18
19
|
The MCP now auto-aligns the simplest single-node preset overrides, but still prefer explicit preset ids so the merged graph stays predictable.
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
10. `app_flow_apply`
|
|
21
|
+
11. `app_get_flow` when apply returns `partial_success` or the user asked for verification
|
|
21
22
|
|
|
22
23
|
If you are unsure about presets or node shapes, call `builder_tool_contract(tool_name="app_flow_apply")` before guessing.
|
|
23
24
|
|
|
@@ -96,7 +97,18 @@ Preferred fix order:
|
|
|
96
97
|
|
|
97
98
|
### `FLOW_DEPENDENCY_MISSING`
|
|
98
99
|
|
|
99
|
-
The workflow depends on fields that do not exist yet, usually
|
|
100
|
+
The workflow depends on fields that do not exist yet, usually an explicit business status field. Fix schema first.
|
|
101
|
+
|
|
102
|
+
Do:
|
|
103
|
+
|
|
104
|
+
- add a business field such as `状态`, `处理状态`, `审批状态`, `工单状态`, or `流程阶段`
|
|
105
|
+
- use type `select` / `single_select`
|
|
106
|
+
- use business options such as `草稿`, `进行中`, `已完成`
|
|
107
|
+
|
|
108
|
+
Do not:
|
|
109
|
+
|
|
110
|
+
- create platform workflow system fields such as `当前流程状态`, `当前处理人`, `当前处理节点`, or `流程标题`
|
|
111
|
+
- skip the flow and still report workflow completion
|
|
100
112
|
|
|
101
113
|
Preferred recovery:
|
|
102
114
|
|
|
@@ -272,7 +272,32 @@ At least one `query_conditions.rows` field does not exist on the app.
|
|
|
272
272
|
|
|
273
273
|
### `INVALID_QUERY_CONDITION_FIELD`
|
|
274
274
|
|
|
275
|
-
The field exists but cannot be used in the frontend query-condition panel
|
|
275
|
+
The field exists but cannot be used in the frontend query-condition panel.
|
|
276
|
+
|
|
277
|
+
Supported `query_conditions.rows` fields are query-panel fields:
|
|
278
|
+
|
|
279
|
+
- text / long text
|
|
280
|
+
- number / amount
|
|
281
|
+
- date / datetime
|
|
282
|
+
- single select / multi select
|
|
283
|
+
- member / department
|
|
284
|
+
- phone / email
|
|
285
|
+
- boolean
|
|
286
|
+
|
|
287
|
+
Do not use these fields in `query_conditions`:
|
|
288
|
+
|
|
289
|
+
- relation
|
|
290
|
+
- attachment
|
|
291
|
+
- subtable or subtable subfield
|
|
292
|
+
- address/location
|
|
293
|
+
- Q-Linker
|
|
294
|
+
- code block
|
|
295
|
+
|
|
296
|
+
Fix path:
|
|
297
|
+
|
|
298
|
+
- If you wanted a saved filter that opens with the view, use `filters`.
|
|
299
|
+
- If you wanted a related report/view to match the current record, use `app_associated_resources_apply.match_mappings`.
|
|
300
|
+
- If you only need frontend search fields, remove unsupported fields from `query_conditions.rows` and keep query-panel supported fields only.
|
|
276
301
|
|
|
277
302
|
## Notes
|
|
278
303
|
|
|
@@ -281,6 +306,6 @@ The field exists but cannot be used in the frontend query-condition panel, such
|
|
|
281
306
|
- `app_get_views` should be treated as canonical readback and now returns `columns`
|
|
282
307
|
- If `app_views_apply` returns `AMBIGUOUS_VIEW`, stop and re-run `app_get_views`; then retry with the exact `view_key`
|
|
283
308
|
- `filters` are ANDed together as one flat condition group
|
|
284
|
-
- `query_conditions.rows` does not mean OR; it controls frontend query-field layout
|
|
309
|
+
- `query_conditions.rows` does not mean OR; it controls frontend query-field layout and should only contain query-panel supported fields
|
|
285
310
|
- `app_views_apply` publishes by default
|
|
286
311
|
- For select-style filters, success means the backend preserved the option value in readback, not just that the view name now exists
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
VALID_PORTAL_STATUSES = {"verified", "unverified", "not_created"}
|
|
12
|
+
VALID_APP_VERIFY_STATUSES = {"verified", "unverified", "failed"}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main(argv: list[str] | None = None) -> int:
|
|
16
|
+
parser = argparse.ArgumentParser(description="Validate a Qingflow complete-system delivery summary JSON file.")
|
|
17
|
+
parser.add_argument("summary_file", nargs="?", default="tmp/qingflow_system_build_summary.json")
|
|
18
|
+
args = parser.parse_args(argv)
|
|
19
|
+
path = Path(args.summary_file)
|
|
20
|
+
issues = validate_summary_file(path)
|
|
21
|
+
if issues:
|
|
22
|
+
_emit({"status": "failed", "error_code": "SYSTEM_BUILD_SUMMARY_INVALID", "path": str(path), "issues": issues})
|
|
23
|
+
return 1
|
|
24
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
25
|
+
_emit(
|
|
26
|
+
{
|
|
27
|
+
"status": "success",
|
|
28
|
+
"path": str(path),
|
|
29
|
+
"summary": {
|
|
30
|
+
"package_id": payload.get("package_id"),
|
|
31
|
+
"package_name": payload.get("package_name"),
|
|
32
|
+
"portal_dash_key": payload.get("portal_dash_key"),
|
|
33
|
+
"portal_live_status": payload.get("portal_live_status"),
|
|
34
|
+
"app_count": len(payload.get("apps") or []),
|
|
35
|
+
"partial_count": len(payload.get("partial_items") or []),
|
|
36
|
+
"needs_followup_count": len(payload.get("needs_followup") or []),
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
return 0
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def validate_summary_file(path: Path) -> list[dict[str, Any]]:
|
|
44
|
+
if not path.exists():
|
|
45
|
+
return [_issue("MISSING_FILE", "$", f"summary file does not exist: {path}")]
|
|
46
|
+
try:
|
|
47
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
48
|
+
except OSError as exc:
|
|
49
|
+
return [_issue("READ_FAILED", "$", str(exc))]
|
|
50
|
+
except json.JSONDecodeError as exc:
|
|
51
|
+
return [_issue("INVALID_JSON", "$", exc.msg)]
|
|
52
|
+
if not isinstance(payload, dict):
|
|
53
|
+
return [_issue("INVALID_ROOT", "$", "summary root must be a JSON object")]
|
|
54
|
+
|
|
55
|
+
issues: list[dict[str, Any]] = []
|
|
56
|
+
_require_positive_int(payload, "package_id", issues)
|
|
57
|
+
_require_nonempty_string(payload, "package_name", issues)
|
|
58
|
+
_require_bool(payload, "front_end_visible", issues)
|
|
59
|
+
_require_string_enum(payload, "portal_live_status", VALID_PORTAL_STATUSES, issues)
|
|
60
|
+
if payload.get("portal_live_status") == "verified":
|
|
61
|
+
_require_nonempty_string(payload, "portal_dash_key", issues)
|
|
62
|
+
elif "portal_dash_key" in payload and payload.get("portal_dash_key") is not None and not isinstance(payload.get("portal_dash_key"), str):
|
|
63
|
+
issues.append(_issue("INVALID_TYPE", "$.portal_dash_key", "portal_dash_key must be a string when present"))
|
|
64
|
+
|
|
65
|
+
apps = payload.get("apps")
|
|
66
|
+
if not isinstance(apps, list) or not apps:
|
|
67
|
+
issues.append(_issue("INVALID_APPS", "$.apps", "apps must be a non-empty array"))
|
|
68
|
+
else:
|
|
69
|
+
for index, app in enumerate(apps):
|
|
70
|
+
prefix = f"$.apps[{index}]"
|
|
71
|
+
if not isinstance(app, dict):
|
|
72
|
+
issues.append(_issue("INVALID_APP_ITEM", prefix, "app item must be an object"))
|
|
73
|
+
continue
|
|
74
|
+
_require_nonempty_string(app, "app_key", issues, prefix)
|
|
75
|
+
_require_nonempty_string(app, "app_name", issues, prefix)
|
|
76
|
+
for key in ("fields_count", "views_count", "flows_count", "charts_count"):
|
|
77
|
+
_require_nonnegative_int(app, key, issues, prefix)
|
|
78
|
+
_require_string_enum(app, "publish_verify_status", VALID_APP_VERIFY_STATUSES, issues, prefix)
|
|
79
|
+
|
|
80
|
+
for key in ("warnings", "partial_items", "needs_followup"):
|
|
81
|
+
if not isinstance(payload.get(key), list):
|
|
82
|
+
issues.append(_issue("INVALID_TYPE", f"$.{key}", f"{key} must be an array"))
|
|
83
|
+
return issues
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _require_positive_int(payload: dict[str, Any], key: str, issues: list[dict[str, Any]], prefix: str = "$") -> None:
|
|
87
|
+
value = payload.get(key)
|
|
88
|
+
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
|
|
89
|
+
issues.append(_issue("INVALID_TYPE", f"{prefix}.{key}", f"{key} must be a positive integer"))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _require_nonnegative_int(payload: dict[str, Any], key: str, issues: list[dict[str, Any]], prefix: str = "$") -> None:
|
|
93
|
+
value = payload.get(key)
|
|
94
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
|
95
|
+
issues.append(_issue("INVALID_TYPE", f"{prefix}.{key}", f"{key} must be a non-negative integer"))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _require_nonempty_string(payload: dict[str, Any], key: str, issues: list[dict[str, Any]], prefix: str = "$") -> None:
|
|
99
|
+
value = payload.get(key)
|
|
100
|
+
if not isinstance(value, str) or not value.strip():
|
|
101
|
+
issues.append(_issue("INVALID_TYPE", f"{prefix}.{key}", f"{key} must be a non-empty string"))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _require_bool(payload: dict[str, Any], key: str, issues: list[dict[str, Any]], prefix: str = "$") -> None:
|
|
105
|
+
if not isinstance(payload.get(key), bool):
|
|
106
|
+
issues.append(_issue("INVALID_TYPE", f"{prefix}.{key}", f"{key} must be a boolean"))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _require_string_enum(payload: dict[str, Any], key: str, allowed: set[str], issues: list[dict[str, Any]], prefix: str = "$") -> None:
|
|
110
|
+
value = payload.get(key)
|
|
111
|
+
if value not in allowed:
|
|
112
|
+
issues.append(_issue("INVALID_VALUE", f"{prefix}.{key}", f"{key} must be one of {sorted(allowed)}"))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _issue(code: str, path: str, message: str) -> dict[str, Any]:
|
|
116
|
+
return {"code": code, "path": path, "message": message}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _emit(payload: dict[str, Any]) -> None:
|
|
120
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
if __name__ == "__main__":
|
|
124
|
+
sys.exit(main())
|
|
@@ -7,7 +7,7 @@ metadata:
|
|
|
7
7
|
|
|
8
8
|
# Qingflow App Builder Code Integrations
|
|
9
9
|
|
|
10
|
-
> **Skill 版本**:`qingflow-skills-2026.06.
|
|
10
|
+
> **Skill 版本**:`qingflow-skills-2026.06.24.1`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
|
|
11
11
|
|
|
12
12
|
Use this skill when the user wants to build or repair:
|
|
13
13
|
- `code_block` fields
|
|
@@ -101,6 +101,7 @@ from .models import (
|
|
|
101
101
|
)
|
|
102
102
|
|
|
103
103
|
BUILDER_PORTAL_LIST_DETAIL_VERIFY_LIMIT = 50
|
|
104
|
+
CHART_APPLY_RECOMMENDED_UPSERT_BATCH_SIZE = 8
|
|
104
105
|
|
|
105
106
|
|
|
106
107
|
QUESTION_TYPE_TO_FIELD_TYPE: dict[int, str] = {
|
|
@@ -186,6 +187,37 @@ QUERY_CONDITION_UNSUPPORTED_FIELD_TYPES = {
|
|
|
186
187
|
FieldType.relation.value,
|
|
187
188
|
FieldType.subtable.value,
|
|
188
189
|
}
|
|
190
|
+
QUERY_CONDITION_UNSUPPORTED_FIELD_TYPES_PUBLIC = sorted(
|
|
191
|
+
{
|
|
192
|
+
"address",
|
|
193
|
+
"attachment",
|
|
194
|
+
"code_block",
|
|
195
|
+
"q_linker",
|
|
196
|
+
"relation",
|
|
197
|
+
"subtable",
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
QUERY_CONDITION_SUPPORTED_FIELD_TYPES_PUBLIC = sorted(
|
|
201
|
+
{
|
|
202
|
+
"text",
|
|
203
|
+
"long_text",
|
|
204
|
+
"number",
|
|
205
|
+
"amount",
|
|
206
|
+
"date",
|
|
207
|
+
"datetime",
|
|
208
|
+
"single_select",
|
|
209
|
+
"multi_select",
|
|
210
|
+
"phone",
|
|
211
|
+
"email",
|
|
212
|
+
"boolean",
|
|
213
|
+
"member",
|
|
214
|
+
"department",
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
QUERY_CONDITION_FIX_HINT = (
|
|
218
|
+
"Remove this field from query_conditions. query_conditions only configures the frontend query panel for supported query fields. "
|
|
219
|
+
"Use filters for fixed saved filters, and use associated-resource match_mappings for current-record relation/report matching."
|
|
220
|
+
)
|
|
189
221
|
|
|
190
222
|
ASSOCIATED_RESOURCE_LIMIT_TYPE_ALL = 1
|
|
191
223
|
ASSOCIATED_RESOURCE_LIMIT_TYPE_SELECT = 2
|
|
@@ -4378,7 +4410,7 @@ class AiBuilderFacade:
|
|
|
4378
4410
|
"details": {"edit_version_no": None, "associated_item_ids_by_client_key": client_key_to_id, "readback_failed": False},
|
|
4379
4411
|
"request_id": None,
|
|
4380
4412
|
"suggested_next_call": None,
|
|
4381
|
-
"noop":
|
|
4413
|
+
"noop": False,
|
|
4382
4414
|
"warnings": [],
|
|
4383
4415
|
"verification": {"associated_resources_verified": True, "associated_resource_view_configs_verified": True, "readback_loaded": True, "published": False},
|
|
4384
4416
|
"verified": True,
|
|
@@ -7511,7 +7543,12 @@ class AiBuilderFacade:
|
|
|
7511
7543
|
"nodes": public_nodes,
|
|
7512
7544
|
"transitions": transitions,
|
|
7513
7545
|
},
|
|
7514
|
-
details={
|
|
7546
|
+
details={
|
|
7547
|
+
"missing_dependencies": ["status field"],
|
|
7548
|
+
"fix_hint": "Add an explicit business status select field before applying approval workflows. Do not create platform system fields such as 当前流程状态.",
|
|
7549
|
+
"recommended_field_names": ["状态", "处理状态", "审批状态", "工单状态", "流程阶段"],
|
|
7550
|
+
"forbidden_system_field_names": ["当前流程状态", "当前处理人", "当前处理节点", "流程标题"],
|
|
7551
|
+
},
|
|
7515
7552
|
missing_fields=["status"],
|
|
7516
7553
|
suggested_next_call={
|
|
7517
7554
|
"tool_name": "app_schema_apply",
|
|
@@ -7519,7 +7556,7 @@ class AiBuilderFacade:
|
|
|
7519
7556
|
"profile": profile,
|
|
7520
7557
|
"app_key": request.app_key,
|
|
7521
7558
|
"create_if_missing": False,
|
|
7522
|
-
"add_fields": [{"name": "状态", "type": "
|
|
7559
|
+
"add_fields": [{"name": "状态", "type": "select", "options": ["草稿", "进行中", "已完成"], "required": True}],
|
|
7523
7560
|
"update_fields": [],
|
|
7524
7561
|
"remove_fields": [],
|
|
7525
7562
|
},
|
|
@@ -8266,7 +8303,16 @@ class AiBuilderFacade:
|
|
|
8266
8303
|
relation_field_count = _count_relation_fields(current_fields)
|
|
8267
8304
|
relation_limit_verified = relation_field_count <= 1
|
|
8268
8305
|
relation_warnings = (
|
|
8269
|
-
[
|
|
8306
|
+
[
|
|
8307
|
+
_warning(
|
|
8308
|
+
"RELATION_FIELD_LIMIT_RISK",
|
|
8309
|
+
"multiple relation fields were written and verified, but this backend capability can be unstable; do not downgrade to text unless the write fails or relation readback mismatches",
|
|
8310
|
+
severity="warning",
|
|
8311
|
+
relation_field_count=relation_field_count,
|
|
8312
|
+
relation_fields_created=True,
|
|
8313
|
+
do_not_downgrade_to_text_unless_apply_failed=True,
|
|
8314
|
+
)
|
|
8315
|
+
]
|
|
8270
8316
|
if not relation_limit_verified
|
|
8271
8317
|
else []
|
|
8272
8318
|
)
|
|
@@ -8397,6 +8443,8 @@ class AiBuilderFacade:
|
|
|
8397
8443
|
"app_key": target.app_key,
|
|
8398
8444
|
"field_diff": {"added": added, "updated": updated, "removed": removed},
|
|
8399
8445
|
"relation_field_count": relation_field_count,
|
|
8446
|
+
"relation_fields_created": False,
|
|
8447
|
+
"recommended_modeling_fallback": "Keep the primary relation field as relation; use text/reference summary fields only for secondary cross-object links, and report the downgrade as partial.",
|
|
8400
8448
|
"transport_error": {
|
|
8401
8449
|
"http_status": api_error.http_status,
|
|
8402
8450
|
"backend_code": api_error.backend_code,
|
|
@@ -10154,6 +10202,12 @@ class AiBuilderFacade:
|
|
|
10154
10202
|
"backend_code": api_error.backend_code,
|
|
10155
10203
|
"category": api_error.category,
|
|
10156
10204
|
},
|
|
10205
|
+
**_view_apply_failure_diagnostics(
|
|
10206
|
+
patch=patch,
|
|
10207
|
+
current_fields_by_name=current_fields_by_name,
|
|
10208
|
+
backend_code=api_error.backend_code,
|
|
10209
|
+
operation_phase=operation_phase,
|
|
10210
|
+
),
|
|
10157
10211
|
},
|
|
10158
10212
|
}
|
|
10159
10213
|
failed_views.append(failure_entry)
|
|
@@ -11224,6 +11278,56 @@ class AiBuilderFacade:
|
|
|
11224
11278
|
normalized_args["upsert_charts"] = [patch.model_dump(mode="json") for patch in upsert_charts]
|
|
11225
11279
|
normalized_args["patch_results"] = patch_results
|
|
11226
11280
|
|
|
11281
|
+
if len(upsert_charts) > CHART_APPLY_RECOMMENDED_UPSERT_BATCH_SIZE:
|
|
11282
|
+
batching_context = _chart_apply_batching_context(
|
|
11283
|
+
request=request,
|
|
11284
|
+
upsert_charts=upsert_charts,
|
|
11285
|
+
batch_size=CHART_APPLY_RECOMMENDED_UPSERT_BATCH_SIZE,
|
|
11286
|
+
)
|
|
11287
|
+
first_batch = batching_context["suggested_batch_payloads"][0]
|
|
11288
|
+
return finalize({
|
|
11289
|
+
"status": "failed",
|
|
11290
|
+
"error_code": "CHART_UPSERT_BATCH_TOO_LARGE",
|
|
11291
|
+
"recoverable": True,
|
|
11292
|
+
"message": (
|
|
11293
|
+
f"upsert_charts contains {len(upsert_charts)} items; split into batches of "
|
|
11294
|
+
f"{CHART_APPLY_RECOMMENDED_UPSERT_BATCH_SIZE} or fewer before writing"
|
|
11295
|
+
),
|
|
11296
|
+
"normalized_args": normalized_args,
|
|
11297
|
+
"missing_fields": [],
|
|
11298
|
+
"allowed_values": {"chart.chart_type": [member.value for member in PublicChartType], "chart.filter.operator": [member.value for member in ViewFilterOperator]},
|
|
11299
|
+
"details": batching_context,
|
|
11300
|
+
"request_id": None,
|
|
11301
|
+
"suggested_next_call": {"tool_name": "app_charts_apply", "arguments": {"profile": profile, **first_batch}},
|
|
11302
|
+
"backend_code": None,
|
|
11303
|
+
"http_status": None,
|
|
11304
|
+
"noop": False,
|
|
11305
|
+
"warnings": [
|
|
11306
|
+
_warning(
|
|
11307
|
+
"CHART_UPSERT_BATCH_SIZE_BLOCKED",
|
|
11308
|
+
"large chart upsert batches are blocked before write to avoid timeout and unknown write state",
|
|
11309
|
+
upsert_count=len(upsert_charts),
|
|
11310
|
+
max_upsert_count=CHART_APPLY_RECOMMENDED_UPSERT_BATCH_SIZE,
|
|
11311
|
+
)
|
|
11312
|
+
],
|
|
11313
|
+
"verification": {
|
|
11314
|
+
"charts_verified": False,
|
|
11315
|
+
"readback_unavailable": False,
|
|
11316
|
+
"readback_before_retry": False,
|
|
11317
|
+
"chart_delete_readback_results": [],
|
|
11318
|
+
"chart_order_verified": True,
|
|
11319
|
+
"chart_list_source": existing_chart_list_source,
|
|
11320
|
+
},
|
|
11321
|
+
"app_key": app_key,
|
|
11322
|
+
"app_name": app_name,
|
|
11323
|
+
"chart_results": [],
|
|
11324
|
+
"verified": False,
|
|
11325
|
+
"write_executed": False,
|
|
11326
|
+
"write_succeeded": False,
|
|
11327
|
+
"safe_to_retry": True,
|
|
11328
|
+
"next_action": "split_upsert_charts_and_retry",
|
|
11329
|
+
})
|
|
11330
|
+
|
|
11227
11331
|
chart_results: list[dict[str, Any]] = []
|
|
11228
11332
|
created_ids: list[str] = []
|
|
11229
11333
|
updated_ids: list[str] = []
|
|
@@ -11510,6 +11614,15 @@ class AiBuilderFacade:
|
|
|
11510
11614
|
primary_failure = failed_items[0]
|
|
11511
11615
|
primary_error_code = str(primary_failure.get("error_code") or "").strip()
|
|
11512
11616
|
primary_message = str(primary_failure.get("message") or "").strip()
|
|
11617
|
+
retry_context = _chart_apply_retry_context(
|
|
11618
|
+
request=request,
|
|
11619
|
+
failed_items=failed_items,
|
|
11620
|
+
created_ids=created_ids,
|
|
11621
|
+
updated_ids=updated_ids,
|
|
11622
|
+
removed_ids=removed_ids,
|
|
11623
|
+
reordered=reordered,
|
|
11624
|
+
write_executed=write_executed,
|
|
11625
|
+
)
|
|
11513
11626
|
return finalize({
|
|
11514
11627
|
"status": "partial_success" if successful_changes else "failed",
|
|
11515
11628
|
"error_code": "CHART_APPLY_PARTIAL" if successful_changes else primary_error_code or "CHART_APPLY_FAILED",
|
|
@@ -11520,6 +11633,7 @@ class AiBuilderFacade:
|
|
|
11520
11633
|
"allowed_values": {"chart.chart_type": [member.value for member in PublicChartType], "chart.filter.operator": [member.value for member in ViewFilterOperator]},
|
|
11521
11634
|
"details": {
|
|
11522
11635
|
"per_chart_results": chart_results,
|
|
11636
|
+
**retry_context,
|
|
11523
11637
|
**({"readback_error": _transport_error_payload(readback_error)} if readback_error is not None else {}),
|
|
11524
11638
|
},
|
|
11525
11639
|
"request_id": failed_items[0].get("request_id") or (readback_error.request_id if readback_error is not None else None),
|
|
@@ -11536,6 +11650,7 @@ class AiBuilderFacade:
|
|
|
11536
11650
|
"verification": {
|
|
11537
11651
|
"charts_verified": False if failed_items else verified,
|
|
11538
11652
|
"readback_unavailable": any_readback_unavailable,
|
|
11653
|
+
"readback_before_retry": bool(write_executed),
|
|
11539
11654
|
"chart_delete_readback_results": [deepcopy(item) for item in chart_results if item.get("operation") == "delete"],
|
|
11540
11655
|
"chart_order_verified": False if request.reorder_chart_ids else True,
|
|
11541
11656
|
"chart_list_source": readback_list_source or existing_chart_list_source,
|
|
@@ -11546,6 +11661,7 @@ class AiBuilderFacade:
|
|
|
11546
11661
|
"verified": False if failed_items else verified,
|
|
11547
11662
|
"write_executed": write_executed,
|
|
11548
11663
|
"write_succeeded": write_succeeded,
|
|
11664
|
+
**({"write_may_have_succeeded": True, "next_action": "readback_before_retry"} if write_executed else {}),
|
|
11549
11665
|
"safe_to_retry": not write_executed,
|
|
11550
11666
|
})
|
|
11551
11667
|
result_verified = verified or noop
|
|
@@ -15682,6 +15798,155 @@ def _public_chart_filter_groups_from_qingbi_config(config: dict[str, Any]) -> li
|
|
|
15682
15798
|
return groups
|
|
15683
15799
|
|
|
15684
15800
|
|
|
15801
|
+
def _chart_apply_retry_context(
|
|
15802
|
+
*,
|
|
15803
|
+
request: ChartApplyRequest,
|
|
15804
|
+
failed_items: list[dict[str, Any]],
|
|
15805
|
+
created_ids: list[str],
|
|
15806
|
+
updated_ids: list[str],
|
|
15807
|
+
removed_ids: list[str],
|
|
15808
|
+
reordered: bool,
|
|
15809
|
+
write_executed: bool,
|
|
15810
|
+
) -> dict[str, Any]:
|
|
15811
|
+
failed_chart_names = [
|
|
15812
|
+
str(item.get("name") or "").strip()
|
|
15813
|
+
for item in failed_items
|
|
15814
|
+
if str(item.get("name") or "").strip()
|
|
15815
|
+
]
|
|
15816
|
+
failed_delete_ids = [
|
|
15817
|
+
str(item.get("chart_id") or "").strip()
|
|
15818
|
+
for item in failed_items
|
|
15819
|
+
if str(item.get("operation") or "") == "delete" and str(item.get("chart_id") or "").strip()
|
|
15820
|
+
]
|
|
15821
|
+
reorder_failed = any(str(item.get("operation") or "") == "reorder" for item in failed_items)
|
|
15822
|
+
failed_name_set = set(failed_chart_names)
|
|
15823
|
+
retry_payload: dict[str, Any] = {"app_key": request.app_key}
|
|
15824
|
+
retry_upserts = [
|
|
15825
|
+
patch.model_dump(mode="json", exclude_none=True)
|
|
15826
|
+
for patch in request.upsert_charts
|
|
15827
|
+
if patch.name in failed_name_set
|
|
15828
|
+
]
|
|
15829
|
+
if retry_upserts:
|
|
15830
|
+
retry_payload["upsert_charts"] = retry_upserts
|
|
15831
|
+
if failed_delete_ids:
|
|
15832
|
+
retry_payload["remove_chart_ids"] = failed_delete_ids
|
|
15833
|
+
if reorder_failed and request.reorder_chart_ids:
|
|
15834
|
+
retry_payload["reorder_chart_ids"] = list(request.reorder_chart_ids)
|
|
15835
|
+
has_retry_payload = any(key in retry_payload for key in ("upsert_charts", "remove_chart_ids", "reorder_chart_ids"))
|
|
15836
|
+
context: dict[str, Any] = {
|
|
15837
|
+
"created_chart_ids": list(created_ids),
|
|
15838
|
+
"updated_chart_ids": list(updated_ids),
|
|
15839
|
+
"removed_chart_ids": list(removed_ids),
|
|
15840
|
+
"failed_chart_names": failed_chart_names,
|
|
15841
|
+
"failed_delete_chart_ids": failed_delete_ids,
|
|
15842
|
+
"readback_first": bool(write_executed),
|
|
15843
|
+
"retry_rule": (
|
|
15844
|
+
"read app charts before retrying; retry only the failed chart payloads if they are still missing or unverified"
|
|
15845
|
+
if write_executed
|
|
15846
|
+
else "fix the failed chart payloads and retry only those items"
|
|
15847
|
+
),
|
|
15848
|
+
}
|
|
15849
|
+
if has_retry_payload:
|
|
15850
|
+
context["suggested_retry_payload"] = retry_payload
|
|
15851
|
+
return context
|
|
15852
|
+
|
|
15853
|
+
|
|
15854
|
+
def _chart_apply_batching_context(
|
|
15855
|
+
*,
|
|
15856
|
+
request: ChartApplyRequest,
|
|
15857
|
+
upsert_charts: list[ChartUpsertPatch],
|
|
15858
|
+
batch_size: int,
|
|
15859
|
+
) -> dict[str, Any]:
|
|
15860
|
+
suggested_batch_payloads: list[dict[str, Any]] = []
|
|
15861
|
+
for start in range(0, len(upsert_charts), batch_size):
|
|
15862
|
+
batch = upsert_charts[start : start + batch_size]
|
|
15863
|
+
suggested_batch_payloads.append(
|
|
15864
|
+
{
|
|
15865
|
+
"app_key": request.app_key,
|
|
15866
|
+
"upsert_charts": [patch.model_dump(mode="json", exclude_none=True) for patch in batch],
|
|
15867
|
+
}
|
|
15868
|
+
)
|
|
15869
|
+
|
|
15870
|
+
followup_payload: dict[str, Any] = {"app_key": request.app_key}
|
|
15871
|
+
if request.remove_chart_ids:
|
|
15872
|
+
followup_payload["remove_chart_ids"] = list(request.remove_chart_ids)
|
|
15873
|
+
if request.reorder_chart_ids:
|
|
15874
|
+
followup_payload["reorder_chart_ids"] = list(request.reorder_chart_ids)
|
|
15875
|
+
if len(followup_payload) > 1:
|
|
15876
|
+
suggested_batch_payloads.append(followup_payload)
|
|
15877
|
+
|
|
15878
|
+
return {
|
|
15879
|
+
"upsert_count": len(upsert_charts),
|
|
15880
|
+
"max_upsert_count": batch_size,
|
|
15881
|
+
"write_executed": False,
|
|
15882
|
+
"readback_first": False,
|
|
15883
|
+
"retry_rule": "run suggested_batch_payloads one at a time; do not submit the original large upsert_charts array",
|
|
15884
|
+
"suggested_batch_payloads": suggested_batch_payloads,
|
|
15885
|
+
}
|
|
15886
|
+
|
|
15887
|
+
|
|
15888
|
+
def _view_apply_failure_diagnostics(
|
|
15889
|
+
*,
|
|
15890
|
+
patch: ViewUpsertPatch,
|
|
15891
|
+
current_fields_by_name: dict[str, dict[str, Any]],
|
|
15892
|
+
backend_code: Any,
|
|
15893
|
+
operation_phase: str,
|
|
15894
|
+
) -> dict[str, Any]:
|
|
15895
|
+
references: list[tuple[str, str]] = []
|
|
15896
|
+
|
|
15897
|
+
def add_reference(name: Any, source: str) -> None:
|
|
15898
|
+
text = str(name or "").strip()
|
|
15899
|
+
if text:
|
|
15900
|
+
references.append((text, source))
|
|
15901
|
+
|
|
15902
|
+
for column in patch.columns:
|
|
15903
|
+
add_reference(column, "columns")
|
|
15904
|
+
add_reference(patch.group_by, "group_by")
|
|
15905
|
+
add_reference(patch.start_field, "start_field")
|
|
15906
|
+
add_reference(patch.end_field, "end_field")
|
|
15907
|
+
add_reference(patch.title_field, "title_field")
|
|
15908
|
+
for rule in patch.filters:
|
|
15909
|
+
add_reference(rule.field_name, "filters")
|
|
15910
|
+
|
|
15911
|
+
seen: set[str] = set()
|
|
15912
|
+
field_entries: list[dict[str, Any]] = []
|
|
15913
|
+
for field_name, source in references:
|
|
15914
|
+
key = f"{field_name}\0{source}"
|
|
15915
|
+
if key in seen:
|
|
15916
|
+
continue
|
|
15917
|
+
seen.add(key)
|
|
15918
|
+
field = current_fields_by_name.get(field_name) or {}
|
|
15919
|
+
field_entries.append(
|
|
15920
|
+
{
|
|
15921
|
+
"field_name": field_name,
|
|
15922
|
+
"source": source,
|
|
15923
|
+
"field_id": field.get("field_id"),
|
|
15924
|
+
"que_id": field.get("que_id"),
|
|
15925
|
+
"field_type": field.get("type"),
|
|
15926
|
+
"que_type": field.get("que_type"),
|
|
15927
|
+
"exists_in_schema": bool(field),
|
|
15928
|
+
}
|
|
15929
|
+
)
|
|
15930
|
+
diagnostics: dict[str, Any] = {
|
|
15931
|
+
"operation_phase": operation_phase,
|
|
15932
|
+
"field_level_diagnostics": field_entries,
|
|
15933
|
+
}
|
|
15934
|
+
if backend_code_value_int(backend_code) == 40038:
|
|
15935
|
+
diagnostics.update(
|
|
15936
|
+
{
|
|
15937
|
+
"suspected_fields": field_entries,
|
|
15938
|
+
"recovery_hint": "Do not delete app fields or recreate the app. First retry the view with the smallest required columns; then add non-critical columns back one by one.",
|
|
15939
|
+
"recommended_minimal_retry": {
|
|
15940
|
+
"name": patch.name,
|
|
15941
|
+
"type": patch.type.value,
|
|
15942
|
+
"columns": [field_entries[0]["field_name"]] if field_entries else [],
|
|
15943
|
+
**({"filters": [rule.model_dump(mode="json", exclude_none=True) for rule in patch.filters]} if patch.filters else {}),
|
|
15944
|
+
},
|
|
15945
|
+
}
|
|
15946
|
+
)
|
|
15947
|
+
return diagnostics
|
|
15948
|
+
|
|
15949
|
+
|
|
15685
15950
|
def _public_chart_group_by_from_qingbi_config(config: dict[str, Any]) -> list[str]:
|
|
15686
15951
|
fields: list[dict[str, Any]] = []
|
|
15687
15952
|
for key in ("selectedDimensions", "xDimensions", "yDimensions", "selectedTime"):
|
|
@@ -23737,6 +24002,9 @@ def _resolve_query_condition_field(
|
|
|
23737
24002
|
"field": raw,
|
|
23738
24003
|
"parent_field": subfield_parent.get("name"),
|
|
23739
24004
|
"message": "subtable subfields cannot be used as view query conditions",
|
|
24005
|
+
"fix_hint": QUERY_CONDITION_FIX_HINT,
|
|
24006
|
+
"supported_field_types": QUERY_CONDITION_SUPPORTED_FIELD_TYPES_PUBLIC,
|
|
24007
|
+
"unsupported_field_types": QUERY_CONDITION_UNSUPPORTED_FIELD_TYPES_PUBLIC,
|
|
23740
24008
|
}
|
|
23741
24009
|
return {}, {
|
|
23742
24010
|
"error_code": "QUERY_CONDITION_FIELD_NOT_FOUND",
|
|
@@ -23753,6 +24021,9 @@ def _resolve_query_condition_field(
|
|
|
23753
24021
|
"field": field.get("name"),
|
|
23754
24022
|
"field_type": field_type,
|
|
23755
24023
|
"message": "this field type is not supported by the frontend query condition panel",
|
|
24024
|
+
"fix_hint": QUERY_CONDITION_FIX_HINT,
|
|
24025
|
+
"supported_field_types": QUERY_CONDITION_SUPPORTED_FIELD_TYPES_PUBLIC,
|
|
24026
|
+
"unsupported_field_types": QUERY_CONDITION_UNSUPPORTED_FIELD_TYPES_PUBLIC,
|
|
23756
24027
|
}
|
|
23757
24028
|
que_id = _coerce_positive_int(field.get("que_id"))
|
|
23758
24029
|
if que_id is None:
|
|
@@ -23762,6 +24033,8 @@ def _resolve_query_condition_field(
|
|
|
23762
24033
|
"missing_fields": [],
|
|
23763
24034
|
"field": field.get("name"),
|
|
23764
24035
|
"message": "query condition field has no backend queId",
|
|
24036
|
+
"fix_hint": QUERY_CONDITION_FIX_HINT,
|
|
24037
|
+
"supported_field_types": QUERY_CONDITION_SUPPORTED_FIELD_TYPES_PUBLIC,
|
|
23765
24038
|
}
|
|
23766
24039
|
return field, None
|
|
23767
24040
|
|
|
@@ -9,7 +9,7 @@ from ..errors import QingflowApiError, backend_code_value_int, message_looks_lik
|
|
|
9
9
|
from ..public_surface import cli_public_tool_spec_from_namespace
|
|
10
10
|
from ..response_trim import resolve_cli_tool_name, trim_error_response, trim_public_response
|
|
11
11
|
from ..tools.ai_builder_tools import _attach_builder_apply_envelope
|
|
12
|
-
from ..version import get_cli_version
|
|
12
|
+
from ..version import get_cli_version, get_cli_version_info
|
|
13
13
|
from .context import CliContext, build_cli_context
|
|
14
14
|
from .formatters import emit_json_result, emit_text_result
|
|
15
15
|
from .commands import register_all_commands
|
|
@@ -120,12 +120,11 @@ def run(
|
|
|
120
120
|
|
|
121
121
|
|
|
122
122
|
def _handle_version(_args: argparse.Namespace, _context: CliContext) -> dict[str, Any]:
|
|
123
|
-
|
|
123
|
+
info = get_cli_version_info()
|
|
124
124
|
return {
|
|
125
125
|
"ok": True,
|
|
126
126
|
"status": "success",
|
|
127
|
-
|
|
128
|
-
"package": "@qingflow-tech/qingflow-cli",
|
|
127
|
+
**info,
|
|
129
128
|
}
|
|
130
129
|
|
|
131
130
|
|
|
@@ -136,8 +135,7 @@ def _emit_version(*, json_mode: bool, stdout: TextIO) -> int:
|
|
|
136
135
|
{
|
|
137
136
|
"ok": True,
|
|
138
137
|
"status": "success",
|
|
139
|
-
|
|
140
|
-
"package": "@qingflow-tech/qingflow-cli",
|
|
138
|
+
**get_cli_version_info(),
|
|
141
139
|
},
|
|
142
140
|
stream=stdout,
|
|
143
141
|
)
|
|
@@ -631,15 +631,16 @@ class AiBuilderTools(ToolBase):
|
|
|
631
631
|
"allowed_values": {"tool_name": public_tool_names},
|
|
632
632
|
"details": {"reason_path": "tool_name"},
|
|
633
633
|
"suggested_next_call": None,
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
634
|
+
"request_id": None,
|
|
635
|
+
"backend_code": None,
|
|
636
|
+
"http_status": None,
|
|
637
|
+
"noop": False,
|
|
638
|
+
"warnings": [],
|
|
639
|
+
"verification": {},
|
|
640
|
+
"verified": False,
|
|
641
|
+
}
|
|
642
642
|
contract = _builder_contract_with_apply_output(lookup_name, contract)
|
|
643
|
+
contract_summary = _builder_tool_contract_summary(lookup_name, contract)
|
|
643
644
|
return {
|
|
644
645
|
"status": "success",
|
|
645
646
|
"error_code": None,
|
|
@@ -658,6 +659,8 @@ class AiBuilderTools(ToolBase):
|
|
|
658
659
|
"verification": {},
|
|
659
660
|
"verified": True,
|
|
660
661
|
"tool_name": requested,
|
|
662
|
+
"summary": contract_summary,
|
|
663
|
+
"json_paths": contract_summary["json_paths"],
|
|
661
664
|
"contract": contract,
|
|
662
665
|
}
|
|
663
666
|
|
|
@@ -680,6 +683,12 @@ class AiBuilderTools(ToolBase):
|
|
|
680
683
|
"color_count": len(catalog["icon_colors"]),
|
|
681
684
|
"warnings": [],
|
|
682
685
|
"verification": {"source": "backend AiBuildConstant ICON_NAMES/ICON_COLORS"},
|
|
686
|
+
"json_paths": {
|
|
687
|
+
"icon_names": "$.icon_names",
|
|
688
|
+
"icon_colors": "$.icon_colors",
|
|
689
|
+
"generic_icon_names": "$.generic_icon_names",
|
|
690
|
+
"common_examples": "$.common_examples",
|
|
691
|
+
},
|
|
683
692
|
}
|
|
684
693
|
|
|
685
694
|
@tool_cn_name("分组创建")
|
|
@@ -3927,11 +3936,58 @@ def _builder_contract_with_apply_output(tool_name: str, contract: JSONObject) ->
|
|
|
3927
3936
|
"schema_version": BUILDER_APPLY_SCHEMA_VERSION,
|
|
3928
3937
|
"preferred_ui_fields": ["operation", "summary", "resources"],
|
|
3929
3938
|
"resource_fields": ["resource_type", "operation", "status", "id", "key", "name", "ids", "parent", "icon_config", "error_code", "message"],
|
|
3939
|
+
"json_paths": _builder_apply_json_paths(),
|
|
3930
3940
|
"legacy_fields_preserved": True,
|
|
3931
3941
|
}
|
|
3932
3942
|
return public
|
|
3933
3943
|
|
|
3934
3944
|
|
|
3945
|
+
def _builder_tool_contract_summary(tool_name: str, contract: JSONObject) -> JSONObject:
|
|
3946
|
+
allowed_values = contract.get("allowed_values") if isinstance(contract, dict) else {}
|
|
3947
|
+
allowed_values_keys = sorted(str(key) for key in allowed_values) if isinstance(allowed_values, dict) else []
|
|
3948
|
+
return {
|
|
3949
|
+
"tool_name": tool_name,
|
|
3950
|
+
"contract_path": "$.contract",
|
|
3951
|
+
"allowed_keys_path": "$.contract.allowed_keys",
|
|
3952
|
+
"allowed_values_path": "$.contract.allowed_values",
|
|
3953
|
+
"allowed_values_key_style": "flat_dotted_keys",
|
|
3954
|
+
"minimal_example_path": "$.contract.minimal_example",
|
|
3955
|
+
"execution_notes_path": "$.contract.execution_notes",
|
|
3956
|
+
"top_level_allowed_values_usage": "empty on successful contract lookup; use $.contract.allowed_values instead",
|
|
3957
|
+
"allowed_values_keys_sample": allowed_values_keys[:12],
|
|
3958
|
+
"json_paths": {
|
|
3959
|
+
"contract": "$.contract",
|
|
3960
|
+
"allowed_keys": "$.contract.allowed_keys",
|
|
3961
|
+
"allowed_values": "$.contract.allowed_values",
|
|
3962
|
+
"minimal_example": "$.contract.minimal_example",
|
|
3963
|
+
"execution_notes": "$.contract.execution_notes",
|
|
3964
|
+
"aliases": "$.contract.aliases",
|
|
3965
|
+
},
|
|
3966
|
+
}
|
|
3967
|
+
|
|
3968
|
+
|
|
3969
|
+
def _builder_apply_json_paths() -> JSONObject:
|
|
3970
|
+
return {
|
|
3971
|
+
"status": "$.status",
|
|
3972
|
+
"schema_version": "$.schema_version",
|
|
3973
|
+
"operation": "$.operation",
|
|
3974
|
+
"summary": "$.summary",
|
|
3975
|
+
"resources": "$.resources",
|
|
3976
|
+
"warnings": "$.warnings",
|
|
3977
|
+
"verification": "$.verification",
|
|
3978
|
+
"details": "$.details",
|
|
3979
|
+
"write_executed": "$.write_executed",
|
|
3980
|
+
"write_may_have_succeeded": "$.write_may_have_succeeded",
|
|
3981
|
+
"safe_to_retry": "$.safe_to_retry",
|
|
3982
|
+
"next_action": "$.next_action",
|
|
3983
|
+
"summary_write_executed": "$.summary.write_executed",
|
|
3984
|
+
"summary_safe_to_retry": "$.summary.safe_to_retry",
|
|
3985
|
+
"resource_keys": "$.resources[].key",
|
|
3986
|
+
"resource_ids": "$.resources[].id",
|
|
3987
|
+
"resource_statuses": "$.resources[].status",
|
|
3988
|
+
}
|
|
3989
|
+
|
|
3990
|
+
|
|
3935
3991
|
def _attach_builder_apply_envelope(tool_name: str, payload: JSONObject) -> JSONObject:
|
|
3936
3992
|
if not isinstance(payload, dict):
|
|
3937
3993
|
return payload
|
|
@@ -3940,6 +3996,7 @@ def _attach_builder_apply_envelope(tool_name: str, payload: JSONObject) -> JSONO
|
|
|
3940
3996
|
payload["operation"] = tool_name
|
|
3941
3997
|
payload["resources"] = resources
|
|
3942
3998
|
payload["summary"] = _builder_apply_summary(payload, resources)
|
|
3999
|
+
payload["json_paths"] = _builder_apply_json_paths()
|
|
3943
4000
|
return payload
|
|
3944
4001
|
|
|
3945
4002
|
|
|
@@ -5768,12 +5825,13 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5768
5825
|
"node.type": PUBLIC_STABLE_FLOW_NODE_TYPES,
|
|
5769
5826
|
},
|
|
5770
5827
|
"dependency_hints": [
|
|
5771
|
-
"approval-style workflows require an explicit status field",
|
|
5828
|
+
"approval-style workflows require an explicit business status select field before app_flow_apply, such as 状态 / 处理状态 / 审批状态 / 工单状态",
|
|
5772
5829
|
"approve/fill/copy nodes require at least one assignee",
|
|
5773
5830
|
],
|
|
5774
5831
|
"execution_notes": [
|
|
5775
5832
|
"public flow building is intentionally limited to linear workflows",
|
|
5776
5833
|
"branch and condition nodes are disabled because the backend workflow route is not front-end stable for these node types",
|
|
5834
|
+
"do not create platform system workflow fields such as 当前流程状态 / 当前处理人 / 当前处理节点 / 流程标题; use an explicit business status field instead",
|
|
5777
5835
|
],
|
|
5778
5836
|
"minimal_example": {
|
|
5779
5837
|
"profile": "default",
|
|
@@ -5808,13 +5866,14 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5808
5866
|
"node.type": PUBLIC_STABLE_FLOW_NODE_TYPES,
|
|
5809
5867
|
},
|
|
5810
5868
|
"dependency_hints": [
|
|
5811
|
-
"approval-style workflows require an explicit status field",
|
|
5869
|
+
"approval-style workflows require an explicit business status select field before app_flow_apply, such as 状态 / 处理状态 / 审批状态 / 工单状态",
|
|
5812
5870
|
"approve/fill/copy nodes require at least one assignee",
|
|
5813
5871
|
],
|
|
5814
5872
|
"execution_notes": [
|
|
5815
5873
|
"public flow building is intentionally limited to linear workflows",
|
|
5816
5874
|
"app_flow_apply is replace-only; do not treat node snippets as partial patches",
|
|
5817
5875
|
"branch and condition nodes are disabled because the backend workflow route is not front-end stable for these node types",
|
|
5876
|
+
"do not create platform system workflow fields such as 当前流程状态 / 当前处理人 / 当前处理节点 / 流程标题; use an explicit business status field instead",
|
|
5818
5877
|
"workflow verification only covers linear node structure in the public tool surface",
|
|
5819
5878
|
],
|
|
5820
5879
|
"minimal_example": {
|
|
@@ -5894,7 +5953,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5894
5953
|
"creating a new view follows backend createViewgraphConfig and requires both ViewManagementAuth and DataManageAuth; updating/deleting existing views only requires ViewManagementAuth",
|
|
5895
5954
|
"upsert_views[].visibility may set per-view visibility; omit it to preserve an existing view's auth or default a new view to workspace/not",
|
|
5896
5955
|
"filters are saved fixed filters that apply when the view opens; query_conditions configure the frontend query panel and only apply after a user enters query values",
|
|
5897
|
-
"upsert_views[].query_conditions.rows is a layout matrix of field names; it is compiled to backend queryCondition queIds",
|
|
5956
|
+
"upsert_views[].query_conditions.rows is a layout matrix of query-panel supported field names; it is compiled to backend queryCondition queIds",
|
|
5957
|
+
"do not put relation/attachment/subtable/code_block/q_linker/address fields in query_conditions; use filters for fixed filters or app_associated_resources_apply.match_mappings for current-record relation/report matching",
|
|
5898
5958
|
"use patch_views for partial parameter replacement on existing views; the tool reads current config, merges patch_views[].set/unset, then submits the backend full-save payload internally",
|
|
5899
5959
|
"remove_views accepts a raw view_key or an exact unique view name; after DELETE the tool verifies deletion by single view_key readback, not by a full app view list",
|
|
5900
5960
|
"deleted views return verification.by_view[].delete_executed, readback_status, and safe_to_retry_delete=false; if readback is pending, do not blindly repeat the delete",
|
|
@@ -5945,6 +6005,14 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5945
6005
|
],
|
|
5946
6006
|
"remove_views": [],
|
|
5947
6007
|
},
|
|
6008
|
+
"query_conditions_field_rules": {
|
|
6009
|
+
"supported_field_types": ["text", "long_text", "number", "amount", "date", "datetime", "single_select", "multi_select", "phone", "email", "boolean", "member", "department"],
|
|
6010
|
+
"unsupported_field_types": ["relation", "attachment", "subtable", "address", "code_block", "q_linker"],
|
|
6011
|
+
"use_instead": {
|
|
6012
|
+
"fixed_filter": "filters",
|
|
6013
|
+
"current_record_related_report_or_view": "app_associated_resources_apply.match_mappings",
|
|
6014
|
+
},
|
|
6015
|
+
},
|
|
5948
6016
|
"gantt_example": {
|
|
5949
6017
|
"profile": "default",
|
|
5950
6018
|
"app_key": "APP_KEY",
|
|
@@ -6024,7 +6092,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6024
6092
|
"buttons omitted preserves existing button config; buttons=[] clears all buttons; buttons=[...] replaces the full button config",
|
|
6025
6093
|
"upsert_views[].visibility may set per-view visibility; omit it to preserve an existing view's auth or default a new view to workspace/not",
|
|
6026
6094
|
"filters are saved fixed filters that apply when the view opens; query_conditions configure the frontend query panel and only apply after a user enters query values",
|
|
6027
|
-
"upsert_views[].query_conditions.rows is a layout matrix of field names; it is compiled to backend queryCondition queIds",
|
|
6095
|
+
"upsert_views[].query_conditions.rows is a layout matrix of query-panel supported field names; it is compiled to backend queryCondition queIds",
|
|
6096
|
+
"do not put relation/attachment/subtable/code_block/q_linker/address fields in query_conditions; use filters for fixed filters or app_associated_resources_apply.match_mappings for current-record relation/report matching",
|
|
6028
6097
|
"use patch_views for partial parameter replacement on existing views; the public update mode is patch even though the backend save is still a full view payload",
|
|
6029
6098
|
"remove_views accepts a raw view_key or an exact unique view name; after DELETE the tool verifies deletion by single view_key readback, not by a full app view list",
|
|
6030
6099
|
"deleted views return verification.by_view[].delete_executed, readback_status, and safe_to_retry_delete=false; if readback is pending, do not blindly repeat the delete",
|
|
@@ -6078,6 +6147,14 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6078
6147
|
],
|
|
6079
6148
|
"remove_views": [],
|
|
6080
6149
|
},
|
|
6150
|
+
"query_conditions_field_rules": {
|
|
6151
|
+
"supported_field_types": ["text", "long_text", "number", "amount", "date", "datetime", "single_select", "multi_select", "phone", "email", "boolean", "member", "department"],
|
|
6152
|
+
"unsupported_field_types": ["relation", "attachment", "subtable", "address", "code_block", "q_linker"],
|
|
6153
|
+
"use_instead": {
|
|
6154
|
+
"fixed_filter": "filters",
|
|
6155
|
+
"current_record_related_report_or_view": "app_associated_resources_apply.match_mappings",
|
|
6156
|
+
},
|
|
6157
|
+
},
|
|
6081
6158
|
"gantt_example": {
|
|
6082
6159
|
"profile": "default",
|
|
6083
6160
|
"app_key": "APP_KEY",
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
import sys
|
|
4
7
|
from importlib import metadata
|
|
5
8
|
from pathlib import Path
|
|
6
9
|
|
|
@@ -15,7 +18,31 @@ def get_cli_version() -> str:
|
|
|
15
18
|
return "0+local"
|
|
16
19
|
|
|
17
20
|
|
|
21
|
+
def get_cli_version_info() -> dict[str, str | None]:
|
|
22
|
+
package_root = _find_package_root()
|
|
23
|
+
return {
|
|
24
|
+
"version": get_cli_version(),
|
|
25
|
+
"package": _find_package_name(package_root) or "@qingflow-tech/qingflow-cli",
|
|
26
|
+
"executable_path": _resolve_executable_path(),
|
|
27
|
+
"command_path": shutil.which("qingflow"),
|
|
28
|
+
"package_root": str(package_root) if package_root is not None else None,
|
|
29
|
+
"skill_version": _find_skill_version(package_root),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
18
33
|
def _find_package_json_version() -> str | None:
|
|
34
|
+
package_root = _find_package_root()
|
|
35
|
+
if package_root is None:
|
|
36
|
+
return None
|
|
37
|
+
try:
|
|
38
|
+
payload = json.loads((package_root / "package.json").read_text(encoding="utf-8"))
|
|
39
|
+
except (OSError, json.JSONDecodeError):
|
|
40
|
+
return None
|
|
41
|
+
version = str(payload.get("version") or "")
|
|
42
|
+
return version or None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _find_package_root() -> Path | None:
|
|
19
46
|
current = Path(__file__).resolve()
|
|
20
47
|
for parent in current.parents:
|
|
21
48
|
package_json = parent / "package.json"
|
|
@@ -36,5 +63,48 @@ def _find_package_json_version() -> str | None:
|
|
|
36
63
|
"@josephyan/qingflow-app-user-mcp",
|
|
37
64
|
"@josephyan/qingflow-app-builder-mcp",
|
|
38
65
|
}:
|
|
39
|
-
return
|
|
66
|
+
return parent
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _find_package_name(package_root: Path | None) -> str | None:
|
|
71
|
+
if package_root is None:
|
|
72
|
+
return None
|
|
73
|
+
try:
|
|
74
|
+
payload = json.loads((package_root / "package.json").read_text(encoding="utf-8"))
|
|
75
|
+
except (OSError, json.JSONDecodeError):
|
|
76
|
+
return None
|
|
77
|
+
name = str(payload.get("name") or "")
|
|
78
|
+
return name or None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _resolve_executable_path() -> str | None:
|
|
82
|
+
if not sys.argv:
|
|
83
|
+
return None
|
|
84
|
+
raw = str(sys.argv[0] or "").strip()
|
|
85
|
+
if not raw:
|
|
86
|
+
return None
|
|
87
|
+
try:
|
|
88
|
+
return str(Path(raw).resolve())
|
|
89
|
+
except OSError:
|
|
90
|
+
return raw
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _find_skill_version(package_root: Path | None) -> str | None:
|
|
94
|
+
if package_root is None:
|
|
95
|
+
return None
|
|
96
|
+
candidates = [
|
|
97
|
+
package_root / "skills" / "qingflow-cli" / "SKILL.md",
|
|
98
|
+
package_root / "skill" / "qingflow-cli" / "SKILL.md",
|
|
99
|
+
]
|
|
100
|
+
for skill_file in candidates:
|
|
101
|
+
if not skill_file.exists():
|
|
102
|
+
continue
|
|
103
|
+
try:
|
|
104
|
+
text = skill_file.read_text(encoding="utf-8")
|
|
105
|
+
except OSError:
|
|
106
|
+
continue
|
|
107
|
+
match = re.search(r"Skill\s*版本\**[::]\s*`?([^`\s))]+)", text)
|
|
108
|
+
if match:
|
|
109
|
+
return match.group(1).strip()
|
|
40
110
|
return None
|