@qingflow-tech/qingflow-app-user-mcp 1.0.34 → 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 CHANGED
@@ -3,13 +3,13 @@
3
3
  Install:
4
4
 
5
5
  ```bash
6
- npm install @qingflow-tech/qingflow-app-user-mcp@1.0.34
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.34 qingflow-app-user-mcp
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qingflow-tech/qingflow-app-user-mcp",
3
- "version": "1.0.34",
3
+ "version": "1.0.35",
4
4
  "description": "Operational end-user MCP for Qingflow records, tasks, comments, and directory workflows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qingflow-mcp"
7
- version = "1.0.34"
7
+ version = "1.0.35"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -47,6 +47,7 @@ Builder schema inputs should follow agent-intuitive semantics:
47
47
  - icons may be `icon/color`, `icon_name/icon_color`, `icon_config`, or `icon: {name, color}`
48
48
  - `single_select` / `multi_select` options may be strings or objects such as `{label, value}`; tools normalize to option labels
49
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"`
50
51
 
51
52
  ## Public Tools You Should Think In
52
53
 
@@ -84,6 +85,7 @@ Treat these as the official surface. Do not default to `package_create`, `packag
84
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"`
85
86
  - it does not attach the report to the Qingflow app associated-resource display; use `app_associated_resources_apply` for that
86
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
87
89
  - use `patch_charts` for changing a chart name, visibility, filters, or one config fragment on an existing chart
88
90
  - `visibility` is a public capability and should be treated as a base-only permission update
89
91
  - do not model chart visibility changes as raw config rewrites
@@ -108,14 +110,15 @@ Treat these as the official surface. Do not default to `package_create`, `packag
108
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`
109
111
  - `client_key` is only a same-call reference for `view_configs[].associated_item_refs`; it is not saved and cannot deduplicate later calls
110
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"`
111
- - use `match_mappings` for associated view/report filters; dynamic conditions use `source_field`, static conditions use `value`
113
+ - use `match_mappings` for associated view/report filters; dynamic context matches use `source_field`, static filters use `value`; do not handwrite raw `matchRules`
112
114
  - if field type compatibility is unclear, read [references/match-rules.md](references/match-rules.md)
113
115
  - Views and flows:
114
116
  - stay on `app_views_apply` / `app_flow_apply`
115
117
  - use `patch_views` for existing-view parameter changes such as `query_conditions`, `filters`, `columns`, or visibility
116
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`
117
120
  - use `builder_tool_contract` whenever the minimal legal shape is unclear
118
- - 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
119
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
120
123
  - configure associated reports/views through `app_associated_resources_apply`, not through `app_views_apply`
121
124
 
@@ -161,6 +164,7 @@ Treat these as the official surface. Do not default to `package_create`, `packag
161
164
  - Do not use raw `portal_*` writes or raw `qingbi_report_*` writes as the default builder strategy.
162
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.
163
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.
164
168
  - If the same validation error repeats twice, stop guessing and re-read `builder_tool_contract`.
165
169
  - For workflow assignees, prefer `role_search` over explicit members unless the user explicitly wants named members.
166
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.
@@ -43,6 +43,8 @@ Use this pattern instead:
43
43
 
44
44
  ## Example
45
45
 
46
+ When designing fields, do not add platform system fields such as `数据ID`, `编号`, `申请人`, `申请时间`, `创建人`, `创建时间`, `提交人`, `提交时间`, `更新时间`, `更新人`, `当前流程状态`, `当前处理人`, `当前处理节点`, or `流程标题`. Qingflow provides them automatically; create only business fields.
47
+
46
48
  Create a new package only after the user confirms package creation:
47
49
 
