@qingflow-tech/qingflow-app-user-mcp 1.0.33 → 1.0.35
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 +32 -8
- package/skills/qingflow-app-builder/references/create-app.md +9 -2
- package/skills/qingflow-app-builder/references/match-rules.md +15 -0
- package/skills/qingflow-app-builder/references/tool-selection.md +10 -3
- package/skills/qingflow-app-builder/references/update-schema.md +3 -0
- package/skills/qingflow-app-builder/references/update-views.md +6 -4
- package/skills/qingflow-record-insert/SKILL.md +2 -0
- package/skills/qingflow-task-ops/SKILL.md +2 -2
- package/src/qingflow_mcp/builder_facade/models.py +50 -1
- package/src/qingflow_mcp/builder_facade/service.py +9 -9
- package/src/qingflow_mcp/cli/commands/builder.py +33 -5
- package/src/qingflow_mcp/tools/ai_builder_tools.py +402 -13
- package/src/qingflow_mcp/tools/record_tools.py +38 -1
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @qingflow-tech/qingflow-app-user-mcp@1.0.
|
|
6
|
+
npm install @qingflow-tech/qingflow-app-user-mcp@1.0.35
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @qingflow-tech/qingflow-app-user-mcp@1.0.
|
|
12
|
+
npx -y -p @qingflow-tech/qingflow-app-user-mcp@1.0.35 qingflow-app-user-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -32,6 +32,23 @@ Default modeling rules:
|
|
|
32
32
|
- Another business object -> a separate app, not a text field
|
|
33
33
|
- Cross-object links -> relation fields, not text fields
|
|
34
34
|
|
|
35
|
+
## Route First
|
|
36
|
+
|
|
37
|
+
Before any builder write, classify the request:
|
|
38
|
+
|
|
39
|
+
- **Complete system / app package**: the user asks for a system, package, workspace module set, or several related forms/apps. Use `package_apply` for the package, then one `app_schema_apply(apps=[...])` for the app shells and fields. Do not squeeze several business objects into one app.
|
|
40
|
+
- **Single app**: the user names one form/app or gives one `app_key`. Use `app_resolve`/`app_get`, then `app_schema_apply` and the app-scoped apply tools.
|
|
41
|
+
- **Record/user operation**: the user wants to add, edit, delete, approve, or analyze data. Route to the record/task skills instead of builder tools.
|
|
42
|
+
|
|
43
|
+
For complete systems, `apps[]` should use stable `client_key` values. Same-call relation fields may use `target_app_ref` for a client key or `target_app` for another app name. Prefer `target_app_ref` when names may collide.
|
|
44
|
+
|
|
45
|
+
Builder schema inputs should follow agent-intuitive semantics:
|
|
46
|
+
|
|
47
|
+
- icons may be `icon/color`, `icon_name/icon_color`, `icon_config`, or `icon: {name, color}`
|
|
48
|
+
- `single_select` / `multi_select` options may be strings or objects such as `{label, value}`; tools normalize to option labels
|
|
49
|
+
- relation fields need a target plus `display_field` and `visible_fields`
|
|
50
|
+
- do not create built-in system fields as form fields: `数据ID`, `编号`, `申请人`, `申请时间`, `创建人`, `创建时间`, `提交人`, `提交时间`, `更新时间`, `更新人`, `当前流程状态`, `当前处理人`, `当前处理节点`, `流程标题`. They are platform-generated; only reference supported system fields where a tool explicitly says so, such as button `source_field: "数据ID"`
|
|
51
|
+
|
|
35
52
|
## Public Tools You Should Think In
|
|
36
53
|
|
|
37
54
|
- Package: `package_get`, `package_apply`
|
|
@@ -51,7 +68,7 @@ Treat these as the official surface. Do not default to `package_create`, `packag
|
|
|
51
68
|
- use `package_apply(...)` for package creation, rename, icon, visibility, grouping, ordering, and app/portal layout
|
|
52
69
|
- Multi-app schema work:
|
|
53
70
|
- use one `app_schema_apply(apps=[...])` / CLI `builder schema apply --apps-file` when creating several apps in one package
|
|
54
|
-
- same-call relation fields may use `target_app_ref` to point at another `apps[].client_key`
|
|
71
|
+
- same-call relation fields may use `target_app_ref` to point at another `apps[].client_key`, or `target_app` to point at another `apps[].app_name`
|
|
55
72
|
- App base permissions:
|
|
56
73
|
- trust `app_get.editability.can_edit_app_base` for app base-info writes like app name, icon, and visibility
|
|
57
74
|
- `can_edit_form` only means schema/form-route capability; it no longer implies app base-info write capability
|
|
@@ -68,6 +85,7 @@ Treat these as the official surface. Do not default to `package_create`, `packag
|
|
|
68
85
|
- dataset BI reports are not created or edited by `app_charts_apply` yet; create them in QingBI first, then attach the existing report with `app_associated_resources_apply` and `report_source="dataset"`
|
|
69
86
|
- it does not attach the report to the Qingflow app associated-resource display; use `app_associated_resources_apply` for that
|
|
70
87
|
- 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`
|
|
88
|
+
- chart `filters` use the unified fixed-filter DSL: `field_name + operator + value/values`; the tool compiles them to QingBI string judge types
|
|
71
89
|
- use `patch_charts` for changing a chart name, visibility, filters, or one config fragment on an existing chart
|
|
72
90
|
- `visibility` is a public capability and should be treated as a base-only permission update
|
|
73
91
|
- do not model chart visibility changes as raw config rewrites
|
|
@@ -92,14 +110,15 @@ Treat these as the official surface. Do not default to `package_create`, `packag
|
|
|
92
110
|
- before creating a resource, check `app_get.associated_resources` for the same `target_app_key + view_key/chart_key`; if it already exists, use `patch_resources` with that `associated_item_id`
|
|
93
111
|
- `client_key` is only a same-call reference for `view_configs[].associated_item_refs`; it is not saved and cannot deduplicate later calls
|
|
94
112
|
- do not pass backend raw `sourceType`; view resources infer the internal Qingflow view source, report/chart resources default to BI app reports, and existing dataset reports use `report_source="dataset"`
|
|
95
|
-
- use `match_mappings` for associated view/report filters; dynamic
|
|
113
|
+
- use `match_mappings` for associated view/report filters; dynamic context matches use `source_field`, static filters use `value`; do not handwrite raw `matchRules`
|
|
96
114
|
- if field type compatibility is unclear, read [references/match-rules.md](references/match-rules.md)
|
|
97
115
|
- Views and flows:
|
|
98
116
|
- stay on `app_views_apply` / `app_flow_apply`
|
|
99
117
|
- use `patch_views` for existing-view parameter changes such as `query_conditions`, `filters`, `columns`, or visibility
|
|
100
118
|
- 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`
|
|
119
|
+
- 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`
|
|
101
120
|
- use `builder_tool_contract` whenever the minimal legal shape is unclear
|
|
102
|
-
- keep view `filters` and `query_conditions` separate: `filters` are fixed saved filters; `query_conditions` configure the frontend query panel fields and only take effect after users enter query values
|
|
121
|
+
- 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
|
|
103
122
|
- 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
|
|
104
123
|
- configure associated reports/views through `app_associated_resources_apply`, not through `app_views_apply`
|
|
105
124
|
|
|
@@ -107,11 +126,15 @@ Treat these as the official surface. Do not default to `package_create`, `packag
|
|
|
107
126
|
|
|
108
127
|
1. Trust the current MCP/session when it is already injected by the runtime; only run auth/workspace recovery after a tool explicitly reports an auth, credential, or workspace error
|
|
109
128
|
2. Confirm whether the task is read-only or write-impacting
|
|
110
|
-
3.
|
|
129
|
+
3. Classify the build scope:
|
|
130
|
+
- complete system/package -> `package_apply` or `package_get`, then one `app_schema_apply(apps=[...])`
|
|
131
|
+
- single app -> `app_resolve` / `app_get`, then app-scoped apply tools
|
|
132
|
+
- record/task/data request -> leave builder and use the matching record/task skill
|
|
133
|
+
4. Resolve the smallest stable target:
|
|
111
134
|
- app-scoped work -> `app_resolve`
|
|
112
135
|
- package-scoped work with known id -> `package_get`
|
|
113
136
|
- portal inventory -> `portal_list`
|
|
114
|
-
|
|
137
|
+
5. Read only the smallest config slice needed:
|
|
115
138
|
- app map -> `app_get` (default entry; includes compact views, charts, custom buttons, and associated resource pool)
|
|
116
139
|
- fields -> `app_get_fields`
|
|
117
140
|
- layout -> `app_get_layout`
|
|
@@ -119,8 +142,8 @@ Treat these as the official surface. Do not default to `package_create`, `packag
|
|
|
119
142
|
- flow -> `app_get_flow`
|
|
120
143
|
- charts -> `app_get_charts` only when the app_get compact list is not enough
|
|
121
144
|
- portal -> `portal_get`
|
|
122
|
-
|
|
123
|
-
|
|
145
|
+
6. If the public shape is unclear, call `builder_tool_contract`
|
|
146
|
+
7. Apply the smallest patch tool that fits:
|
|
124
147
|
- fields -> `app_schema_apply`
|
|
125
148
|
- layout -> `app_layout_apply`
|
|
126
149
|
- flow -> `app_flow_apply`
|
|
@@ -130,7 +153,7 @@ Treat these as the official surface. Do not default to `package_create`, `packag
|
|
|
130
153
|
- existing charts -> `app_charts_apply.patch_charts`; new/full charts -> `app_charts_apply.upsert_charts`
|
|
131
154
|
- portal -> `portal_apply`
|
|
132
155
|
- package metadata/layout -> `package_apply`
|
|
133
|
-
|
|
156
|
+
8. Use `app_publish_verify` only when the user explicitly wants final publish/live verification or you need a dedicated verification pass
|
|
134
157
|
|
|
135
158
|
## Safe Usage Rules
|
|
136
159
|
|
|
@@ -141,6 +164,7 @@ Treat these as the official surface. Do not default to `package_create`, `packag
|
|
|
141
164
|
- Do not use raw `portal_*` writes or raw `qingbi_report_*` writes as the default builder strategy.
|
|
142
165
|
- `app_schema_apply`, `app_layout_apply`, `app_flow_apply`, and `app_views_apply` publish by default; `app_custom_buttons_apply` and `app_associated_resources_apply` publish after at least one write succeeds and do not accept a draft-only `publish` parameter; `app_charts_apply` is immediate-live without publish.
|
|
143
166
|
- When creating or updating fields, configure the app data title/cover directly in field JSON: `as_data_title: true` is required on exactly one readable top-level title field; `as_data_cover: true` is optional and only valid on one top-level `attachment` field.
|
|
167
|
+
- Do not add platform system fields to `add_fields`; if the user asks for record id, number, applicant, creation/update time, or workflow status, use the built-in data/list/readback fields instead of creating duplicate controls.
|
|
144
168
|
- If the same validation error repeats twice, stop guessing and re-read `builder_tool_contract`.
|
|
145
169
|
- For workflow assignees, prefer `role_search` over explicit members unless the user explicitly wants named members.
|
|
146
170
|
- Public flow building is still intentionally limited to stable linear workflows. If a requirement sounds like branches/conditions, explain the limitation instead of freehanding unsupported graph shapes.
|
|
@@ -9,6 +9,7 @@ Do not use this playbook when the user is really asking for a system/package wit
|
|
|
9
9
|
2. create the related apps in one `app_schema_apply(apps=[...])` call
|
|
10
10
|
3. keep package ownership on the public `package_id` path instead of a separate attach step
|
|
11
11
|
4. add same-call relation fields with `target_app_ref` when one new app references another new app
|
|
12
|
+
5. choose explicit non-template `icon + color` for each new package/app
|
|
12
13
|
|
|
13
14
|
Hierarchy reminder:
|
|
14
15
|
|
|
@@ -42,6 +43,8 @@ Use this pattern instead:
|
|
|
42
43
|
|
|
43
44
|
## Example
|
|
44
45
|
|
|
46
|
+
When designing fields, do not add platform system fields such as `数据ID`, `编号`, `申请人`, `申请时间`, `创建人`, `创建时间`, `提交人`, `提交时间`, `更新时间`, `更新人`, `当前流程状态`, `当前处理人`, `当前处理节点`, or `流程标题`. Qingflow provides them automatically; create only business fields.
|
|
47
|
+
|
|
45
48
|
Create a new package only after the user confirms package creation:
|
|
46
49
|
|
|
47
50
|
```json
|
|
@@ -50,7 +53,8 @@ Create a new package only after the user confirms package creation:
|
|
|
50
53
|
"arguments": {
|
|
51
54
|
"profile": "default",
|
|
52
55
|
"package_name": "研发项目管理",
|
|
53
|
-
"create_if_missing": true
|
|
56
|
+
"create_if_missing": true,
|
|
57
|
+
"icon": {"name": "briefcase", "color": "azure"}
|
|
54
58
|
}
|
|
55
59
|
}
|
|
56
60
|
```
|
|
@@ -76,6 +80,7 @@ Apply schema for a new app:
|
|
|
76
80
|
"profile": "default",
|
|
77
81
|
"app_name": "客户订单",
|
|
78
82
|
"package_id": 1218950,
|
|
83
|
+
"icon": {"name": "delivery-box-1", "color": "emerald"},
|
|
79
84
|
"create_if_missing": true,
|
|
80
85
|
"publish": true,
|
|
81
86
|
"add_fields": [
|
|
@@ -83,7 +88,7 @@ Apply schema for a new app:
|
|
|
83
88
|
{"name": "客户名称", "type": "text", "required": true},
|
|
84
89
|
{"name": "订单封面", "type": "attachment", "as_data_cover": true},
|
|
85
90
|
{"name": "订单金额", "type": "amount"},
|
|
86
|
-
{"name": "状态", "type": "single_select", "options": ["草稿", "进行中", "已完成"], "required": true}
|
|
91
|
+
{"name": "状态", "type": "single_select", "options": [{"label": "草稿"}, {"label": "进行中"}, {"label": "已完成"}], "required": true}
|
|
87
92
|
],
|
|
88
93
|
"update_fields": [],
|
|
89
94
|
"remove_fields": []
|
|
@@ -105,6 +110,7 @@ Apply schema for multiple apps in one call:
|
|
|
105
110
|
{
|
|
106
111
|
"client_key": "customer",
|
|
107
112
|
"app_name": "客户",
|
|
113
|
+
"icon": {"name": "business-personalcard", "color": "emerald"},
|
|
108
114
|
"add_fields": [
|
|
109
115
|
{"name": "客户名称", "type": "text", "required": true, "as_data_title": true}
|
|
110
116
|
]
|
|
@@ -112,6 +118,7 @@ Apply schema for multiple apps in one call:
|
|
|
112
118
|
{
|
|
113
119
|
"client_key": "order",
|
|
114
120
|
"app_name": "订单",
|
|
121
|
+
"icon": {"name": "delivery-box-1", "color": "blue"},
|
|
115
122
|
"add_fields": [
|
|
116
123
|
{"name": "订单编号", "type": "text", "required": true, "as_data_title": true},
|
|
117
124
|
{
|
|
@@ -4,6 +4,12 @@ Use this reference for builder-side field matching rules in custom buttons and a
|
|
|
4
4
|
|
|
5
5
|
This is not the same thing as `record_access` analysis filters or normal view `filters`.
|
|
6
6
|
|
|
7
|
+
Unified public semantics:
|
|
8
|
+
|
|
9
|
+
- Fixed filters use `field_name + operator + value/values`, for example view `filters` and chart `filters`.
|
|
10
|
+
- Current-record context matches use `target_field + operator + source_field/value`, for example associated resource `match_mappings` and add-data button mappings.
|
|
11
|
+
- Do not handwrite `judgeType`, `judgeValues`, or `matchRules`; the CLI compiles them for the target backend protocol.
|
|
12
|
+
|
|
7
13
|
## Where These Rules Apply
|
|
8
14
|
|
|
9
15
|
- `app_custom_buttons_apply.upsert_buttons[].trigger_add_data_config.field_mappings`
|
|
@@ -80,6 +86,15 @@ Use `match_mappings` for associated view/report filters:
|
|
|
80
86
|
|
|
81
87
|
Dynamic conditions use `source_field`; static conditions use `value`.
|
|
82
88
|
|
|
89
|
+
Recommended operators are `eq`, `neq`, `in`, `contains`, `gte`, `lte`, `is_empty`, and `not_empty`; aliases such as `equal`, `equals`, `=`, `!=`, `any_of`, `one_of`, and `empty` are accepted. `field_name` and `field` can be used as aliases for `target_field`. Static single-value filters should use `value`; a single-item `values` array is also accepted.
|
|
90
|
+
|
|
91
|
+
Examples:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{"target_field": "客户ID", "operator": "eq", "source_field": "数据ID"}
|
|
95
|
+
{"target_field": "状态", "operator": "eq", "value": "有效"}
|
|
96
|
+
```
|
|
97
|
+
|
|
83
98
|
For an existing associated resource, put the same filter mapping under `patch_resources[].set`:
|
|
84
99
|
|
|
85
100
|
```json
|
|
@@ -19,6 +19,12 @@ Before picking tools, decide which layer the request targets:
|
|
|
19
19
|
|
|
20
20
|
If the user asks for multiple forms/modules that relate to each other, this is a package-level multi-app task, not a single-app create.
|
|
21
21
|
|
|
22
|
+
Use this split consistently:
|
|
23
|
+
|
|
24
|
+
- Complete system/package: `package_apply` first, then one `app_schema_apply(apps=[...])`.
|
|
25
|
+
- Single app: `app_resolve/app_get` first, then app-scoped apply tools.
|
|
26
|
+
- Record/task/data operation: leave builder and use record/task skills.
|
|
27
|
+
|
|
22
28
|
## Resolve
|
|
23
29
|
|
|
24
30
|
- `package_get`: read one known package by `package_id`; it reads package `baseInfo` first and may warn `PACKAGE_DETAIL_READ_DEGRADED` when the richer detail endpoint needs package edit/add-app permission. Treat that warning as degraded detail, not as package-read failure.
|
|
@@ -48,8 +54,8 @@ These execute normalized patches. Some app apply tools publish by default and st
|
|
|
48
54
|
- `app_flow_apply`: replace workflow
|
|
49
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"`
|
|
50
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.
|
|
51
|
-
- `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` 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.
|
|
52
|
-
- `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 `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
|
|
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
|
|
53
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
|
|
54
60
|
|
|
55
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.
|
|
@@ -65,7 +71,7 @@ For object-level updates, the safe partial syntax is `patch_*` with the object's
|
|
|
65
71
|
- Create a brand new package, then create one app in it:
|
|
66
72
|
`package_apply(create_if_missing=true) -> app_schema_apply`
|
|
67
73
|
- Create a brand new multi-app system/package:
|
|
68
|
-
`package_apply(create_if_missing=true) -> app_schema_apply(apps[])`
|
|
74
|
+
`package_apply(create_if_missing=true) -> app_schema_apply(apps[])`; use `apps[].client_key` plus `target_app_ref`, or `target_app` when referencing by app name
|
|
69
75
|
- Update fields on an existing app:
|
|
70
76
|
`app_resolve -> app_get_fields -> app_schema_apply`
|
|
71
77
|
- Tidy layout:
|
|
@@ -97,3 +103,4 @@ For object-level updates, the safe partial syntax is `patch_*` with the object's
|
|
|
97
103
|
- Do not omit assignees on approval/fill/copy nodes
|
|
98
104
|
- Do not patch preset flows with brand new approval/fill node ids unless you are intentionally replacing the skeleton; reuse preset ids like `approve_1` and `fill_1`
|
|
99
105
|
- Do not guess role ids, member ids, or editable field ids; resolve names first
|
|
106
|
+
- Do not force agent-authored schema into backend-internal names when public aliases exist: icons may use `icon_config` / `icon:{name,color}`, options may use `{label,value}`, and same-call relation targets may use `target_app_ref` / `target_app`.
|
|
@@ -10,6 +10,8 @@ Use this when the app already exists and the task is only about fields.
|
|
|
10
10
|
|
|
11
11
|
## Example
|
|
12
12
|
|
|
13
|
+
Do not add platform system fields as form controls: `数据ID`, `编号`, `申请人`, `申请时间`, `创建人`, `创建时间`, `提交人`, `提交时间`, `更新时间`, `更新人`, `当前流程状态`, `当前处理人`, `当前处理节点`, `流程标题`. They are generated by Qingflow; use readback/list fields when you need them.
|
|
14
|
+
|
|
13
15
|
Read current fields first:
|
|
14
16
|
|
|
15
17
|
```json
|
|
@@ -70,3 +72,4 @@ Treat it as uncertain write state. Read back with `app_get_fields` before retryi
|
|
|
70
72
|
- `checkbox -> multi_select`
|
|
71
73
|
- `as_data_title: true` marks the app data title; the final form must have exactly one top-level data title field
|
|
72
74
|
- `as_data_cover: true` marks the app data cover; the cover is optional and must be a top-level `attachment` field
|
|
75
|
+
- `数据ID`, `编号`, applicant/creation/update time, and workflow status fields are system-generated and must not appear in `add_fields`
|
|
@@ -28,16 +28,18 @@ Canonical rules before any example:
|
|
|
28
28
|
- Always use `columns`
|
|
29
29
|
- Do not emit `column_names`
|
|
30
30
|
- Treat `fields` only as a legacy alias the MCP may normalize, not as the preferred shape
|
|
31
|
-
- Use `filters` with
|
|
31
|
+
- Use `filters` with the unified fixed-filter DSL: `field_name`, `operator`, `value`/`values`; do not write raw `judgeType` / `judgeValues`
|
|
32
32
|
- Use `query_conditions` for the frontend query panel. Do not put query-panel fields into `filters`.
|
|
33
|
+
- Do not create views named `全部数据`, `我的数据`, `我发起的`, `待办`, `已办`, or `抄送`. These are built-in default/system views. New views must use business-specific names such as `订单台账视图`, `客户跟进视图`, or `逾期任务看板`.
|
|
34
|
+
- To change a built-in default/system view, first read the existing raw `view_key` from `app_get.views` or `app_get_views`, then use `patch_views` or a full `upsert_views` item with that `view_key`.
|
|
33
35
|
- New views created by `app_views_apply` default the frontend associated report/view area to visible with `limit_type="all"`. Existing views preserve their current associated-resource display unless `associated_resources` is explicitly patched.
|
|
34
|
-
- Use `app_associated_resources_apply` for the associated report/view resource pool, selected resources, and match rules. Permission is split like the backend: resource pool writes use EditAppAuth, while `view_configs` uses the view config/DataManageAuth path. `associated_item_id` is the internal associated-resource id from `app_get.associated_resources`; `view_configs`, remove, and reorder may also pass an existing resource's `chart_id`/`chart_key`/`view_key`, which the tool resolves to the internal id. Do not write backend raw `sourceType`; reports default to BI app reports, and dataset reports use `report_source="dataset"`. Use `match_mappings` for associated view/report filters; read `match-rules.md` if field type compatibility is unclear.
|
|
36
|
+
- Use `app_associated_resources_apply` for the associated report/view resource pool, selected resources, and match rules. Permission is split like the backend: resource pool writes use EditAppAuth, while `view_configs` uses the view config/DataManageAuth path. `associated_item_id` is the internal associated-resource id from `app_get.associated_resources`; `view_configs`, remove, and reorder may also pass an existing resource's `chart_id`/`chart_key`/`view_key`, which the tool resolves to the internal id. Do not write backend raw `sourceType`; reports default to BI app reports, and dataset reports use `report_source="dataset"`. Use `match_mappings` with `target_field + operator + source_field/value` for associated view/report filters; read `match-rules.md` if field type compatibility is unclear.
|
|
35
37
|
- For gantt, use `start_field`, `end_field`, and optionally `title_field`
|
|
36
38
|
- If `app_get.views` or `app_get_views` shows duplicate view names, include `view_key` in `upsert_views[]` and update that exact target
|
|
37
39
|
- Builder view writes always use the raw `view_key` from `app_get.views[].view_key`, such as `emsrao25rs02`. Do not pass `custom:emsrao25rs02`; that prefixed form is only for record-data `view_id`.
|
|
38
40
|
- For an existing view, prefer `patch_views` for parameter replacement. Do not send a partial `upsert_views` object such as only `name/type/query_conditions`; backend view saves require other type-specific fields, and the patch path preserves them for you.
|
|
39
41
|
|
|
40
|
-
Apply a
|
|
42
|
+
Apply a business table view:
|
|
41
43
|
|
|
42
44
|
```json
|
|
43
45
|
{
|
|
@@ -48,7 +50,7 @@ Apply a default table view:
|
|
|
48
50
|
"publish": true,
|
|
49
51
|
"upsert_views": [
|
|
50
52
|
{
|
|
51
|
-
"name": "
|
|
53
|
+
"name": "订单台账视图",
|
|
52
54
|
"view_key": "VIEW_KEY_IF_DUPLICATE_NAMES_EXIST",
|
|
53
55
|
"type": "table",
|
|
54
56
|
"columns": ["订单编号", "客户名称", "订单金额", "状态"]
|
|
@@ -79,6 +79,7 @@ Without `record_id` / `workflow_node_id` / `fields-file`, the result is a static
|
|
|
79
79
|
16. If post-write detail context matters, read `record_get.fields[]`, `media_assets.items[].local_path`, `file_assets.items[].local_path`, `file_assets.items[].extraction.text_path`, and `semantic_context`; `record_get` follows the frontend storage cookie redirect path for Qingflow attachments, so prefer local paths over remote URLs and do not expect legacy flat record shapes
|
|
80
80
|
17. Treat nested schema shape as guidance, not a brittle contract; do not hard-code transient implementation details like optional nested `field_id` shape when composing inserts
|
|
81
81
|
18. For `partial_success`, read `created_record_ids`, then repair only the failed `items[].row_number` using `failed_fields`; never retry the whole batch after any row has `write_executed=true`
|
|
82
|
+
19. Do not put Qingflow system fields in `fields`: `数据ID`, `编号`, `申请人`, `申请时间`, `创建人`, `创建时间`, `提交人`, `提交时间`, `更新时间`, `更新人`, `当前流程状态`, `当前处理人`, `当前处理节点`, `流程标题`. They are generated by the platform and can be read after creation, not manually inserted.
|
|
82
83
|
|
|
83
84
|
## Field Notes
|
|
84
85
|
|
|
@@ -120,6 +121,7 @@ qingflow --json record insert --app-key APP_KEY --items-file records.json
|
|
|
120
121
|
|
|
121
122
|
- Do not skip `record_insert_schema_get`
|
|
122
123
|
- Do not invent missing required fields
|
|
124
|
+
- Do not fill platform system fields such as `数据ID`, `编号`, `申请人`, `创建时间`, or `更新时间`
|
|
123
125
|
- Do not flatten subtable leaf fields to the top level
|
|
124
126
|
- Do not pre-query or silently guess member / department / relation ids when a natural string is enough
|
|
125
127
|
- Do not retry a whole batch after `created_record_ids` is non-empty
|
|
@@ -62,7 +62,7 @@ Use one of these two modes:
|
|
|
62
62
|
2. Discover the exact target with `task_list`
|
|
63
63
|
3. If the target or action requirements are ambiguous, read `task_get`; otherwise go straight to `task_action_execute`
|
|
64
64
|
4. Execute through `task_action_execute`
|
|
65
|
-
5. After actions, report
|
|
65
|
+
5. After actions, report whether it succeeded, the `task_id`, the executed action, the final route/status, and any warnings
|
|
66
66
|
|
|
67
67
|
## Task-Center Rules
|
|
68
68
|
|
|
@@ -111,7 +111,7 @@ Use one of these two modes:
|
|
|
111
111
|
- Do not execute `task_action_execute` until the user explicitly confirms the chosen action
|
|
112
112
|
- Exception: if the user has already explicitly authorized a concrete action on exact targets, you may execute directly after exact target resolution
|
|
113
113
|
- Avoid actions on ambiguous tasks or records
|
|
114
|
-
- Summarize the final action
|
|
114
|
+
- Summarize the final action by `task_id`; include `app_key`, `record_id`, or `workflow_node_id` only as read-only context when the tool returns them, not as the action locator
|
|
115
115
|
- `reject` requires `payload.audit_feedback`
|
|
116
116
|
- For approve/reject, trust the current task detail or an explicit frontend-provided `formId`; app baseInfo is only a fallback. A baseInfo `40002` is not final task-action denial when `formId` is already known.
|
|
117
117
|
- `save_only` requires non-empty `fields` and is only available when the backend exposes editable fields for the current node
|
|
@@ -1259,11 +1259,30 @@ class CustomButtonFieldMappingPatch(StrictModel):
|
|
|
1259
1259
|
|
|
1260
1260
|
|
|
1261
1261
|
class FieldMatchMappingPatch(StrictModel):
|
|
1262
|
-
target_field: Any = Field(
|
|
1262
|
+
target_field: Any = Field(
|
|
1263
|
+
validation_alias=AliasChoices("target_field", "targetField", "target", "field", "field_name", "fieldName")
|
|
1264
|
+
)
|
|
1263
1265
|
source_field: Any | None = Field(default=None, validation_alias=AliasChoices("source_field", "sourceField", "source"))
|
|
1264
1266
|
value: Any | None = Field(default=None, validation_alias=AliasChoices("value", "static_value", "staticValue"))
|
|
1265
1267
|
operator: str = Field(default="eq", validation_alias=AliasChoices("operator", "op", "judge_type", "judgeType"))
|
|
1266
1268
|
|
|
1269
|
+
@model_validator(mode="before")
|
|
1270
|
+
@classmethod
|
|
1271
|
+
def normalize_semantic_aliases(cls, value: Any) -> Any:
|
|
1272
|
+
if not isinstance(value, dict):
|
|
1273
|
+
return value
|
|
1274
|
+
payload = dict(value)
|
|
1275
|
+
has_static_value = any(key in payload for key in ("value", "static_value", "staticValue"))
|
|
1276
|
+
if "values" in payload:
|
|
1277
|
+
values = payload.pop("values")
|
|
1278
|
+
if has_static_value:
|
|
1279
|
+
return payload
|
|
1280
|
+
if isinstance(values, list) and len(values) == 1:
|
|
1281
|
+
payload["value"] = values[0]
|
|
1282
|
+
else:
|
|
1283
|
+
payload["value"] = values
|
|
1284
|
+
return payload
|
|
1285
|
+
|
|
1267
1286
|
@model_validator(mode="after")
|
|
1268
1287
|
def validate_shape(self) -> "FieldMatchMappingPatch":
|
|
1269
1288
|
has_source = self.source_field is not None and str(self.source_field).strip() != ""
|
|
@@ -2292,6 +2311,8 @@ def _normalize_field_payload(value: Any) -> Any:
|
|
|
2292
2311
|
payload = dict(value)
|
|
2293
2312
|
if "fields" in payload and "subfields" not in payload:
|
|
2294
2313
|
payload["subfields"] = payload.pop("fields")
|
|
2314
|
+
if "options" in payload:
|
|
2315
|
+
payload["options"] = _normalize_field_options(payload.get("options"))
|
|
2295
2316
|
raw_type = payload.get("type")
|
|
2296
2317
|
if isinstance(raw_type, int):
|
|
2297
2318
|
normalized_from_id = FIELD_TYPE_ID_ALIASES.get(raw_type)
|
|
@@ -2324,6 +2345,34 @@ def _normalize_field_payload(value: Any) -> Any:
|
|
|
2324
2345
|
return payload
|
|
2325
2346
|
|
|
2326
2347
|
|
|
2348
|
+
def _normalize_field_options(value: Any) -> Any:
|
|
2349
|
+
if not isinstance(value, list):
|
|
2350
|
+
return value
|
|
2351
|
+
normalized: list[str] = []
|
|
2352
|
+
for item in value:
|
|
2353
|
+
if isinstance(item, str):
|
|
2354
|
+
normalized.append(item)
|
|
2355
|
+
continue
|
|
2356
|
+
if isinstance(item, (int, float, bool)):
|
|
2357
|
+
normalized.append(str(item))
|
|
2358
|
+
continue
|
|
2359
|
+
if isinstance(item, dict):
|
|
2360
|
+
label = (
|
|
2361
|
+
item.get("label")
|
|
2362
|
+
or item.get("name")
|
|
2363
|
+
or item.get("title")
|
|
2364
|
+
or item.get("value")
|
|
2365
|
+
or item.get("text")
|
|
2366
|
+
or item.get("optValue")
|
|
2367
|
+
or item.get("optName")
|
|
2368
|
+
)
|
|
2369
|
+
if label is not None:
|
|
2370
|
+
normalized.append(str(label))
|
|
2371
|
+
continue
|
|
2372
|
+
normalized.append(str(item))
|
|
2373
|
+
return normalized
|
|
2374
|
+
|
|
2375
|
+
|
|
2327
2376
|
def _slugify_title(title: str) -> str:
|
|
2328
2377
|
normalized = "".join(ch.lower() if ch.isalnum() else "_" for ch in str(title or ""))
|
|
2329
2378
|
collapsed = "_".join(part for part in normalized.split("_") if part)
|
|
@@ -15049,14 +15049,14 @@ def _build_public_chart_filter_matrix(
|
|
|
15049
15049
|
return []
|
|
15050
15050
|
group: list[dict[str, Any]] = []
|
|
15051
15051
|
judge_map = {
|
|
15052
|
-
ViewFilterOperator.eq.value:
|
|
15053
|
-
ViewFilterOperator.neq.value:
|
|
15054
|
-
ViewFilterOperator.gte.value:
|
|
15055
|
-
ViewFilterOperator.lte.value:
|
|
15056
|
-
ViewFilterOperator.in_.value:
|
|
15057
|
-
ViewFilterOperator.contains.value:
|
|
15058
|
-
ViewFilterOperator.is_empty.value:
|
|
15059
|
-
ViewFilterOperator.not_empty.value:
|
|
15052
|
+
ViewFilterOperator.eq.value: "equal",
|
|
15053
|
+
ViewFilterOperator.neq.value: "unequal",
|
|
15054
|
+
ViewFilterOperator.gte.value: "greaterOrEqual",
|
|
15055
|
+
ViewFilterOperator.lte.value: "lessOrEqual",
|
|
15056
|
+
ViewFilterOperator.in_.value: "anyMatch",
|
|
15057
|
+
ViewFilterOperator.contains.value: "include",
|
|
15058
|
+
ViewFilterOperator.is_empty.value: "isNull",
|
|
15059
|
+
ViewFilterOperator.not_empty.value: "notNull",
|
|
15060
15060
|
}
|
|
15061
15061
|
for rule in rules:
|
|
15062
15062
|
qingbi_field = _resolve_qingbi_chart_field(
|
|
@@ -15074,7 +15074,7 @@ def _build_public_chart_filter_matrix(
|
|
|
15074
15074
|
"fieldId": field_id,
|
|
15075
15075
|
"fieldName": qingbi_field.get("fieldName") or form_field.get("name") or field_id,
|
|
15076
15076
|
"fieldType": qingbi_field.get("fieldType") or _qingbi_field_type_from_public_field(str(form_field.get("type") or "")),
|
|
15077
|
-
"judgeType": judge_map.get(operator,
|
|
15077
|
+
"judgeType": judge_map.get(operator, "equal"),
|
|
15078
15078
|
"judgeValues": values,
|
|
15079
15079
|
"matchType": 1,
|
|
15080
15080
|
}
|
|
@@ -4,6 +4,7 @@ import argparse
|
|
|
4
4
|
|
|
5
5
|
from ..context import CliContext
|
|
6
6
|
from .common import load_list_arg, load_object_arg, raise_config_error, require_list_arg
|
|
7
|
+
from ..json_io import load_json_value
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
@@ -476,26 +477,30 @@ def _handle_chart_get(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
476
477
|
|
|
477
478
|
|
|
478
479
|
def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
|
|
479
|
-
|
|
480
|
+
apps_payload = _load_apps_file_arg(args.apps_file)
|
|
481
|
+
apps = apps_payload["apps"]
|
|
482
|
+
package_id = args.package_id
|
|
483
|
+
if package_id is None and apps_payload.get("package_id") is not None:
|
|
484
|
+
package_id = int(apps_payload["package_id"])
|
|
480
485
|
if args.apps_file:
|
|
481
486
|
if not apps:
|
|
482
487
|
raise_config_error(
|
|
483
488
|
"schema apply multi-app mode requires a non-empty --apps-file.",
|
|
484
|
-
fix_hint="Pass a JSON array with at least one app item.",
|
|
489
|
+
fix_hint="Pass a JSON array, or a JSON object like {\"package_id\":1001,\"apps\":[...]} with at least one app item.",
|
|
485
490
|
)
|
|
486
491
|
if args.app_key or args.app_name or args.app_title or args.add_fields_file or args.update_fields_file or args.remove_fields_file:
|
|
487
492
|
raise_config_error(
|
|
488
493
|
"schema apply multi-app mode accepts --package-id/--create-if-missing plus --apps-file only.",
|
|
489
494
|
fix_hint="Use `--apps-file` for batch mode, or remove `--apps-file` and use the single-app arguments.",
|
|
490
495
|
)
|
|
491
|
-
if
|
|
496
|
+
if package_id is None:
|
|
492
497
|
raise_config_error(
|
|
493
498
|
"schema apply multi-app mode requires --package-id.",
|
|
494
|
-
fix_hint="Pass `--package-id`
|
|
499
|
+
fix_hint="Pass `--package-id`, or put `package_id` at the top level of --apps-file. `package_name` alone does not create the package here; run `builder package apply` first.",
|
|
495
500
|
)
|
|
496
501
|
return context.builder.app_schema_apply(
|
|
497
502
|
profile=args.profile,
|
|
498
|
-
package_id=
|
|
503
|
+
package_id=package_id,
|
|
499
504
|
visibility=load_object_arg(args.visibility_file, option_name="--visibility-file"),
|
|
500
505
|
create_if_missing=bool(args.create_if_missing),
|
|
501
506
|
publish=bool(args.publish),
|
|
@@ -537,6 +542,29 @@ def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
537
542
|
)
|
|
538
543
|
|
|
539
544
|
|
|
545
|
+
def _load_apps_file_arg(path: str | None) -> dict[str, object]:
|
|
546
|
+
if not path:
|
|
547
|
+
return {"apps": []}
|
|
548
|
+
payload = load_json_value(path, option_name="--apps-file")
|
|
549
|
+
if isinstance(payload, list):
|
|
550
|
+
return {"apps": payload}
|
|
551
|
+
if isinstance(payload, dict):
|
|
552
|
+
apps = payload.get("apps")
|
|
553
|
+
if not isinstance(apps, list):
|
|
554
|
+
raise_config_error(
|
|
555
|
+
"--apps-file JSON object requires an apps array.",
|
|
556
|
+
fix_hint="Use {\"package_id\":1001,\"apps\":[...]} or pass a raw JSON array.",
|
|
557
|
+
)
|
|
558
|
+
result: dict[str, object] = {"apps": apps}
|
|
559
|
+
if payload.get("package_id") is not None:
|
|
560
|
+
result["package_id"] = payload.get("package_id")
|
|
561
|
+
return result
|
|
562
|
+
raise_config_error(
|
|
563
|
+
"--apps-file must be a JSON array or an object containing apps.",
|
|
564
|
+
fix_hint="Use [{...}] or {\"package_id\":1001,\"apps\":[...]}",
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
|
|
540
568
|
def _handle_layout_apply(args: argparse.Namespace, context: CliContext) -> dict:
|
|
541
569
|
return context.builder.app_layout_apply(
|
|
542
570
|
profile=args.profile,
|
|
@@ -146,8 +146,11 @@ class AiBuilderTools(ToolBase):
|
|
|
146
146
|
package_id: int | None = None,
|
|
147
147
|
package_name: str | None = None,
|
|
148
148
|
create_if_missing: bool = False,
|
|
149
|
-
icon: str | None = None,
|
|
149
|
+
icon: str | JSONObject | None = None,
|
|
150
150
|
color: str | None = None,
|
|
151
|
+
icon_name: str | None = None,
|
|
152
|
+
icon_color: str | None = None,
|
|
153
|
+
icon_config: JSONObject | None = None,
|
|
151
154
|
visibility: JSONObject | None = None,
|
|
152
155
|
items: list[dict] | None = None,
|
|
153
156
|
allow_detach: bool = False,
|
|
@@ -159,6 +162,9 @@ class AiBuilderTools(ToolBase):
|
|
|
159
162
|
create_if_missing=create_if_missing,
|
|
160
163
|
icon=icon,
|
|
161
164
|
color=color,
|
|
165
|
+
icon_name=icon_name,
|
|
166
|
+
icon_color=icon_color,
|
|
167
|
+
icon_config=icon_config,
|
|
162
168
|
visibility=visibility,
|
|
163
169
|
items=items or None,
|
|
164
170
|
allow_detach=allow_detach,
|
|
@@ -364,8 +370,11 @@ class AiBuilderTools(ToolBase):
|
|
|
364
370
|
package_id: int | None = None,
|
|
365
371
|
app_name: str = "",
|
|
366
372
|
app_title: str = "",
|
|
367
|
-
icon: str = "",
|
|
373
|
+
icon: str | JSONObject = "",
|
|
368
374
|
color: str = "",
|
|
375
|
+
icon_name: str | None = None,
|
|
376
|
+
icon_color: str | None = None,
|
|
377
|
+
icon_config: JSONObject | None = None,
|
|
369
378
|
visibility: JSONObject | None = None,
|
|
370
379
|
create_if_missing: bool = False,
|
|
371
380
|
publish: bool = True,
|
|
@@ -423,6 +432,9 @@ class AiBuilderTools(ToolBase):
|
|
|
423
432
|
app_title=app_title,
|
|
424
433
|
icon=icon,
|
|
425
434
|
color=color,
|
|
435
|
+
icon_name=icon_name,
|
|
436
|
+
icon_color=icon_color,
|
|
437
|
+
icon_config=icon_config,
|
|
426
438
|
visibility=visibility,
|
|
427
439
|
create_if_missing=create_if_missing,
|
|
428
440
|
publish=publish,
|
|
@@ -733,13 +745,23 @@ class AiBuilderTools(ToolBase):
|
|
|
733
745
|
package_id: int | None = None,
|
|
734
746
|
package_name: str | None = None,
|
|
735
747
|
create_if_missing: bool = False,
|
|
736
|
-
icon: str | None = None,
|
|
748
|
+
icon: str | JSONObject | None = None,
|
|
737
749
|
color: str | None = None,
|
|
750
|
+
icon_name: str | None = None,
|
|
751
|
+
icon_color: str | None = None,
|
|
752
|
+
icon_config: JSONObject | None = None,
|
|
738
753
|
visibility: JSONObject | None = None,
|
|
739
754
|
items: list[dict] | None = None,
|
|
740
755
|
allow_detach: bool = False,
|
|
741
756
|
) -> JSONObject:
|
|
742
757
|
"""执行分组与包相关逻辑。"""
|
|
758
|
+
icon, color = _normalize_builder_icon_args(
|
|
759
|
+
icon=icon,
|
|
760
|
+
color=color,
|
|
761
|
+
icon_name=icon_name,
|
|
762
|
+
icon_color=icon_color,
|
|
763
|
+
icon_config=icon_config,
|
|
764
|
+
)
|
|
743
765
|
visibility_patch = None
|
|
744
766
|
if visibility is not None:
|
|
745
767
|
try:
|
|
@@ -1459,6 +1481,22 @@ class AiBuilderTools(ToolBase):
|
|
|
1459
1481
|
remove_fields: list[JSONObject],
|
|
1460
1482
|
) -> JSONObject:
|
|
1461
1483
|
"""执行应用相关逻辑。"""
|
|
1484
|
+
system_field_failure = _reserved_system_field_name_failure(
|
|
1485
|
+
tool_name="app_schema_plan",
|
|
1486
|
+
profile=profile,
|
|
1487
|
+
app_key=app_key,
|
|
1488
|
+
package_id=package_id,
|
|
1489
|
+
app_name=app_name,
|
|
1490
|
+
icon=icon,
|
|
1491
|
+
color=color,
|
|
1492
|
+
visibility=visibility,
|
|
1493
|
+
create_if_missing=create_if_missing,
|
|
1494
|
+
add_fields=add_fields,
|
|
1495
|
+
update_fields=update_fields,
|
|
1496
|
+
remove_fields=remove_fields,
|
|
1497
|
+
)
|
|
1498
|
+
if system_field_failure is not None:
|
|
1499
|
+
return system_field_failure
|
|
1462
1500
|
try:
|
|
1463
1501
|
request = SchemaPlanRequest.model_validate(
|
|
1464
1502
|
{
|
|
@@ -1609,6 +1647,15 @@ class AiBuilderTools(ToolBase):
|
|
|
1609
1647
|
preset: str | None = None,
|
|
1610
1648
|
) -> JSONObject:
|
|
1611
1649
|
"""执行应用相关逻辑。"""
|
|
1650
|
+
reserved_failure = _reserved_system_view_name_failure(
|
|
1651
|
+
tool_name="app_views_plan",
|
|
1652
|
+
profile=profile,
|
|
1653
|
+
app_key=app_key,
|
|
1654
|
+
upsert_views=upsert_views,
|
|
1655
|
+
remove_views=remove_views,
|
|
1656
|
+
)
|
|
1657
|
+
if reserved_failure is not None:
|
|
1658
|
+
return reserved_failure
|
|
1612
1659
|
try:
|
|
1613
1660
|
request = ViewsPlanRequest.model_validate(
|
|
1614
1661
|
{
|
|
@@ -1650,8 +1697,11 @@ class AiBuilderTools(ToolBase):
|
|
|
1650
1697
|
package_id: int | None = None,
|
|
1651
1698
|
app_name: str = "",
|
|
1652
1699
|
app_title: str = "",
|
|
1653
|
-
icon: str = "",
|
|
1700
|
+
icon: str | JSONObject = "",
|
|
1654
1701
|
color: str = "",
|
|
1702
|
+
icon_name: str | None = None,
|
|
1703
|
+
icon_color: str | None = None,
|
|
1704
|
+
icon_config: JSONObject | None = None,
|
|
1655
1705
|
visibility: JSONObject | None = None,
|
|
1656
1706
|
create_if_missing: bool = False,
|
|
1657
1707
|
publish: bool = True,
|
|
@@ -1661,7 +1711,15 @@ class AiBuilderTools(ToolBase):
|
|
|
1661
1711
|
apps: list[JSONObject] | None = None,
|
|
1662
1712
|
) -> JSONObject:
|
|
1663
1713
|
"""执行应用相关逻辑。"""
|
|
1714
|
+
icon, color = _normalize_builder_icon_args(
|
|
1715
|
+
icon=icon,
|
|
1716
|
+
color=color,
|
|
1717
|
+
icon_name=icon_name,
|
|
1718
|
+
icon_color=icon_color,
|
|
1719
|
+
icon_config=icon_config,
|
|
1720
|
+
)
|
|
1664
1721
|
if apps:
|
|
1722
|
+
apps = [_normalize_schema_app_item(item) if isinstance(item, dict) else item for item in apps]
|
|
1665
1723
|
result = self._app_schema_apply_multi(
|
|
1666
1724
|
profile=profile,
|
|
1667
1725
|
package_id=package_id,
|
|
@@ -1725,6 +1783,17 @@ class AiBuilderTools(ToolBase):
|
|
|
1725
1783
|
}
|
|
1726
1784
|
if visibility is not None:
|
|
1727
1785
|
normalized_args["visibility"] = deepcopy(visibility)
|
|
1786
|
+
system_field_failure = _reserved_system_field_name_failure_for_apps(
|
|
1787
|
+
tool_name="app_schema_apply",
|
|
1788
|
+
profile=profile,
|
|
1789
|
+
package_id=package_id,
|
|
1790
|
+
visibility=visibility,
|
|
1791
|
+
create_if_missing=create_if_missing,
|
|
1792
|
+
publish=publish,
|
|
1793
|
+
apps=apps,
|
|
1794
|
+
)
|
|
1795
|
+
if system_field_failure is not None:
|
|
1796
|
+
return system_field_failure
|
|
1728
1797
|
if package_id is None:
|
|
1729
1798
|
return _config_failure(
|
|
1730
1799
|
tool_name="app_schema_apply",
|
|
@@ -1801,6 +1870,7 @@ class AiBuilderTools(ToolBase):
|
|
|
1801
1870
|
)
|
|
1802
1871
|
|
|
1803
1872
|
client_key_to_app_key: dict[str, str] = {}
|
|
1873
|
+
app_name_to_app_key: dict[str, str] = {}
|
|
1804
1874
|
created_app_keys: list[str] = []
|
|
1805
1875
|
results: list[JSONObject] = []
|
|
1806
1876
|
any_write_executed = False
|
|
@@ -1864,11 +1934,14 @@ class AiBuilderTools(ToolBase):
|
|
|
1864
1934
|
created_app_keys.append(resolved_key)
|
|
1865
1935
|
if client_key:
|
|
1866
1936
|
client_key_to_app_key[client_key] = resolved_key
|
|
1937
|
+
resolved_name = str(public_shell.get("app_name_after") or public_shell.get("app_name") or app_name or "").strip()
|
|
1938
|
+
if resolved_name:
|
|
1939
|
+
app_name_to_app_key[resolved_name] = resolved_key
|
|
1867
1940
|
results.append({
|
|
1868
1941
|
"index": index,
|
|
1869
1942
|
"row_number": index + 1,
|
|
1870
1943
|
"client_key": client_key or None,
|
|
1871
|
-
"app_name":
|
|
1944
|
+
"app_name": resolved_name or None,
|
|
1872
1945
|
"app_key": resolved_key,
|
|
1873
1946
|
"status": "shell_ready",
|
|
1874
1947
|
"created": bool(public_shell.get("created")),
|
|
@@ -1888,7 +1961,7 @@ class AiBuilderTools(ToolBase):
|
|
|
1888
1961
|
item = deepcopy(raw_item)
|
|
1889
1962
|
app_key = str(existing.get("app_key") or "").strip()
|
|
1890
1963
|
try:
|
|
1891
|
-
compiled_item = _compile_multi_app_schema_item_refs(item, client_key_to_app_key)
|
|
1964
|
+
compiled_item = _compile_multi_app_schema_item_refs(item, client_key_to_app_key, app_name_to_app_key)
|
|
1892
1965
|
except ValueError as error:
|
|
1893
1966
|
final_items.append({
|
|
1894
1967
|
**{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
|
|
@@ -2012,6 +2085,23 @@ class AiBuilderTools(ToolBase):
|
|
|
2012
2085
|
) -> JSONObject:
|
|
2013
2086
|
"""执行内部辅助逻辑。"""
|
|
2014
2087
|
effective_app_name = app_name or app_title
|
|
2088
|
+
system_field_failure = _reserved_system_field_name_failure(
|
|
2089
|
+
tool_name="app_schema_apply",
|
|
2090
|
+
profile=profile,
|
|
2091
|
+
app_key=app_key,
|
|
2092
|
+
package_id=package_id,
|
|
2093
|
+
app_name=effective_app_name,
|
|
2094
|
+
icon=icon,
|
|
2095
|
+
color=color,
|
|
2096
|
+
visibility=visibility,
|
|
2097
|
+
create_if_missing=create_if_missing,
|
|
2098
|
+
publish=publish,
|
|
2099
|
+
add_fields=add_fields,
|
|
2100
|
+
update_fields=update_fields,
|
|
2101
|
+
remove_fields=remove_fields,
|
|
2102
|
+
)
|
|
2103
|
+
if system_field_failure is not None:
|
|
2104
|
+
return system_field_failure
|
|
2015
2105
|
icon_failure = _validate_workspace_icon_for_builder(
|
|
2016
2106
|
tool_name="app_schema_apply",
|
|
2017
2107
|
icon=icon,
|
|
@@ -2343,6 +2433,17 @@ class AiBuilderTools(ToolBase):
|
|
|
2343
2433
|
remove_views: list[str],
|
|
2344
2434
|
) -> JSONObject:
|
|
2345
2435
|
"""执行内部辅助逻辑。"""
|
|
2436
|
+
reserved_failure = _reserved_system_view_name_failure(
|
|
2437
|
+
tool_name="app_views_apply",
|
|
2438
|
+
profile=profile,
|
|
2439
|
+
app_key=app_key,
|
|
2440
|
+
publish=publish,
|
|
2441
|
+
upsert_views=upsert_views,
|
|
2442
|
+
patch_views=patch_views,
|
|
2443
|
+
remove_views=remove_views,
|
|
2444
|
+
)
|
|
2445
|
+
if reserved_failure is not None:
|
|
2446
|
+
return reserved_failure
|
|
2346
2447
|
if patch_views:
|
|
2347
2448
|
try:
|
|
2348
2449
|
parsed_views = [ViewUpsertPatch.model_validate(item) for item in (upsert_views or [])]
|
|
@@ -2417,7 +2518,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2417
2518
|
"profile": profile,
|
|
2418
2519
|
"app_key": str(plan_args.get("app_key") or app_key),
|
|
2419
2520
|
"publish": publish,
|
|
2420
|
-
"upsert_views": plan_args.get("upsert_views") or [{"name": "
|
|
2521
|
+
"upsert_views": plan_args.get("upsert_views") or [{"name": "业务台账视图", "type": "table", "columns": ["字段A"]}],
|
|
2421
2522
|
"remove_views": plan_args.get("remove_views") or [],
|
|
2422
2523
|
},
|
|
2423
2524
|
},
|
|
@@ -2786,8 +2887,58 @@ def _multi_app_item_failure(index: int, item: object, error_code: str, message:
|
|
|
2786
2887
|
}
|
|
2787
2888
|
|
|
2788
2889
|
|
|
2789
|
-
def
|
|
2890
|
+
def _normalize_builder_icon_args(
|
|
2891
|
+
*,
|
|
2892
|
+
icon: str | JSONObject | None,
|
|
2893
|
+
color: str | None,
|
|
2894
|
+
icon_name: str | None = None,
|
|
2895
|
+
icon_color: str | None = None,
|
|
2896
|
+
icon_config: JSONObject | None = None,
|
|
2897
|
+
) -> tuple[str | None, str | None]:
|
|
2898
|
+
payloads: list[JSONObject] = []
|
|
2899
|
+
if isinstance(icon_config, dict):
|
|
2900
|
+
payloads.append(icon_config)
|
|
2901
|
+
if isinstance(icon, dict):
|
|
2902
|
+
payloads.append(icon)
|
|
2903
|
+
|
|
2904
|
+
normalized_icon = None if isinstance(icon, dict) else icon
|
|
2905
|
+
normalized_color = color
|
|
2906
|
+
for payload in payloads:
|
|
2907
|
+
normalized_icon = normalized_icon or payload.get("icon") or payload.get("icon_name") or payload.get("iconName") or payload.get("name")
|
|
2908
|
+
normalized_color = normalized_color or payload.get("color") or payload.get("icon_color") or payload.get("iconColor")
|
|
2909
|
+
normalized_icon = normalized_icon or icon_name
|
|
2910
|
+
normalized_color = normalized_color or icon_color
|
|
2911
|
+
return (
|
|
2912
|
+
str(normalized_icon).strip() if normalized_icon is not None else None,
|
|
2913
|
+
str(normalized_color).strip() if normalized_color is not None else None,
|
|
2914
|
+
)
|
|
2915
|
+
|
|
2916
|
+
|
|
2917
|
+
def _normalize_schema_app_item(item: JSONObject) -> JSONObject:
|
|
2918
|
+
normalized = deepcopy(item)
|
|
2919
|
+
icon, color = _normalize_builder_icon_args(
|
|
2920
|
+
icon=normalized.get("icon"),
|
|
2921
|
+
color=normalized.get("color"),
|
|
2922
|
+
icon_name=normalized.get("icon_name") or normalized.get("iconName"),
|
|
2923
|
+
icon_color=normalized.get("icon_color") or normalized.get("iconColor"),
|
|
2924
|
+
icon_config=normalized.get("icon_config") or normalized.get("iconConfig"),
|
|
2925
|
+
)
|
|
2926
|
+
if icon:
|
|
2927
|
+
normalized["icon"] = icon
|
|
2928
|
+
if color:
|
|
2929
|
+
normalized["color"] = color
|
|
2930
|
+
for key in ("icon_name", "iconName", "icon_color", "iconColor", "icon_config", "iconConfig"):
|
|
2931
|
+
normalized.pop(key, None)
|
|
2932
|
+
return normalized
|
|
2933
|
+
|
|
2934
|
+
|
|
2935
|
+
def _compile_multi_app_schema_item_refs(
|
|
2936
|
+
item: JSONObject,
|
|
2937
|
+
client_key_to_app_key: dict[str, str],
|
|
2938
|
+
app_name_to_app_key: dict[str, str] | None = None,
|
|
2939
|
+
) -> JSONObject:
|
|
2790
2940
|
compiled = deepcopy(item)
|
|
2941
|
+
app_name_to_app_key = app_name_to_app_key or {}
|
|
2791
2942
|
|
|
2792
2943
|
def visit(value):
|
|
2793
2944
|
if isinstance(value, list):
|
|
@@ -2807,6 +2958,13 @@ def _compile_multi_app_schema_item_refs(item: JSONObject, client_key_to_app_key:
|
|
|
2807
2958
|
if not target_app_key:
|
|
2808
2959
|
raise ValueError(f"target_app_ref '{ref_key}' did not match any apps[].client_key")
|
|
2809
2960
|
payload["target_app_key"] = target_app_key
|
|
2961
|
+
target_app = payload.pop("target_app", None) or payload.pop("targetApp", None)
|
|
2962
|
+
if target_app is not None and not str(payload.get("target_app_key") or "").strip():
|
|
2963
|
+
target_name = str(target_app or "").strip()
|
|
2964
|
+
target_app_key = app_name_to_app_key.get(target_name)
|
|
2965
|
+
if not target_app_key:
|
|
2966
|
+
raise ValueError(f"target_app '{target_name}' did not match any apps[].app_name in the same call")
|
|
2967
|
+
payload["target_app_key"] = target_app_key
|
|
2810
2968
|
return payload
|
|
2811
2969
|
|
|
2812
2970
|
return visit(compiled)
|
|
@@ -2855,7 +3013,7 @@ def _contains_multi_app_target_ref(value: object) -> bool:
|
|
|
2855
3013
|
if not isinstance(value, dict):
|
|
2856
3014
|
return False
|
|
2857
3015
|
for key, entry in value.items():
|
|
2858
|
-
if key in {"target_app_ref", "targetAppRef", "target_app_client_key", "targetAppClientKey"}:
|
|
3016
|
+
if key in {"target_app_ref", "targetAppRef", "target_app_client_key", "targetAppClientKey", "target_app", "targetApp"}:
|
|
2859
3017
|
return True
|
|
2860
3018
|
if _contains_multi_app_target_ref(entry):
|
|
2861
3019
|
return True
|
|
@@ -2968,6 +3126,197 @@ def _visibility_validation_failure(
|
|
|
2968
3126
|
return result
|
|
2969
3127
|
|
|
2970
3128
|
|
|
3129
|
+
_RESERVED_SYSTEM_FIELD_NAMES = {
|
|
3130
|
+
"数据ID",
|
|
3131
|
+
"编号",
|
|
3132
|
+
"申请人",
|
|
3133
|
+
"申请时间",
|
|
3134
|
+
"创建人",
|
|
3135
|
+
"创建时间",
|
|
3136
|
+
"提交人",
|
|
3137
|
+
"提交时间",
|
|
3138
|
+
"更新时间",
|
|
3139
|
+
"更新人",
|
|
3140
|
+
"当前流程状态",
|
|
3141
|
+
"当前处理人",
|
|
3142
|
+
"当前处理节点",
|
|
3143
|
+
"流程标题",
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
|
|
3147
|
+
def _field_patch_name(item: object) -> str:
|
|
3148
|
+
if not isinstance(item, dict):
|
|
3149
|
+
return ""
|
|
3150
|
+
value = item.get("name")
|
|
3151
|
+
if value is None:
|
|
3152
|
+
value = item.get("title")
|
|
3153
|
+
if value is None:
|
|
3154
|
+
value = item.get("field_name")
|
|
3155
|
+
if value is None:
|
|
3156
|
+
value = item.get("fieldName")
|
|
3157
|
+
return str(value or "").strip()
|
|
3158
|
+
|
|
3159
|
+
|
|
3160
|
+
def _reserved_system_field_name_failure(
|
|
3161
|
+
*,
|
|
3162
|
+
tool_name: str,
|
|
3163
|
+
profile: str,
|
|
3164
|
+
app_key: str = "",
|
|
3165
|
+
package_id: int | None = None,
|
|
3166
|
+
app_name: str = "",
|
|
3167
|
+
icon: str | None = None,
|
|
3168
|
+
color: str | None = None,
|
|
3169
|
+
visibility: JSONObject | None = None,
|
|
3170
|
+
create_if_missing: bool = False,
|
|
3171
|
+
publish: bool | None = None,
|
|
3172
|
+
add_fields: list[JSONObject] | None = None,
|
|
3173
|
+
update_fields: list[JSONObject] | None = None,
|
|
3174
|
+
remove_fields: list[JSONObject] | None = None,
|
|
3175
|
+
app_index: int | None = None,
|
|
3176
|
+
) -> JSONObject | None:
|
|
3177
|
+
for index, item in enumerate(add_fields or []):
|
|
3178
|
+
name = _field_patch_name(item)
|
|
3179
|
+
if name not in _RESERVED_SYSTEM_FIELD_NAMES:
|
|
3180
|
+
continue
|
|
3181
|
+
suggested_add_fields = [field for field in (add_fields or []) if _field_patch_name(field) not in _RESERVED_SYSTEM_FIELD_NAMES]
|
|
3182
|
+
arguments: JSONObject = {
|
|
3183
|
+
"profile": profile,
|
|
3184
|
+
"app_key": app_key,
|
|
3185
|
+
"package_id": package_id,
|
|
3186
|
+
"app_name": app_name,
|
|
3187
|
+
"icon": icon or "",
|
|
3188
|
+
"color": color or "",
|
|
3189
|
+
"visibility": visibility,
|
|
3190
|
+
"create_if_missing": create_if_missing,
|
|
3191
|
+
"add_fields": suggested_add_fields,
|
|
3192
|
+
"update_fields": update_fields or [],
|
|
3193
|
+
"remove_fields": remove_fields or [],
|
|
3194
|
+
}
|
|
3195
|
+
if publish is not None:
|
|
3196
|
+
arguments["publish"] = publish
|
|
3197
|
+
return _config_failure(
|
|
3198
|
+
tool_name=tool_name,
|
|
3199
|
+
error_code="RESERVED_SYSTEM_FIELD_NAME",
|
|
3200
|
+
message=f"add_fields[{index}].name uses built-in system field name '{name}'",
|
|
3201
|
+
fix_hint="Do not create form fields named 数据ID, 编号, 申请人, 申请时间, 创建人, 创建时间, 提交人, 提交时间, 更新时间, 更新人, 当前流程状态, 当前处理人, 当前处理节点, or 流程标题. These fields are provided by Qingflow. Remove them from add_fields; only reference them where the tool explicitly supports system fields, such as button source_field 数据ID.",
|
|
3202
|
+
details={
|
|
3203
|
+
"reserved_field_names": sorted(_RESERVED_SYSTEM_FIELD_NAMES),
|
|
3204
|
+
"blocked_index": index,
|
|
3205
|
+
"blocked_name": name,
|
|
3206
|
+
"app_index": app_index,
|
|
3207
|
+
},
|
|
3208
|
+
suggested_next_call={"tool_name": tool_name, "arguments": arguments},
|
|
3209
|
+
)
|
|
3210
|
+
return None
|
|
3211
|
+
|
|
3212
|
+
|
|
3213
|
+
def _reserved_system_field_name_failure_for_apps(
|
|
3214
|
+
*,
|
|
3215
|
+
tool_name: str,
|
|
3216
|
+
profile: str,
|
|
3217
|
+
package_id: int | None,
|
|
3218
|
+
visibility: JSONObject | None,
|
|
3219
|
+
create_if_missing: bool,
|
|
3220
|
+
publish: bool,
|
|
3221
|
+
apps: list[JSONObject],
|
|
3222
|
+
) -> JSONObject | None:
|
|
3223
|
+
for index, item in enumerate(apps or []):
|
|
3224
|
+
if not isinstance(item, dict):
|
|
3225
|
+
continue
|
|
3226
|
+
failure = _reserved_system_field_name_failure(
|
|
3227
|
+
tool_name=tool_name,
|
|
3228
|
+
profile=profile,
|
|
3229
|
+
app_key=str(item.get("app_key") or item.get("appKey") or ""),
|
|
3230
|
+
package_id=package_id,
|
|
3231
|
+
app_name=str(item.get("app_name") or item.get("appName") or ""),
|
|
3232
|
+
icon=str(item.get("icon") or ""),
|
|
3233
|
+
color=str(item.get("color") or ""),
|
|
3234
|
+
visibility=item.get("visibility") if isinstance(item.get("visibility"), dict) else visibility,
|
|
3235
|
+
create_if_missing=create_if_missing,
|
|
3236
|
+
publish=publish,
|
|
3237
|
+
add_fields=list(item.get("add_fields") or item.get("addFields") or []),
|
|
3238
|
+
update_fields=list(item.get("update_fields") or item.get("updateFields") or []),
|
|
3239
|
+
remove_fields=list(item.get("remove_fields") or item.get("removeFields") or []),
|
|
3240
|
+
app_index=index,
|
|
3241
|
+
)
|
|
3242
|
+
if failure is not None:
|
|
3243
|
+
suggested = deepcopy(failure.get("suggested_next_call") or {})
|
|
3244
|
+
arguments = suggested.get("arguments")
|
|
3245
|
+
if isinstance(arguments, dict):
|
|
3246
|
+
fixed_apps = deepcopy(apps)
|
|
3247
|
+
if isinstance(fixed_apps[index], dict):
|
|
3248
|
+
fixed_apps[index]["add_fields"] = arguments.get("add_fields") or []
|
|
3249
|
+
arguments = {
|
|
3250
|
+
"profile": profile,
|
|
3251
|
+
"package_id": package_id,
|
|
3252
|
+
"visibility": visibility,
|
|
3253
|
+
"create_if_missing": create_if_missing,
|
|
3254
|
+
"publish": publish,
|
|
3255
|
+
"apps": fixed_apps,
|
|
3256
|
+
}
|
|
3257
|
+
failure["suggested_next_call"] = {"tool_name": tool_name, "arguments": arguments}
|
|
3258
|
+
return failure
|
|
3259
|
+
return None
|
|
3260
|
+
|
|
3261
|
+
|
|
3262
|
+
_RESERVED_SYSTEM_VIEW_NAMES = {
|
|
3263
|
+
"全部数据",
|
|
3264
|
+
"我的数据",
|
|
3265
|
+
"我发起的",
|
|
3266
|
+
"待办",
|
|
3267
|
+
"已办",
|
|
3268
|
+
"抄送",
|
|
3269
|
+
}
|
|
3270
|
+
|
|
3271
|
+
|
|
3272
|
+
def _reserved_system_view_name_failure(
|
|
3273
|
+
*,
|
|
3274
|
+
tool_name: str,
|
|
3275
|
+
profile: str,
|
|
3276
|
+
app_key: str,
|
|
3277
|
+
publish: bool | None = None,
|
|
3278
|
+
upsert_views: list[JSONObject] | None = None,
|
|
3279
|
+
patch_views: list[JSONObject] | None = None,
|
|
3280
|
+
remove_views: list[str] | None = None,
|
|
3281
|
+
) -> JSONObject | None:
|
|
3282
|
+
for index, item in enumerate(upsert_views or []):
|
|
3283
|
+
if not isinstance(item, dict):
|
|
3284
|
+
continue
|
|
3285
|
+
name = str(item.get("name") or "").strip()
|
|
3286
|
+
view_key = str(item.get("view_key") or item.get("viewKey") or "").strip()
|
|
3287
|
+
if name in _RESERVED_SYSTEM_VIEW_NAMES and not view_key:
|
|
3288
|
+
suggested: JSONObject = {
|
|
3289
|
+
"tool_name": tool_name,
|
|
3290
|
+
"arguments": {
|
|
3291
|
+
"profile": profile,
|
|
3292
|
+
"app_key": app_key,
|
|
3293
|
+
"upsert_views": [
|
|
3294
|
+
{
|
|
3295
|
+
**item,
|
|
3296
|
+
"name": "业务台账视图",
|
|
3297
|
+
}
|
|
3298
|
+
],
|
|
3299
|
+
"patch_views": patch_views or [],
|
|
3300
|
+
"remove_views": remove_views or [],
|
|
3301
|
+
},
|
|
3302
|
+
}
|
|
3303
|
+
if publish is not None:
|
|
3304
|
+
suggested["arguments"]["publish"] = publish
|
|
3305
|
+
return _config_failure(
|
|
3306
|
+
tool_name=tool_name,
|
|
3307
|
+
error_code="RESERVED_SYSTEM_VIEW_NAME",
|
|
3308
|
+
message=f"upsert_views[{index}].name uses built-in system view name '{name}' without view_key",
|
|
3309
|
+
fix_hint="Do not create business views named 全部数据, 我的数据, 我发起的, 待办, 已办, or 抄送. Use a business-specific view name for new views; to modify a built-in view, pass its existing raw view_key or use patch_views.",
|
|
3310
|
+
details={
|
|
3311
|
+
"reserved_view_names": sorted(_RESERVED_SYSTEM_VIEW_NAMES),
|
|
3312
|
+
"blocked_index": index,
|
|
3313
|
+
"blocked_name": name,
|
|
3314
|
+
},
|
|
3315
|
+
suggested_next_call=suggested,
|
|
3316
|
+
)
|
|
3317
|
+
return None
|
|
3318
|
+
|
|
3319
|
+
|
|
2971
3320
|
def _config_failure(
|
|
2972
3321
|
*,
|
|
2973
3322
|
tool_name: str,
|
|
@@ -2976,6 +3325,7 @@ def _config_failure(
|
|
|
2976
3325
|
error_code: str = "CONFIG_ERROR",
|
|
2977
3326
|
details: JSONObject | None = None,
|
|
2978
3327
|
allowed_values: JSONObject | None = None,
|
|
3328
|
+
suggested_next_call: JSONObject | None = None,
|
|
2979
3329
|
) -> JSONObject:
|
|
2980
3330
|
contract = _BUILDER_TOOL_CONTRACTS.get(tool_name or "")
|
|
2981
3331
|
public_allowed_values = deepcopy(contract.get("allowed_values", {})) if isinstance(contract, dict) else {}
|
|
@@ -2996,7 +3346,7 @@ def _config_failure(
|
|
|
2996
3346
|
"missing_fields": [],
|
|
2997
3347
|
"allowed_values": public_allowed_values,
|
|
2998
3348
|
"details": public_details,
|
|
2999
|
-
"suggested_next_call":
|
|
3349
|
+
"suggested_next_call": suggested_next_call,
|
|
3000
3350
|
"request_id": None,
|
|
3001
3351
|
"backend_code": None,
|
|
3002
3352
|
"http_status": None,
|
|
@@ -4054,13 +4404,29 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4054
4404
|
},
|
|
4055
4405
|
},
|
|
4056
4406
|
"package_apply": {
|
|
4057
|
-
"allowed_keys": [
|
|
4407
|
+
"allowed_keys": [
|
|
4408
|
+
"package_id",
|
|
4409
|
+
"package_name",
|
|
4410
|
+
"create_if_missing",
|
|
4411
|
+
"icon",
|
|
4412
|
+
"color",
|
|
4413
|
+
"icon_name",
|
|
4414
|
+
"icon_color",
|
|
4415
|
+
"icon_config",
|
|
4416
|
+
"visibility",
|
|
4417
|
+
"items",
|
|
4418
|
+
"allow_detach",
|
|
4419
|
+
],
|
|
4058
4420
|
"aliases": {
|
|
4059
4421
|
"packageId": "package_id",
|
|
4060
4422
|
"packageName": "package_name",
|
|
4061
4423
|
"createIfMissing": "create_if_missing",
|
|
4062
4424
|
"iconName": "icon",
|
|
4063
4425
|
"iconColor": "color",
|
|
4426
|
+
"icon_config.name": "icon",
|
|
4427
|
+
"icon_config.icon_name": "icon",
|
|
4428
|
+
"icon_config.color": "color",
|
|
4429
|
+
"icon_config.icon_color": "color",
|
|
4064
4430
|
"allowDetach": "allow_detach",
|
|
4065
4431
|
},
|
|
4066
4432
|
"allowed_values": {
|
|
@@ -4072,6 +4438,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4072
4438
|
"execution_notes": [
|
|
4073
4439
|
"create or update package metadata, visibility, grouping, and ordering in one call",
|
|
4074
4440
|
"creating a package requires explicit icon + color; icon=template is blocked because it is too generic",
|
|
4441
|
+
"icon can be passed as icon/color, icon_name/icon_color, icon_config, or icon={name,color}; all forms normalize to icon/color",
|
|
4075
4442
|
"updating a package preserves existing icon/color when omitted; explicit icon/color values are still validated",
|
|
4076
4443
|
"call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
|
|
4077
4444
|
"metadata keys omitted on update are preserved",
|
|
@@ -4536,6 +4903,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4536
4903
|
"create mode may set visibility for the new app; edit mode may update visibility on an existing app",
|
|
4537
4904
|
"create mode should include explicit non-template icon + color; apply mode enforces this before writing",
|
|
4538
4905
|
"call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
|
|
4906
|
+
"do not add form fields named 数据ID, 编号, 申请人, 申请时间, 创建人, 创建时间, 提交人, 提交时间, 更新时间, 更新人, 当前流程状态, 当前处理人, 当前处理节点, or 流程标题; these are Qingflow built-in system fields, not fields to create",
|
|
4539
4907
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
4540
4908
|
],
|
|
4541
4909
|
"minimal_example": {
|
|
@@ -4575,6 +4943,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4575
4943
|
"app_title",
|
|
4576
4944
|
"icon",
|
|
4577
4945
|
"color",
|
|
4946
|
+
"icon_name",
|
|
4947
|
+
"icon_color",
|
|
4948
|
+
"icon_config",
|
|
4578
4949
|
"visibility",
|
|
4579
4950
|
"create_if_missing",
|
|
4580
4951
|
"publish",
|
|
@@ -4587,11 +4958,15 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4587
4958
|
"apps[].app_name",
|
|
4588
4959
|
"apps[].icon",
|
|
4589
4960
|
"apps[].color",
|
|
4961
|
+
"apps[].icon_name",
|
|
4962
|
+
"apps[].icon_color",
|
|
4963
|
+
"apps[].icon_config",
|
|
4590
4964
|
"apps[].visibility",
|
|
4591
4965
|
"apps[].add_fields",
|
|
4592
4966
|
"apps[].update_fields",
|
|
4593
4967
|
"apps[].remove_fields",
|
|
4594
4968
|
"apps[].add_fields[].target_app_ref",
|
|
4969
|
+
"apps[].add_fields[].target_app",
|
|
4595
4970
|
],
|
|
4596
4971
|
"aliases": {
|
|
4597
4972
|
"app_title": "app_name",
|
|
@@ -4601,8 +4976,16 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4601
4976
|
"apps[].appKey": "apps[].app_key",
|
|
4602
4977
|
"apps[].appName": "apps[].app_name",
|
|
4603
4978
|
"apps[].appTitle": "apps[].app_name",
|
|
4979
|
+
"apps[].iconName": "apps[].icon",
|
|
4980
|
+
"apps[].iconColor": "apps[].color",
|
|
4981
|
+
"apps[].icon_config.name": "apps[].icon",
|
|
4982
|
+
"apps[].icon_config.color": "apps[].color",
|
|
4604
4983
|
"field.targetAppRef": "field.target_app_ref",
|
|
4605
4984
|
"field.targetAppClientKey": "field.target_app_ref",
|
|
4985
|
+
"field.targetApp": "field.target_app",
|
|
4986
|
+
"field.options[].label": "field.options[]",
|
|
4987
|
+
"field.options[].value": "field.options[]",
|
|
4988
|
+
"field.options[].optValue": "field.options[]",
|
|
4606
4989
|
"field.title": "field.name",
|
|
4607
4990
|
"field.label": "field.name",
|
|
4608
4991
|
"field.fields": "field.subfields",
|
|
@@ -4642,12 +5025,16 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4642
5025
|
"create mode follows backend CreateAppBean: package add_app permission is checked on the target package; package edit_app is not required for the create precheck",
|
|
4643
5026
|
"multi-app mode: pass package_id + create_if_missing + apps[]; do not mix apps with top-level app_key/app_name/add_fields/update_fields/remove_fields",
|
|
4644
5027
|
"multi-app relation fields may use target_app_ref to point at another apps[].client_key; the tool creates/resolves app shells first and compiles it to target_app_key",
|
|
5028
|
+
"multi-app relation fields may also use target_app with another apps[].app_name; prefer target_app_ref/client_key when names may collide",
|
|
4645
5029
|
"multi-app mode is not transactional; read created_app_keys and apps[].status before retrying, and retry only failed app items",
|
|
4646
5030
|
"create mode defaults new app visibility to workspace/not when visibility is omitted; edit mode preserves current visibility when omitted",
|
|
4647
5031
|
"create mode requires explicit icon + color; icon=template is blocked because it is too generic",
|
|
5032
|
+
"icon can be passed as icon/color, icon_name/icon_color, icon_config, or icon={name,color}; all forms normalize to icon/color",
|
|
4648
5033
|
"multi-app create mode requires each new app item to include a distinct non-template icon and a valid color",
|
|
5034
|
+
"single_select and multi_select options accept strings or objects such as {label,value}; builder normalizes them to option labels before writing",
|
|
4649
5035
|
"edit mode preserves existing icon/color when omitted; explicit icon/color values are still validated",
|
|
4650
5036
|
"call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
|
|
5037
|
+
"do not add form fields named 数据ID, 编号, 申请人, 申请时间, 创建人, 创建时间, 提交人, 提交时间, 更新时间, 更新人, 当前流程状态, 当前处理人, 当前处理节点, or 流程标题; these are Qingflow built-in system fields, not fields to create",
|
|
4651
5038
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
4652
5039
|
"update_fields is the field-level partial update path; it reads current form schema and preserves untouched field config",
|
|
4653
5040
|
"multiple relation fields are backend-risky; read verification.relation_field_limit_verified and warnings before declaring the schema stable",
|
|
@@ -5047,6 +5434,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5047
5434
|
"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",
|
|
5048
5435
|
"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",
|
|
5049
5436
|
"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",
|
|
5437
|
+
"do not create business views named 全部数据, 我的数据, 我发起的, 待办, 已办, or 抄送; these are built-in system/default views. Use business-specific names for new views, and pass the existing raw view_key or patch_views when changing a built-in view",
|
|
5050
5438
|
"new views created by app_views_apply default associated report/view display to visible with limit_type=all; existing views preserve their current associated display unless patch_views/upsert_views explicitly changes associated_resources",
|
|
5051
5439
|
"associated report/view resource pool and per-view selected resources are configured through app_associated_resources_apply; app_views_apply only keeps legacy associated_resources input compatible",
|
|
5052
5440
|
"for multi-value operators such as in, pass values as a list; value may also be used as an alias when it already contains a list",
|
|
@@ -5055,7 +5443,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5055
5443
|
"minimal_example": {
|
|
5056
5444
|
"profile": "default",
|
|
5057
5445
|
"app_key": "APP_KEY",
|
|
5058
|
-
"upsert_views": [{"name": "
|
|
5446
|
+
"upsert_views": [{"name": "项目台账视图", "type": "table", "columns": ["项目名称"], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
|
|
5059
5447
|
"remove_views": [],
|
|
5060
5448
|
},
|
|
5061
5449
|
"query_conditions_example": {
|
|
@@ -5176,6 +5564,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5176
5564
|
"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",
|
|
5177
5565
|
"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",
|
|
5178
5566
|
"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",
|
|
5567
|
+
"do not create business views named 全部数据, 我的数据, 我发起的, 待办, 已办, or 抄送; these are built-in system/default views. Use business-specific names for new views, and pass the existing raw view_key or patch_views when changing a built-in view",
|
|
5179
5568
|
"new views created by app_views_apply default associated report/view display to visible with limit_type=all; existing views preserve their current associated display unless patch_views/upsert_views explicitly changes associated_resources",
|
|
5180
5569
|
"associated report/view resource pool and per-view selected resources are configured through app_associated_resources_apply; app_views_apply keeps legacy associated_resources input compatible but it is no longer the recommended public contract",
|
|
5181
5570
|
"for multi-value operators such as in, pass values as a list; value may also be used as an alias when it already contains a list",
|
|
@@ -5185,7 +5574,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5185
5574
|
"profile": "default",
|
|
5186
5575
|
"app_key": "APP_KEY",
|
|
5187
5576
|
"publish": True,
|
|
5188
|
-
"upsert_views": [{"name": "
|
|
5577
|
+
"upsert_views": [{"name": "项目台账视图", "type": "table", "columns": ["项目名称"], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
|
|
5189
5578
|
"remove_views": [],
|
|
5190
5579
|
},
|
|
5191
5580
|
"query_conditions_example": {
|
|
@@ -75,6 +75,22 @@ MAX_SUMMARY_PREVIEW_COLUMN_LIMIT = 6
|
|
|
75
75
|
VERIFY_TASK_FALLBACK_PAGE_SIZE = 50
|
|
76
76
|
VERIFY_TASK_FALLBACK_MAX_PAGES = 3
|
|
77
77
|
BACKEND_LIST_SEARCH_FIELD_LIMIT = 10
|
|
78
|
+
RECORD_WRITE_SYSTEM_FIELD_NAMES = {
|
|
79
|
+
"数据ID",
|
|
80
|
+
"编号",
|
|
81
|
+
"申请人",
|
|
82
|
+
"申请时间",
|
|
83
|
+
"创建人",
|
|
84
|
+
"创建时间",
|
|
85
|
+
"提交人",
|
|
86
|
+
"提交时间",
|
|
87
|
+
"更新时间",
|
|
88
|
+
"更新人",
|
|
89
|
+
"当前流程状态",
|
|
90
|
+
"当前处理人",
|
|
91
|
+
"当前处理节点",
|
|
92
|
+
"流程标题",
|
|
93
|
+
}
|
|
78
94
|
LOOKUP_RESOLUTION_MIN_SCORE = 0.92
|
|
79
95
|
LOOKUP_RESOLUTION_MIN_MARGIN = 0.08
|
|
80
96
|
LOOKUP_CONFIRMATION_CANDIDATE_LIMIT = 5
|
|
@@ -3649,6 +3665,7 @@ class RecordTools(ToolBase):
|
|
|
3649
3665
|
raise_tool_error(QingflowApiError.config_error("app_key is required"))
|
|
3650
3666
|
if items is not None:
|
|
3651
3667
|
normalized_items = self._normalize_public_record_insert_batch_items(fields=fields, items=items)
|
|
3668
|
+
self._reject_record_insert_system_fields([cast(JSONObject, item["fields"]) for item in normalized_items])
|
|
3652
3669
|
return self._record_insert_public_batch(
|
|
3653
3670
|
profile=profile,
|
|
3654
3671
|
app_key=app_key,
|
|
@@ -3658,15 +3675,35 @@ class RecordTools(ToolBase):
|
|
|
3658
3675
|
)
|
|
3659
3676
|
if fields is not None and not isinstance(fields, dict):
|
|
3660
3677
|
raise_tool_error(QingflowApiError.config_error("fields must be an object map keyed by field title"))
|
|
3678
|
+
normalized_fields = cast(JSONObject, fields or {})
|
|
3679
|
+
self._reject_record_insert_system_fields([normalized_fields])
|
|
3661
3680
|
return self._record_insert_public_single(
|
|
3662
3681
|
profile=profile,
|
|
3663
3682
|
app_key=app_key,
|
|
3664
|
-
fields=
|
|
3683
|
+
fields=normalized_fields,
|
|
3665
3684
|
verify_write=verify_write,
|
|
3666
3685
|
output_profile=normalized_output_profile,
|
|
3667
3686
|
capture_exceptions=False,
|
|
3668
3687
|
)
|
|
3669
3688
|
|
|
3689
|
+
def _reject_record_insert_system_fields(self, field_maps: list[JSONObject]) -> None:
|
|
3690
|
+
for row_index, field_map in enumerate(field_maps):
|
|
3691
|
+
for field_name in field_map:
|
|
3692
|
+
normalized_name = str(field_name or "").strip()
|
|
3693
|
+
if normalized_name in RECORD_WRITE_SYSTEM_FIELD_NAMES:
|
|
3694
|
+
raise_tool_error(
|
|
3695
|
+
QingflowApiError.config_error(
|
|
3696
|
+
f"record_insert fields must not include built-in system field '{normalized_name}'",
|
|
3697
|
+
details={
|
|
3698
|
+
"error_code": "RESERVED_SYSTEM_FIELD_NAME",
|
|
3699
|
+
"row_number": row_index + 1,
|
|
3700
|
+
"field_name": normalized_name,
|
|
3701
|
+
"reserved_field_names": sorted(RECORD_WRITE_SYSTEM_FIELD_NAMES),
|
|
3702
|
+
"fix_hint": "Remove Qingflow built-in system fields from record_insert payload. They are generated by the platform and can be read after creation, not manually inserted.",
|
|
3703
|
+
},
|
|
3704
|
+
)
|
|
3705
|
+
)
|
|
3706
|
+
|
|
3670
3707
|
def _record_insert_public_single(
|
|
3671
3708
|
self,
|
|
3672
3709
|
*,
|