48
50
  ```json
@@ -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
@@ -54,8 +54,8 @@ These execute normalized patches. Some app apply tools publish by default and st
54
54
  - `app_flow_apply`: replace workflow
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
- - `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.
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 `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
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.
@@ -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 canonical keys `field_name`, `operator`, `value`/`values`
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 default table view:
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
@@ -1259,11 +1259,30 @@ class CustomButtonFieldMappingPatch(StrictModel):
1259
1259
 
1260
1260
 
1261
1261
  class FieldMatchMappingPatch(StrictModel):
1262
- target_field: Any = Field(validation_alias=AliasChoices("target_field", "targetField", "target", "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() != ""
@@ -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: 0,
15053
- ViewFilterOperator.neq.value: 1,
15054
- ViewFilterOperator.gte.value: 5,
15055
- ViewFilterOperator.lte.value: 7,
15056
- ViewFilterOperator.in_.value: 9,
15057
- ViewFilterOperator.contains.value: 19,
15058
- ViewFilterOperator.is_empty.value: 15,
15059
- ViewFilterOperator.not_empty.value: 16,
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, 0),
15077
+ "judgeType": judge_map.get(operator, "equal"),
15078
15078
  "judgeValues": values,
15079
15079
  "matchType": 1,
15080
15080
  }
@@ -1481,6 +1481,22 @@ class AiBuilderTools(ToolBase):
1481
1481
  remove_fields: list[JSONObject],
1482
1482
  ) -> JSONObject:
1483
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
1484
1500
  try:
1485
1501
  request = SchemaPlanRequest.model_validate(
1486
1502
  {
@@ -1631,6 +1647,15 @@ class AiBuilderTools(ToolBase):
1631
1647
  preset: str | None = None,
1632
1648
  ) -> JSONObject:
1633
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
1634
1659
  try:
1635
1660
  request = ViewsPlanRequest.model_validate(
1636
1661
  {
@@ -1758,6 +1783,17 @@ class AiBuilderTools(ToolBase):
1758
1783
  }
1759
1784
  if visibility is not None:
1760
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
1761
1797
  if package_id is None:
1762
1798
  return _config_failure(
1763
1799
  tool_name="app_schema_apply",
@@ -2049,6 +2085,23 @@ class AiBuilderTools(ToolBase):
2049
2085
  ) -> JSONObject:
2050
2086
  """执行内部辅助逻辑。"""
2051
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
2052
2105
  icon_failure = _validate_workspace_icon_for_builder(
2053
2106
  tool_name="app_schema_apply",
2054
2107
  icon=icon,
@@ -2380,6 +2433,17 @@ class AiBuilderTools(ToolBase):
2380
2433
  remove_views: list[str],
2381
2434
  ) -> JSONObject:
2382
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
2383
2447
  if patch_views:
2384
2448
  try:
2385
2449
  parsed_views = [ViewUpsertPatch.model_validate(item) for item in (upsert_views or [])]
@@ -2454,7 +2518,7 @@ class AiBuilderTools(ToolBase):
2454
2518
  "profile": profile,
2455
2519
  "app_key": str(plan_args.get("app_key") or app_key),
2456
2520
  "publish": publish,
2457
- "upsert_views": plan_args.get("upsert_views") or [{"name": "全部数据", "type": "table", "columns": ["字段A"]}],
2521
+ "upsert_views": plan_args.get("upsert_views") or [{"name": "业务台账视图", "type": "table", "columns": ["字段A"]}],
2458
2522
  "remove_views": plan_args.get("remove_views") or [],
2459
2523
  },
2460
2524
  },
@@ -3062,6 +3126,197 @@ def _visibility_validation_failure(
3062
3126
  return result
3063
3127
 
3064
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
+
3065
3320
  def _config_failure(
3066
3321
  *,
3067
3322
  tool_name: str,
@@ -3070,6 +3325,7 @@ def _config_failure(
3070
3325
  error_code: str = "CONFIG_ERROR",
3071
3326
  details: JSONObject | None = None,
3072
3327
  allowed_values: JSONObject | None = None,
3328
+ suggested_next_call: JSONObject | None = None,
3073
3329
  ) -> JSONObject:
3074
3330
  contract = _BUILDER_TOOL_CONTRACTS.get(tool_name or "")
3075
3331
  public_allowed_values = deepcopy(contract.get("allowed_values", {})) if isinstance(contract, dict) else {}
@@ -3090,7 +3346,7 @@ def _config_failure(
3090
3346
  "missing_fields": [],
3091
3347
  "allowed_values": public_allowed_values,
3092
3348
  "details": public_details,
3093
- "suggested_next_call": None,
3349
+ "suggested_next_call": suggested_next_call,
3094
3350
  "request_id": None,
3095
3351
  "backend_code": None,
3096
3352
  "http_status": None,
@@ -4647,6 +4903,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4647
4903
  "create mode may set visibility for the new app; edit mode may update visibility on an existing app",
4648
4904
  "create mode should include explicit non-template icon + color; apply mode enforces this before writing",
4649
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",
4650
4907
  *_VISIBILITY_EXECUTION_NOTES,
4651
4908
  ],
4652
4909
  "minimal_example": {
@@ -4777,6 +5034,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4777
5034
  "single_select and multi_select options accept strings or objects such as {label,value}; builder normalizes them to option labels before writing",
4778
5035
  "edit mode preserves existing icon/color when omitted; explicit icon/color values are still validated",
4779
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",
4780
5038
  *_VISIBILITY_EXECUTION_NOTES,
4781
5039
  "update_fields is the field-level partial update path; it reads current form schema and preserves untouched field config",
4782
5040
  "multiple relation fields are backend-risky; read verification.relation_field_limit_verified and warnings before declaring the schema stable",
@@ -5176,6 +5434,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5176
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",
5177
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",
5178
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",
5179
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",
5180
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",
5181
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",
@@ -5184,7 +5443,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5184
5443
  "minimal_example": {
5185
5444
  "profile": "default",
5186
5445
  "app_key": "APP_KEY",
5187
- "upsert_views": [{"name": "全部数据", "type": "table", "columns": ["项目名称"], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
5446
+ "upsert_views": [{"name": "项目台账视图", "type": "table", "columns": ["项目名称"], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
5188
5447
  "remove_views": [],
5189
5448
  },
5190
5449
  "query_conditions_example": {
@@ -5305,6 +5564,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5305
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",
5306
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",
5307
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",
5308
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",
5309
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",
5310
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",
@@ -5314,7 +5574,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5314
5574
  "profile": "default",
5315
5575
  "app_key": "APP_KEY",
5316
5576
  "publish": True,
5317
- "upsert_views": [{"name": "全部数据", "type": "table", "columns": ["项目名称"], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
5577
+ "upsert_views": [{"name": "项目台账视图", "type": "table", "columns": ["项目名称"], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
5318
5578
  "remove_views": [],
5319
5579
  },
5320
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=cast(JSONObject, fields or {}),
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
  *,