@josephyan/qingflow-app-user-mcp 1.1.17 → 1.1.18

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 @josephyan/qingflow-app-user-mcp@1.1.17
6
+ npm install @josephyan/qingflow-app-user-mcp@1.1.18
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-app-user-mcp@1.1.17 qingflow-app-user-mcp
12
+ npx -y -p @josephyan/qingflow-app-user-mcp@1.1.18 qingflow-app-user-mcp
13
13
  ```
14
14
 
15
15
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-app-user-mcp",
3
- "version": "1.1.17",
3
+ "version": "1.1.18",
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.1.17"
7
+ version = "1.1.18"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -2260,6 +2260,22 @@ class ChartApplyRequest(StrictModel):
2260
2260
  return self
2261
2261
 
2262
2262
 
2263
+ class PortalInlineChartPatch(ChartUpsertPatch):
2264
+ app_key: str = Field(validation_alias=AliasChoices("app_key", "appKey"))
2265
+
2266
+ @model_validator(mode="before")
2267
+ @classmethod
2268
+ def normalize_inline_aliases(cls, value: Any) -> Any:
2269
+ if not isinstance(value, dict):
2270
+ return value
2271
+ payload = dict(value)
2272
+ if "chart_name" in payload and "name" not in payload:
2273
+ payload["name"] = payload.pop("chart_name")
2274
+ if "chartName" in payload and "name" not in payload:
2275
+ payload["name"] = payload.pop("chartName")
2276
+ return payload
2277
+
2278
+
2263
2279
  class PortalComponentPositionPatch(StrictModel):
2264
2280
  pc_x: int = Field(default=0, validation_alias=AliasChoices("pc_x", "pcX", "x"))
2265
2281
  pc_y: int = Field(default=0, validation_alias=AliasChoices("pc_y", "pcY", "y"))
@@ -2344,6 +2360,7 @@ class PortalSectionPatch(StrictModel):
2344
2360
  dash_style_config: dict[str, Any] | None = Field(default=None, validation_alias=AliasChoices("dash_style_config", "dashStyleConfigBO"))
2345
2361
  config: dict[str, Any] = Field(default_factory=dict)
2346
2362
  chart_ref: PortalChartRefPatch | None = None
2363
+ chart: PortalInlineChartPatch | None = Field(default=None, validation_alias=AliasChoices("chart", "inline_chart", "inlineChart", "create_chart", "createChart"))
2347
2364
  view_ref: PortalViewRefPatch | None = None
2348
2365
  text: str | None = None
2349
2366
  url: str | None = None
@@ -2362,6 +2379,14 @@ class PortalSectionPatch(StrictModel):
2362
2379
  payload["role"] = raw_role.strip().lower()
2363
2380
  if "chartRef" in payload and "chart_ref" not in payload:
2364
2381
  payload["chart_ref"] = payload.pop("chartRef")
2382
+ if "inlineChart" in payload and "chart" not in payload:
2383
+ payload["chart"] = payload.pop("inlineChart")
2384
+ if "inline_chart" in payload and "chart" not in payload:
2385
+ payload["chart"] = payload.pop("inline_chart")
2386
+ if "createChart" in payload and "chart" not in payload:
2387
+ payload["chart"] = payload.pop("createChart")
2388
+ if "create_chart" in payload and "chart" not in payload:
2389
+ payload["chart"] = payload.pop("create_chart")
2365
2390
  if "viewRef" in payload and "view_ref" not in payload:
2366
2391
  payload["view_ref"] = payload.pop("viewRef")
2367
2392
  if "dashStyleConfigBO" in payload and "dash_style_config" not in payload:
@@ -2373,8 +2398,8 @@ class PortalSectionPatch(StrictModel):
2373
2398
  supported = {"chart", "view", "grid", "filter", "text", "link"}
2374
2399
  if self.source_type not in supported:
2375
2400
  raise ValueError(f"unsupported portal source_type '{self.source_type}'")
2376
- if self.source_type == "chart" and self.chart_ref is None:
2377
- raise ValueError("chart section requires chart_ref")
2401
+ if self.source_type == "chart" and self.chart_ref is None and self.chart is None:
2402
+ raise ValueError("chart section requires chart_ref or chart")
2378
2403
  if self.source_type == "view" and self.view_ref is None:
2379
2404
  raise ValueError("view section requires view_ref")
2380
2405
  if self.source_type == "text" and self.text is None:
@@ -2662,6 +2687,14 @@ def _normalize_field_payload(value: Any) -> Any:
2662
2687
  if not isinstance(value, dict):
2663
2688
  return value
2664
2689
  payload = dict(value)
2690
+ if "data_title" in payload and "as_data_title" not in payload and "asDataTitle" not in payload:
2691
+ payload["as_data_title"] = payload.pop("data_title")
2692
+ if "dataTitle" in payload and "as_data_title" not in payload and "asDataTitle" not in payload:
2693
+ payload["as_data_title"] = payload.pop("dataTitle")
2694
+ if "data_cover" in payload and "as_data_cover" not in payload and "asDataCover" not in payload:
2695
+ payload["as_data_cover"] = payload.pop("data_cover")
2696
+ if "dataCover" in payload and "as_data_cover" not in payload and "asDataCover" not in payload:
2697
+ payload["as_data_cover"] = payload.pop("dataCover")
2665
2698
  if "fields" in payload and "subfields" not in payload:
2666
2699
  payload["subfields"] = payload.pop("fields")
2667
2700
  if "options" in payload:
@@ -13018,6 +13018,7 @@ class AiBuilderFacade:
13018
13018
  draft_readback_error: JSONObject | None = None
13019
13019
  live_readback_error: JSONObject | None = None
13020
13020
  update_payload: dict[str, Any] = {}
13021
+ inline_chart_results: list[dict[str, Any]] = []
13021
13022
  try:
13022
13023
  layout_diagnostics: dict[str, Any] = _empty_portal_layout_diagnostics()
13023
13024
  if creating:
@@ -13065,10 +13066,11 @@ class AiBuilderFacade:
13065
13066
  base_payload=base_payload,
13066
13067
  )
13067
13068
  if sections_requested:
13068
- component_payload, layout_metadata = self._build_portal_components_from_sections(
13069
+ component_payload, layout_metadata, inline_chart_results = self._build_portal_components_from_sections(
13069
13070
  profile=profile,
13070
13071
  sections=request.sections,
13071
13072
  layout_preset=request.layout_preset,
13073
+ inline_chart_results=inline_chart_results,
13072
13074
  )
13073
13075
  layout_diagnostics = _portal_layout_diagnostics(request.sections, component_payload, layout_metadata=layout_metadata)
13074
13076
  update_payload["components"] = component_payload
@@ -13096,11 +13098,12 @@ class AiBuilderFacade:
13096
13098
  draft_result = {}
13097
13099
  except (QingflowApiError, RuntimeError, ValueError) as error:
13098
13100
  api_error = _coerce_api_error(error) if not isinstance(error, ValueError) else None
13099
- if write_executed:
13101
+ inline_chart_write_executed = any(bool(item.get("write_executed")) for item in inline_chart_results)
13102
+ if write_executed or inline_chart_write_executed:
13100
13103
  transport_error = _transport_error_payload(api_error) if api_error is not None else None
13101
13104
  warning = _warning(
13102
13105
  "PORTAL_WRITE_INCOMPLETE_AFTER_PARTIAL_WRITE",
13103
- "one or more portal write steps executed before a later write step failed",
13106
+ "one or more portal or inline-chart write steps executed before a later write step failed",
13104
13107
  )
13105
13108
  if transport_error is not None:
13106
13109
  for key in ("backend_code", "http_status", "request_id"):
@@ -13121,6 +13124,7 @@ class AiBuilderFacade:
13121
13124
  if api_error is not None
13122
13125
  else {"message": str(error)}
13123
13126
  ),
13127
+ **({"inline_chart_results": deepcopy(inline_chart_results)} if inline_chart_results else {}),
13124
13128
  },
13125
13129
  "request_id": api_error.request_id if api_error else None,
13126
13130
  "backend_code": api_error.backend_code if api_error else None,
@@ -13136,6 +13140,7 @@ class AiBuilderFacade:
13136
13140
  "published": False,
13137
13141
  "publish_failed": False,
13138
13142
  "write_incomplete": True,
13143
+ "inline_charts_verified": False if inline_chart_results else None,
13139
13144
  "readback_unavailable": False,
13140
13145
  "metadata_unverified": True,
13141
13146
  },
@@ -13147,6 +13152,7 @@ class AiBuilderFacade:
13147
13152
  "verified": False,
13148
13153
  "write_executed": True,
13149
13154
  "safe_to_retry": False,
13155
+ "inline_chart_results": deepcopy(inline_chart_results),
13150
13156
  })
13151
13157
  return _failed(
13152
13158
  "PORTAL_APPLY_FAILED",
@@ -13258,6 +13264,7 @@ class AiBuilderFacade:
13258
13264
  "draft": draft_meta_mismatches,
13259
13265
  "live": live_meta_mismatches,
13260
13266
  },
13267
+ **({"inline_chart_results": deepcopy(inline_chart_results)} if inline_chart_results else {}),
13261
13268
  **({"publish_error": publish_error} if publish_error is not None else {}),
13262
13269
  **({"draft_readback_error": draft_readback_error} if draft_readback_error is not None else {}),
13263
13270
  **({"live_readback_error": live_readback_error} if live_readback_error is not None else {}),
@@ -13295,6 +13302,7 @@ class AiBuilderFacade:
13295
13302
  "noop": False,
13296
13303
  "warnings": warnings,
13297
13304
  "layout_diagnostics": layout_diagnostics,
13305
+ "inline_chart_results": deepcopy(inline_chart_results),
13298
13306
  "verification": {
13299
13307
  "draft_verified": draft_verified,
13300
13308
  "draft_metadata_verified": draft_meta_verified,
@@ -13302,6 +13310,7 @@ class AiBuilderFacade:
13302
13310
  "live_metadata_verified": live_meta_verified,
13303
13311
  "published": published,
13304
13312
  "publish_failed": publish_failed,
13313
+ "inline_charts_verified": all(str(item.get("status") or "") in {"created", "updated"} and str(item.get("chart_id") or "").strip() for item in inline_chart_results),
13305
13314
  "readback_unavailable": draft_readback_error is not None or live_readback_error is not None,
13306
13315
  "metadata_unverified": draft_readback_error is not None or live_readback_error is not None,
13307
13316
  },
@@ -14055,9 +14064,11 @@ class AiBuilderFacade:
14055
14064
  profile: str,
14056
14065
  sections: list[PortalSectionPatch],
14057
14066
  layout_preset: str | None = None,
14058
- ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
14067
+ inline_chart_results: list[dict[str, Any]] | None = None,
14068
+ ) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]:
14059
14069
  resolved_components: list[dict[str, Any]] = []
14060
14070
  layout_metadata: list[dict[str, Any]] = []
14071
+ inline_chart_results = inline_chart_results if inline_chart_results is not None else []
14061
14072
  pc_x = 0
14062
14073
  pc_y = 0
14063
14074
  pc_row_height = 0
@@ -14084,11 +14095,53 @@ class AiBuilderFacade:
14084
14095
  dash_style = deepcopy(section.dash_style_config) if isinstance(section.dash_style_config, dict) else None
14085
14096
  component: dict[str, Any]
14086
14097
  if section.source_type == "chart":
14087
- resolved_chart = _resolve_chart_reference(
14088
- charts=self.charts,
14089
- profile=profile,
14090
- ref=section.chart_ref,
14091
- )
14098
+ if section.chart is not None:
14099
+ inline_chart_payload = section.chart.model_dump(mode="json", exclude={"app_key"})
14100
+ chart_request = ChartApplyRequest(
14101
+ app_key=section.chart.app_key,
14102
+ upsert_charts=[ChartUpsertPatch.model_validate(inline_chart_payload)],
14103
+ patch_charts=[],
14104
+ remove_chart_ids=[],
14105
+ reorder_chart_ids=[],
14106
+ )
14107
+ chart_apply = self.chart_apply(profile=profile, request=chart_request)
14108
+ chart_items = chart_apply.get("chart_results") if isinstance(chart_apply.get("chart_results"), list) else []
14109
+ successful_chart = next(
14110
+ (
14111
+ item
14112
+ for item in chart_items
14113
+ if isinstance(item, dict)
14114
+ and str(item.get("status") or "") in {"created", "updated"}
14115
+ and str(item.get("chart_id") or "").strip()
14116
+ ),
14117
+ None,
14118
+ )
14119
+ inline_chart_results.append({
14120
+ "title": section.title,
14121
+ "app_key": section.chart.app_key,
14122
+ "chart_name": section.chart.name,
14123
+ "chart_type": section.chart.chart_type.value,
14124
+ "status": str(successful_chart.get("status") or "") if isinstance(successful_chart, dict) else "failed",
14125
+ "chart_id": str(successful_chart.get("chart_id") or "") if isinstance(successful_chart, dict) else "",
14126
+ "chart_apply_status": chart_apply.get("status"),
14127
+ "write_executed": bool(chart_apply.get("write_executed")),
14128
+ **({"error_code": chart_apply.get("error_code")} if chart_apply.get("error_code") else {}),
14129
+ **({"message": chart_apply.get("message")} if chart_apply.get("message") else {}),
14130
+ })
14131
+ if not isinstance(successful_chart, dict):
14132
+ raise ValueError(f"inline chart '{section.chart.name}' could not be created or updated for portal section '{section.title}'")
14133
+ resolved_chart = {
14134
+ "chart_id": str(successful_chart.get("chart_id") or "").strip(),
14135
+ "chart_name": str(successful_chart.get("name") or section.chart.name).strip(),
14136
+ "app_key": section.chart.app_key,
14137
+ "chart_type": section.chart.chart_type.value,
14138
+ }
14139
+ else:
14140
+ resolved_chart = _resolve_chart_reference(
14141
+ charts=self.charts,
14142
+ profile=profile,
14143
+ ref=section.chart_ref,
14144
+ )
14092
14145
  chart_type = str(
14093
14146
  resolved_chart.get("chart_type")
14094
14147
  or _public_chart_type_from_backend(section.config.get("chartType") or section.config.get("chart_type"))
@@ -14148,7 +14201,7 @@ class AiBuilderFacade:
14148
14201
  if dash_style is not None:
14149
14202
  component["dashStyleConfigBO"] = dash_style
14150
14203
  resolved_components.append(component)
14151
- return resolved_components, layout_metadata
14204
+ return resolved_components, layout_metadata, inline_chart_results
14152
14205
 
14153
14206
  def _resolve_current_user_identity(self, *, profile: str) -> JSONObject:
14154
14207
  session_profile = self.apps.sessions.get_profile(profile)
@@ -17794,12 +17847,12 @@ def _portal_layout_diagnostics(
17794
17847
  if source_type == "chart" and role in {"metric", "metrics", "indicator", "kpi"} and not is_metric_chart:
17795
17848
  warnings.append(_warning(
17796
17849
  "PORTAL_METRIC_SECTION_CHART_TYPE_MISMATCH",
17797
- "metric portal section must reference a target/indicator chart; create the missing metric chart before assembling the portal",
17850
+ "metric portal section must reference a target/indicator chart; use inline_chart to create the missing metric chart while assembling the portal",
17798
17851
  section_index=index,
17799
17852
  title=title,
17800
17853
  role=role,
17801
17854
  chart_type=chart_type or None,
17802
- fix_hint="Use app_charts_apply with chart_type=target and metric='count(*)' or another metric expression, then reference that chart.",
17855
+ fix_hint="Use section.inline_chart with chart_type=target or indicator and a metric expression, or change role away from metric.",
17803
17856
  ))
17804
17857
  if section is not None and section.position is not None and not bool(getattr(section.position, "mobile_provided", False)):
17805
17858
  warnings.append(_warning(
@@ -17859,7 +17912,7 @@ def _append_portal_standard_count_warnings(
17859
17912
  "standard portal metric area should contain 4-6 metric cards",
17860
17913
  actual_count=metric_count,
17861
17914
  expected_count="4-6",
17862
- fix_hint="Create missing target/indicator charts first, or keep 4 metric cards in one row with pc.cols=6, pc.rows=5.",
17915
+ fix_hint="Use inline_chart for missing target/indicator metric cards, or keep 4 metric cards in one row with pc.cols=6, pc.rows=5.",
17863
17916
  ))
17864
17917
  if (bi_count or require_complete_standard) and not 2 <= bi_count <= 3:
17865
17918
  warnings.append(_warning(
@@ -214,7 +214,8 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
214
214
  schema_apply_apply.add_argument("--visibility-file")
215
215
  schema_apply_apply.add_argument("--create-if-missing", action="store_true")
216
216
  schema_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=None)
217
- schema_apply_apply.add_argument("--apps-file", help="多应用 schema JSON 数组;每项可带 client_key/app_name/add_fields,支持 relation target_app_ref")
217
+ schema_apply_apply.add_argument("--apps-file", help="多应用 schema JSON 对象;每项可带 client_key/app_name/form,支持 relation target_app_ref")
218
+ schema_apply_apply.add_argument("--form-file", help="单应用表单结构 JSON 数组;form[].rows[][] 同时声明字段和表单分组布局")
218
219
  schema_apply_apply.add_argument("--add-fields-file", help="字段 JSON 数组;字段可用 as_data_title/as_data_cover 标记数据标题/封面")
219
220
  schema_apply_apply.add_argument("--update-fields-file", help="字段更新 JSON 数组;set 内可用 as_data_title/as_data_cover 标记数据标题/封面")
220
221
  schema_apply_apply.add_argument("--remove-fields-file")
@@ -578,10 +579,10 @@ def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
578
579
  fix_hint="Pass a JSON array, or a JSON object like {\"package_id\":1001,\"apps\":[...]} with at least one app item.",
579
580
  error_code="APPS_FILE_EMPTY",
580
581
  )
581
- 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:
582
+ if args.app_key or args.app_name or args.app_title or args.form_file or args.add_fields_file or args.update_fields_file or args.remove_fields_file:
582
583
  raise_config_error(
583
584
  "schema apply multi-app mode accepts --package-id plus --apps-file only; --create-if-missing is optional.",
584
- fix_hint="Use `--apps-file` for batch mode, or remove `--apps-file` and use the single-app arguments.",
585
+ fix_hint="Use apps[].form inside --apps-file for batch mode, or remove --apps-file and use --form-file for a single app.",
585
586
  )
586
587
  if package_id is None:
587
588
  raise_config_error(
@@ -595,6 +596,7 @@ def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
595
596
  create_if_missing=effective_create_if_missing,
596
597
  publish=effective_publish,
597
598
  apps=apps,
599
+ form=None,
598
600
  add_fields=[],
599
601
  update_fields=[],
600
602
  remove_fields=[],
@@ -631,6 +633,7 @@ def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
631
633
  visibility=load_object_arg(args.visibility_file, option_name="--visibility-file"),
632
634
  create_if_missing=bool(args.create_if_missing),
633
635
  publish=True if args.publish is None else bool(args.publish),
636
+ form=load_list_arg(args.form_file, option_name="--form-file") if args.form_file else [],
634
637
  add_fields=load_list_arg(args.add_fields_file, option_name="--add-fields-file"),
635
638
  update_fields=load_list_arg(args.update_fields_file, option_name="--update-fields-file"),
636
639
  remove_fields=load_list_arg(args.remove_fields_file, option_name="--remove-fields-file"),
@@ -428,16 +428,17 @@ def build_builder_server() -> FastMCP:
428
428
  visibility: dict | None = None,
429
429
  create_if_missing: bool | None = None,
430
430
  publish: bool = True,
431
+ form: list[dict] | None = None,
431
432
  add_fields: list[dict] | None = None,
432
433
  update_fields: list[dict] | None = None,
433
434
  remove_fields: list[dict] | None = None,
434
435
  apps: list[dict] | None = None,
435
436
  ) -> dict:
436
437
  if apps:
437
- if app_key or app_name or app_title or add_fields or update_fields or remove_fields:
438
+ if app_key or app_name or app_title or form or add_fields or update_fields or remove_fields:
438
439
  return _config_failure(
439
440
  "app_schema_apply multi-app mode accepts package_id plus apps only; create_if_missing is optional.",
440
- fix_hint="Use `apps` for batch mode, or use the single-app arguments without `apps`.",
441
+ fix_hint="Use apps[].form for batch mode, or use the single-app arguments without apps.",
441
442
  )
442
443
  if package_id is None:
443
444
  return _config_failure(
@@ -450,6 +451,7 @@ def build_builder_server() -> FastMCP:
450
451
  visibility=visibility,
451
452
  create_if_missing=True if create_if_missing is None else bool(create_if_missing),
452
453
  publish=publish,
454
+ form=None,
453
455
  add_fields=[],
454
456
  update_fields=[],
455
457
  remove_fields=[],
@@ -482,10 +484,11 @@ def build_builder_server() -> FastMCP:
482
484
  visibility=visibility,
483
485
  create_if_missing=effective_create_if_missing,
484
486
  publish=publish,
487
+ form=form or [],
485
488
  add_fields=add_fields or [],
486
489
  update_fields=update_fields or [],
487
490
  remove_fields=remove_fields or [],
488
- apps=[],
491
+ apps=None,
489
492
  )
490
493
 
491
494
  @server.tool()
@@ -577,17 +577,18 @@ class AiBuilderTools(ToolBase):
577
577
  visibility: JSONObject | None = None,
578
578
  create_if_missing: bool | None = None,
579
579
  publish: bool = True,
580
+ form: list[JSONObject] | None = None,
580
581
  add_fields: list[JSONObject] | None = None,
581
582
  update_fields: list[JSONObject] | None = None,
582
583
  remove_fields: list[JSONObject] | None = None,
583
584
  apps: list[JSONObject] | None = None,
584
585
  ) -> JSONObject:
585
586
  if apps is not None:
586
- if app_key or app_name or app_title or add_fields or update_fields or remove_fields:
587
+ if app_key or app_name or app_title or form or add_fields or update_fields or remove_fields:
587
588
  return _config_failure(
588
589
  tool_name="app_schema_apply",
589
590
  message="app_schema_apply multi-app mode accepts package_id plus apps only; create_if_missing is optional.",
590
- fix_hint="Use `apps` for batch mode, or use the single-app arguments without `apps`.",
591
+ fix_hint="Put per-app form in apps[].form when using batch mode.",
591
592
  )
592
593
  if package_id is None:
593
594
  return _config_failure(
@@ -602,6 +603,7 @@ class AiBuilderTools(ToolBase):
602
603
  create_if_missing=True if create_if_missing is None else bool(create_if_missing),
603
604
  publish=publish,
604
605
  apps=apps,
606
+ form=None,
605
607
  add_fields=[],
606
608
  update_fields=[],
607
609
  remove_fields=[],
@@ -638,10 +640,11 @@ class AiBuilderTools(ToolBase):
638
640
  visibility=visibility,
639
641
  create_if_missing=effective_create_if_missing,
640
642
  publish=publish,
643
+ form=form or [],
641
644
  add_fields=add_fields or [],
642
645
  update_fields=update_fields or [],
643
646
  remove_fields=remove_fields or [],
644
- apps=[],
647
+ apps=None,
645
648
  )
646
649
 
647
650
  @mcp.tool()
@@ -2101,6 +2104,7 @@ class AiBuilderTools(ToolBase):
2101
2104
  visibility: JSONObject | None = None,
2102
2105
  create_if_missing: bool | None = None,
2103
2106
  publish: bool = True,
2107
+ form: list[JSONObject] | None = None,
2104
2108
  add_fields: list[JSONObject],
2105
2109
  update_fields: list[JSONObject],
2106
2110
  remove_fields: list[JSONObject],
@@ -2126,6 +2130,21 @@ class AiBuilderTools(ToolBase):
2126
2130
  package_id = normalized_apps_payload.get("package_id") # type: ignore[assignment]
2127
2131
  apps = normalized_apps_payload.get("apps") # type: ignore[assignment]
2128
2132
  input_warnings = list(normalized_apps_payload.get("warnings") or [])
2133
+ normalized_apps: list[JSONObject] = []
2134
+ for index, app_item in enumerate(apps or []):
2135
+ if not isinstance(app_item, dict):
2136
+ normalized_apps.append(app_item)
2137
+ continue
2138
+ form_result = _apply_schema_form_to_payload(
2139
+ tool_name="app_schema_apply",
2140
+ payload=app_item,
2141
+ path=f"apps[{index}]",
2142
+ )
2143
+ failure = form_result.get("failure")
2144
+ if isinstance(failure, dict):
2145
+ return _attach_builder_apply_envelope("app_schema_apply", failure)
2146
+ normalized_apps.append(form_result.get("payload") or app_item)
2147
+ apps = normalized_apps
2129
2148
  result = self._app_schema_apply_multi(
2130
2149
  profile=profile,
2131
2150
  package_id=package_id,
@@ -2139,6 +2158,22 @@ class AiBuilderTools(ToolBase):
2139
2158
  result_warnings.extend(deepcopy(input_warnings))
2140
2159
  result["warnings"] = result_warnings
2141
2160
  return _attach_builder_apply_envelope("app_schema_apply", result)
2161
+ inline_form_layout_sections: list[JSONObject] = []
2162
+ if form:
2163
+ form_payload: JSONObject = {"form": form}
2164
+ if add_fields:
2165
+ form_payload["add_fields"] = add_fields
2166
+ form_result = _apply_schema_form_to_payload(
2167
+ tool_name="app_schema_apply",
2168
+ payload=form_payload,
2169
+ path="schema",
2170
+ )
2171
+ failure = form_result.get("failure")
2172
+ if isinstance(failure, dict):
2173
+ return _attach_builder_apply_envelope("app_schema_apply", failure)
2174
+ normalized_payload = form_result.get("payload") if isinstance(form_result.get("payload"), dict) else {}
2175
+ add_fields = list(normalized_payload.get("add_fields") or [])
2176
+ inline_form_layout_sections = list(form_result.get("layout_sections") or [])
2142
2177
  result = self._app_schema_apply_once(
2143
2178
  profile=profile,
2144
2179
  app_key=app_key,
@@ -2153,6 +2188,7 @@ class AiBuilderTools(ToolBase):
2153
2188
  add_fields=add_fields,
2154
2189
  update_fields=update_fields,
2155
2190
  remove_fields=remove_fields,
2191
+ inline_form_layout_sections=inline_form_layout_sections,
2156
2192
  )
2157
2193
  result = self._retry_after_self_lock_release(
2158
2194
  profile=profile,
@@ -2171,6 +2207,7 @@ class AiBuilderTools(ToolBase):
2171
2207
  add_fields=add_fields,
2172
2208
  update_fields=update_fields,
2173
2209
  remove_fields=remove_fields,
2210
+ inline_form_layout_sections=inline_form_layout_sections,
2174
2211
  ),
2175
2212
  )
2176
2213
  return _attach_builder_apply_envelope("app_schema_apply", result)
@@ -2429,6 +2466,19 @@ class AiBuilderTools(ToolBase):
2429
2466
  remove_fields = list(compiled_item.get("remove_fields") or [])
2430
2467
  if bool(existing.get("created")) and not deferred_add_fields and not update_fields and not remove_fields:
2431
2468
  shell_result = existing.get("shell_result") if isinstance(existing.get("shell_result"), dict) else {}
2469
+ layout_sections = list(compiled_item.get("_inline_form_layout_sections") or [])
2470
+ if layout_sections:
2471
+ shell_result = self._apply_inline_form_layout_after_schema(
2472
+ profile=profile,
2473
+ schema_result=shell_result,
2474
+ fallback_app_key=app_key,
2475
+ publish=publish,
2476
+ sections=layout_sections,
2477
+ )
2478
+ if _schema_apply_result_has_write(shell_result):
2479
+ any_write_executed = True
2480
+ if _schema_apply_result_may_have_write(shell_result):
2481
+ any_write_may_have_succeeded = True
2432
2482
  item_status = shell_result.get("status") if shell_result.get("status") in {"success", "partial_success"} else "failed"
2433
2483
  shell_field_diff = existing.get("shell_field_diff") if isinstance(existing.get("shell_field_diff"), dict) else {}
2434
2484
  shell_field_diff_details = existing.get("shell_field_diff_details") if isinstance(existing.get("shell_field_diff_details"), dict) else {}
@@ -2449,6 +2499,8 @@ class AiBuilderTools(ToolBase):
2449
2499
  "safe_to_retry": False,
2450
2500
  **({"next_action": shell_result.get("next_action")} if shell_result.get("next_action") else {}),
2451
2501
  **({"verification": shell_result.get("verification")} if isinstance(shell_result.get("verification"), dict) else {}),
2502
+ **({"inline_form_layout": shell_result.get("inline_form_layout")} if isinstance(shell_result.get("inline_form_layout"), dict) else {}),
2503
+ **({"inline_form_layout_result": shell_result.get("inline_form_layout_result")} if isinstance(shell_result.get("inline_form_layout_result"), dict) else {}),
2452
2504
  })
2453
2505
  continue
2454
2506
 
@@ -2468,6 +2520,15 @@ class AiBuilderTools(ToolBase):
2468
2520
  remove_fields=remove_fields,
2469
2521
  )
2470
2522
  public_result = _publicize_package_fields(field_result)
2523
+ layout_sections = list(compiled_item.get("_inline_form_layout_sections") or [])
2524
+ if layout_sections:
2525
+ public_result = self._apply_inline_form_layout_after_schema(
2526
+ profile=profile,
2527
+ schema_result=public_result,
2528
+ fallback_app_key=app_key,
2529
+ publish=publish,
2530
+ sections=layout_sections,
2531
+ )
2471
2532
  if _schema_apply_result_has_write(public_result):
2472
2533
  any_write_executed = True
2473
2534
  if _schema_apply_result_may_have_write(public_result):
@@ -2499,6 +2560,8 @@ class AiBuilderTools(ToolBase):
2499
2560
  "safe_to_retry": False,
2500
2561
  **({"next_action": public_result.get("next_action")} if public_result.get("next_action") else {}),
2501
2562
  **({"verification": public_result.get("verification")} if isinstance(public_result.get("verification"), dict) else {}),
2563
+ **({"inline_form_layout": public_result.get("inline_form_layout")} if isinstance(public_result.get("inline_form_layout"), dict) else {}),
2564
+ **({"inline_form_layout_result": public_result.get("inline_form_layout_result")} if isinstance(public_result.get("inline_form_layout_result"), dict) else {}),
2502
2565
  })
2503
2566
 
2504
2567
  pending_readback = sum(1 for item in final_items if item.get("status") == "pending_readback")
@@ -2562,6 +2625,7 @@ class AiBuilderTools(ToolBase):
2562
2625
  add_fields: list[JSONObject],
2563
2626
  update_fields: list[JSONObject],
2564
2627
  remove_fields: list[JSONObject],
2628
+ inline_form_layout_sections: list[JSONObject] | None = None,
2565
2629
  ) -> JSONObject:
2566
2630
  """执行内部辅助逻辑。"""
2567
2631
  effective_app_name = app_name or app_title
@@ -2673,7 +2737,83 @@ class AiBuilderTools(ToolBase):
2673
2737
  normalized_args=normalized_args,
2674
2738
  suggested_next_call={"tool_name": "app_schema_apply", "arguments": {"profile": profile, **normalized_args}},
2675
2739
  )
2676
- return _publicize_package_fields(result)
2740
+ public_result = _publicize_package_fields(result)
2741
+ if inline_form_layout_sections:
2742
+ public_result = self._apply_inline_form_layout_after_schema(
2743
+ profile=profile,
2744
+ schema_result=public_result,
2745
+ fallback_app_key=str(plan_args.get("app_key") or app_key),
2746
+ publish=publish,
2747
+ sections=inline_form_layout_sections,
2748
+ )
2749
+ return public_result
2750
+
2751
+ def _apply_inline_form_layout_after_schema(
2752
+ self,
2753
+ *,
2754
+ profile: str,
2755
+ schema_result: JSONObject,
2756
+ fallback_app_key: str = "",
2757
+ publish: bool,
2758
+ sections: list[JSONObject],
2759
+ ) -> JSONObject:
2760
+ result = deepcopy(schema_result)
2761
+ if not sections:
2762
+ return result
2763
+ app_key = str(result.get("app_key") or result.get("appKey") or fallback_app_key or "").strip()
2764
+ result["inline_form_layout"] = {
2765
+ "requested": True,
2766
+ "section_count": len(sections),
2767
+ "applied": False,
2768
+ }
2769
+ if result.get("status") not in {"success", "partial_success"}:
2770
+ result["inline_form_layout"]["skipped_reason"] = "schema_apply_failed"
2771
+ verification = result.get("verification") if isinstance(result.get("verification"), dict) else {}
2772
+ verification = deepcopy(verification)
2773
+ verification["inline_form_layout_verified"] = False
2774
+ result["verification"] = verification
2775
+ return result
2776
+ if not app_key:
2777
+ result["status"] = "partial_success"
2778
+ result["error_code"] = result.get("error_code") or "INLINE_FORM_LAYOUT_APP_KEY_MISSING"
2779
+ result["message"] = "schema applied but inline form layout could not run because app_key was not resolved"
2780
+ result["inline_form_layout"]["error_code"] = "INLINE_FORM_LAYOUT_APP_KEY_MISSING"
2781
+ verification = result.get("verification") if isinstance(result.get("verification"), dict) else {}
2782
+ verification = deepcopy(verification)
2783
+ verification["inline_form_layout_verified"] = False
2784
+ result["verification"] = verification
2785
+ result["safe_to_retry"] = False
2786
+ return result
2787
+ layout_result = self._app_layout_apply_once(
2788
+ profile=profile,
2789
+ app_key=app_key,
2790
+ mode="merge",
2791
+ publish=publish,
2792
+ sections=sections,
2793
+ )
2794
+ layout_ok = isinstance(layout_result, dict) and layout_result.get("status") in {"success", "partial_success"}
2795
+ result["inline_form_layout"] = {
2796
+ "requested": True,
2797
+ "section_count": len(sections),
2798
+ "app_key": app_key,
2799
+ "applied": layout_ok,
2800
+ "layout_status": layout_result.get("status") if isinstance(layout_result, dict) else "failed",
2801
+ **({"error_code": layout_result.get("error_code")} if isinstance(layout_result, dict) and layout_result.get("error_code") else {}),
2802
+ **({"message": layout_result.get("message")} if isinstance(layout_result, dict) and layout_result.get("message") else {}),
2803
+ }
2804
+ result["inline_form_layout_result"] = layout_result
2805
+ verification = result.get("verification") if isinstance(result.get("verification"), dict) else {}
2806
+ verification = deepcopy(verification)
2807
+ verification["inline_form_layout_verified"] = layout_ok
2808
+ result["verification"] = verification
2809
+ if layout_ok:
2810
+ result["write_executed"] = True
2811
+ return result
2812
+ result["status"] = "partial_success" if result.get("status") == "success" else result.get("status", "partial_success")
2813
+ result["error_code"] = result.get("error_code") or "INLINE_FORM_LAYOUT_APPLY_FAILED"
2814
+ result["message"] = "schema applied but inline form layout failed"
2815
+ result["safe_to_retry"] = False
2816
+ return result
2677
2817
 
2678
2818
  @tool_cn_name("应用布局应用")
2679
2819
  def app_layout_apply(
@@ -3556,7 +3696,12 @@ def _schema_apps_expected_shape() -> JSONObject:
3556
3696
  "app_name": "员工花名册",
3557
3697
  "icon": "business-personalcard",
3558
3698
  "color": "emerald",
3559
- "add_fields": [{"name": "员工名称", "type": "text", "as_data_title": True}],
3699
+ "form": [
3700
+ {
3701
+ "section": "基础信息",
3702
+ "rows": [[{"name": "员工名称", "type": "text", "data_title": True}]],
3703
+ }
3704
+ ],
3560
3705
  }
3561
3706
  ],
3562
3707
  }
@@ -3565,7 +3710,7 @@ def _schema_apps_expected_shape() -> JSONObject:
3565
3710
  def _schema_apps_expected_shape_json() -> str:
3566
3711
  return (
3567
3712
  '{"package_id":1001,"apps":[{"client_key":"employee","app_name":"员工花名册",'
3568
- '"icon":"business-personalcard","color":"emerald","add_fields":[{"name":"员工名称","type":"text","as_data_title":true}]}]}'
3713
+ '"icon":"business-personalcard","color":"emerald","form":[{"section":"基础信息","rows":[[{"name":"员工名称","type":"text","data_title":true}]]}]}]}'
3569
3714
  )
3570
3715
 
3571
3716
 
@@ -3650,6 +3795,163 @@ def _normalize_schema_apps_argument(*, tool_name: str, package_id: int | None, a
3650
3795
  return {"package_id": normalized_package_id, "apps": normalized_items, "warnings": warnings}
3651
3796
 
3652
3797
 
3798
+ def _normalize_schema_form_field(field: JSONObject) -> JSONObject:
3799
+ payload = deepcopy(field)
3800
+ if "data_title" in payload and "as_data_title" not in payload and "asDataTitle" not in payload:
3801
+ payload["as_data_title"] = payload.pop("data_title")
3802
+ if "dataTitle" in payload and "as_data_title" not in payload and "asDataTitle" not in payload:
3803
+ payload["as_data_title"] = payload.pop("dataTitle")
3804
+ if "data_cover" in payload and "as_data_cover" not in payload and "asDataCover" not in payload:
3805
+ payload["as_data_cover"] = payload.pop("data_cover")
3806
+ if "dataCover" in payload and "as_data_cover" not in payload and "asDataCover" not in payload:
3807
+ payload["as_data_cover"] = payload.pop("dataCover")
3808
+ return payload
3809
+
3810
+
3811
+ def _schema_form_entry_name(entry: object) -> str:
3812
+ if isinstance(entry, dict):
3813
+ for key in ("name", "title", "label", "field_name", "fieldName"):
3814
+ if entry.get(key) is not None:
3815
+ return str(entry.get(key) or "").strip()
3816
+ return ""
3817
+ if isinstance(entry, (str, int)):
3818
+ return str(entry).strip()
3819
+ return ""
3820
+
3821
+
3822
+ def _schema_form_failure(*, tool_name: str, path: str, message: str, fix_hint: str) -> JSONObject:
3823
+ return _config_failure(
3824
+ tool_name=tool_name,
3825
+ error_code="FORM_SHAPE_INVALID",
3826
+ message=message,
3827
+ fix_hint=fix_hint,
3828
+ details={
3829
+ "path": path,
3830
+ "expected_shape": {
3831
+ "form": [
3832
+ {
3833
+ "section": "基础信息",
3834
+ "rows": [
3835
+ [
3836
+ {"name": "标题", "type": "text", "data_title": True},
3837
+ {"name": "状态", "type": "single_select", "options": ["待处理", "处理中", "已完成"]},
3838
+ ]
3839
+ ],
3840
+ }
3841
+ ]
3842
+ },
3843
+ },
3844
+ )
3845
+
3846
+
3847
+ def _compile_schema_form_payload(*, tool_name: str, form: object, path: str = "form") -> JSONObject:
3848
+ if form is None:
3849
+ return {"add_fields": [], "layout_sections": []}
3850
+ if not isinstance(form, list) or not form:
3851
+ return {
3852
+ "failure": _schema_form_failure(
3853
+ tool_name=tool_name,
3854
+ path=path,
3855
+ message=f"{path} must be a non-empty list of form sections.",
3856
+ fix_hint='Use form=[{"section":"基础信息","rows":[[{"name":"标题","type":"text","data_title":true}]]}].',
3857
+ )
3858
+ }
3859
+
3860
+ add_fields: list[JSONObject] = []
3861
+ layout_sections: list[JSONObject] = []
3862
+ for section_index, section in enumerate(form):
3863
+ section_path = f"{path}[{section_index}]"
3864
+ if not isinstance(section, dict):
3865
+ return {
3866
+ "failure": _schema_form_failure(
3867
+ tool_name=tool_name,
3868
+ path=section_path,
3869
+ message=f"{section_path} must be an object.",
3870
+ fix_hint="Each form section must include section and rows.",
3871
+ )
3872
+ }
3873
+ section_title = str(section.get("section") or section.get("title") or section.get("name") or "").strip()
3874
+ rows = section.get("rows")
3875
+ if not section_title:
3876
+ return {
3877
+ "failure": _schema_form_failure(
3878
+ tool_name=tool_name,
3879
+ path=f"{section_path}.section",
3880
+ message=f"{section_path}.section is required.",
3881
+ fix_hint='Set a business section title such as "基础信息" or "生产信息".',
3882
+ )
3883
+ }
3884
+ if not isinstance(rows, list) or not rows:
3885
+ return {
3886
+ "failure": _schema_form_failure(
3887
+ tool_name=tool_name,
3888
+ path=f"{section_path}.rows",
3889
+ message=f"{section_path}.rows must be a non-empty list.",
3890
+ fix_hint="Use rows as a list of field rows; each row is a list of field objects.",
3891
+ )
3892
+ }
3893
+ layout_rows: list[list[str]] = []
3894
+ for row_index, row in enumerate(rows):
3895
+ row_path = f"{section_path}.rows[{row_index}]"
3896
+ if isinstance(row, dict):
3897
+ row_fields = row.get("fields")
3898
+ else:
3899
+ row_fields = row
3900
+ if not isinstance(row_fields, list) or not row_fields:
3901
+ return {
3902
+ "failure": _schema_form_failure(
3903
+ tool_name=tool_name,
3904
+ path=row_path,
3905
+ message=f"{row_path} must be a non-empty field list.",
3906
+ fix_hint='Use rows like [[{"name":"字段A","type":"text"},{"name":"字段B","type":"number"}]].',
3907
+ )
3908
+ }
3909
+ layout_row: list[str] = []
3910
+ for field_index, field in enumerate(row_fields):
3911
+ field_path = f"{row_path}[{field_index}]"
3912
+ field_name = _schema_form_entry_name(field)
3913
+ if not field_name:
3914
+ return {
3915
+ "failure": _schema_form_failure(
3916
+ tool_name=tool_name,
3917
+ path=field_path,
3918
+ message=f"{field_path} must include a field name.",
3919
+ fix_hint='Each new field object must include name, for example {"name":"工单编号","type":"text"}.',
3920
+ )
3921
+ }
3922
+ layout_row.append(field_name)
3923
+ if isinstance(field, dict):
3924
+ add_fields.append(_normalize_schema_form_field(field))
3925
+ layout_rows.append(layout_row)
3926
+ layout_sections.append({"title": section_title, "rows": layout_rows})
3927
+ return {"add_fields": add_fields, "layout_sections": layout_sections}
3928
+
3929
+
3930
+ def _apply_schema_form_to_payload(*, tool_name: str, payload: JSONObject, path: str) -> JSONObject:
3931
+ form = payload.get("form")
3932
+ if form is None:
3933
+ return {"payload": payload, "layout_sections": []}
3934
+ existing_add_fields = payload.get("add_fields") or payload.get("addFields")
3935
+ if isinstance(existing_add_fields, list) and existing_add_fields:
3936
+ return {
3937
+ "failure": _config_failure(
3938
+ tool_name=tool_name,
3939
+ error_code="FORM_WITH_ADD_FIELDS_CONFLICT",
3940
+ message=f"{path} cannot use form and add_fields together.",
3941
+ fix_hint="Use form as the single schema creation shape: put fields under form[].rows[][].",
3942
+ details={"path": path, "conflicting_keys": ["form", "add_fields"]},
3943
+ )
3944
+ }
3945
+ compiled = _compile_schema_form_payload(tool_name=tool_name, form=form, path=f"{path}.form")
3946
+ if isinstance(compiled.get("failure"), dict):
3947
+ return {"failure": compiled["failure"]}
3948
+ normalized = deepcopy(payload)
3949
+ normalized.pop("form", None)
3950
+ normalized["add_fields"] = compiled.get("add_fields") or []
3951
+ normalized["_inline_form_layout_sections"] = compiled.get("layout_sections") or []
3952
+ return {"payload": normalized, "layout_sections": compiled.get("layout_sections") or []}
3953
+
3954
+
3653
3955
  def _multi_app_item_app_name(item: JSONObject) -> str:
3654
3956
  return str(item.get("app_name") or item.get("appName") or item.get("appTitle") or item.get("app_title") or item.get("title") or "").strip()
3655
3957
 
@@ -3671,7 +3973,7 @@ def _field_type_for_static_validation(field: JSONObject) -> str:
3671
3973
 
3672
3974
 
3673
3975
  def _is_data_title_field(field: JSONObject) -> bool:
3674
- return bool(field.get("as_data_title") or field.get("asDataTitle"))
3976
+ return bool(field.get("as_data_title") or field.get("asDataTitle") or field.get("data_title") or field.get("dataTitle"))
3675
3977
 
3676
3978
 
3677
3979
  def _collect_multi_app_target_refs(value: object, *, path: str) -> list[JSONObject]:
@@ -6004,6 +6306,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6004
6306
  "field.customBtnTextStatus": "field.custom_button_text_enabled",
6005
6307
  "field.customBtnText": "field.custom_button_text",
6006
6308
  "field.subfieldUpdates": "field.subfield_updates",
6309
+ "field.data_title": "field.as_data_title",
6310
+ "field.dataTitle": "field.as_data_title",
6311
+ "field.data_cover": "field.as_data_cover",
6312
+ "field.dataCover": "field.as_data_cover",
6007
6313
  "field.asDataTitle": "field.as_data_title",
6008
6314
  "field.asDataCover": "field.as_data_cover",
6009
6315
  },
@@ -6031,7 +6337,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6031
6337
  "color": "emerald",
6032
6338
  "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
6033
6339
  "create_if_missing": True,
6034
- "add_fields": [{"name": "项目名称", "type": "text", "as_data_title": True}],
6340
+ "add_fields": [{"name": "项目名称", "type": "text", "data_title": True}],
6035
6341
  "update_fields": [],
6036
6342
  "remove_fields": [],
6037
6343
  },
@@ -6066,6 +6372,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6066
6372
  "visibility",
6067
6373
  "create_if_missing",
6068
6374
  "publish",
6375
+ "form",
6069
6376
  "add_fields",
6070
6377
  "update_fields",
6071
6378
  "remove_fields",
@@ -6079,6 +6386,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6079
6386
  "apps[].icon_color",
6080
6387
  "apps[].icon_config",
6081
6388
  "apps[].visibility",
6389
+ "apps[].form",
6390
+ "apps[].form[].section",
6391
+ "apps[].form[].rows",
6082
6392
  "apps[].add_fields",
6083
6393
  "apps[].update_fields",
6084
6394
  "apps[].remove_fields",
@@ -6123,6 +6433,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6123
6433
  "field.customBtnTextStatus": "field.custom_button_text_enabled",
6124
6434
  "field.customBtnText": "field.custom_button_text",
6125
6435
  "field.subfieldUpdates": "field.subfield_updates",
6436
+ "field.data_title": "field.as_data_title",
6437
+ "field.dataTitle": "field.as_data_title",
6438
+ "field.data_cover": "field.as_data_cover",
6439
+ "field.dataCover": "field.as_data_cover",
6126
6440
  "field.asDataTitle": "field.as_data_title",
6127
6441
  "field.asDataCover": "field.as_data_cover",
6128
6442
  },
@@ -6143,6 +6457,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6143
6457
  "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",
6144
6458
  "multi-app mode: pass package_id + apps[]; create_if_missing defaults to true for app_name items and may be set false only when every item uses app_key",
6145
6459
  "CLI --apps-file primary shape is {package_id, apps:[...]}; raw app arrays and singleton wrapper arrays are compatibility paths, not recommended examples",
6460
+ "create app schemas with form: form[].section names form sections; form[].rows[][] contains field objects; builder splits form into field creation plus form layout apply",
6461
+ "single-app CLI primary shape is --form-file; multi-app primary shape is apps[].form inside --apps-file",
6146
6462
  "multi-app mode preflights static errors before writing: duplicate client_key/app_name, missing data title on new apps, explicit create_if_missing=false with app_name items, and unresolved target_app_ref/target_app",
6147
6463
  "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",
6148
6464
  "multi-app relation fields may also use target_app with another apps[].app_name; prefer target_app_ref/client_key when names may collide",
@@ -6186,9 +6502,16 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6186
6502
  "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
6187
6503
  "create_if_missing": True,
6188
6504
  "publish": True,
6189
- "add_fields": [
6190
- {"name": "项目名称", "type": "text", "as_data_title": True},
6191
- {"name": "项目封面", "type": "attachment", "as_data_cover": True},
6505
+ "form": [
6506
+ {
6507
+ "section": "基础信息",
6508
+ "rows": [
6509
+ [
6510
+ {"name": "项目名称", "type": "text", "data_title": True},
6511
+ {"name": "项目封面", "type": "attachment", "data_cover": True},
6512
+ ]
6513
+ ],
6514
+ }
6192
6515
  ],
6193
6516
  "update_fields": [],
6194
6517
  "remove_fields": [],
@@ -6203,9 +6526,16 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6203
6526
  "app_name": "员工花名册",
6204
6527
  "icon": "business-personalcard",
6205
6528
  "color": "emerald",
6206
- "add_fields": [
6207
- {"name": "员工名称", "type": "text", "as_data_title": True},
6208
- {"name": "员工照片", "type": "attachment", "as_data_cover": True},
6529
+ "form": [
6530
+ {
6531
+ "section": "基础信息",
6532
+ "rows": [
6533
+ [
6534
+ {"name": "员工名称", "type": "text", "data_title": True},
6535
+ {"name": "员工照片", "type": "attachment", "data_cover": True},
6536
+ ]
6537
+ ],
6538
+ }
6209
6539
  ],
6210
6540
  },
6211
6541
  {
@@ -6213,15 +6543,22 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6213
6543
  "app_name": "工时表",
6214
6544
  "icon": "clock",
6215
6545
  "color": "blue",
6216
- "add_fields": [
6217
- {"name": "工时标题", "type": "text", "as_data_title": True},
6546
+ "form": [
6218
6547
  {
6219
- "name": "关联员工",
6220
- "type": "relation",
6221
- "target_app_ref": "employee",
6222
- "display_field": {"name": "员工名称"},
6223
- "visible_fields": [{"name": "员工名称"}],
6224
- },
6548
+ "section": "基础信息",
6549
+ "rows": [
6550
+ [{"name": "工时标题", "type": "text", "data_title": True}],
6551
+ [
6552
+ {
6553
+ "name": "关联员工",
6554
+ "type": "relation",
6555
+ "target_app_ref": "employee",
6556
+ "display_field": {"name": "员工名称"},
6557
+ "visible_fields": [{"name": "员工名称"}],
6558
+ }
6559
+ ],
6560
+ ],
6561
+ }
6225
6562
  ],
6226
6563
  },
6227
6564
  ],
@@ -7105,15 +7442,38 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
7105
7442
  "name": "dash_name",
7106
7443
  "sourceType": "source_type",
7107
7444
  "chartRef": "chart_ref",
7445
+ "inlineChart": "inline_chart",
7446
+ "createChart": "inline_chart",
7108
7447
  "viewRef": "view_ref",
7109
7448
  "dashStyleConfigBO": "dash_style_config",
7110
7449
  },
7111
- "section_allowed_keys": ["title", "source_type", "role", "position", "dash_style_config", "config", "chart_ref", "view_ref", "text", "url"],
7450
+ "section_allowed_keys": [
7451
+ "title",
7452
+ "source_type",
7453
+ "role",
7454
+ "position",
7455
+ "dash_style_config",
7456
+ "config",
7457
+ "chart_ref",
7458
+ "inline_chart",
7459
+ "inline_chart.app_key",
7460
+ "inline_chart.chart_name",
7461
+ "inline_chart.chart_type",
7462
+ "inline_chart.metric",
7463
+ "inline_chart.metrics",
7464
+ "inline_chart.group_by",
7465
+ "inline_chart.where",
7466
+ "view_ref",
7467
+ "text",
7468
+ "url",
7469
+ ],
7112
7470
  "section_aliases": {
7113
7471
  "sourceType": "source_type",
7114
7472
  "zone": "role",
7115
7473
  "sectionRole": "role",
7116
7474
  "chartRef": "chart_ref",
7475
+ "inlineChart": "inline_chart",
7476
+ "createChart": "inline_chart",
7117
7477
  "viewRef": "view_ref",
7118
7478
  "dashStyleConfigBO": "dash_style_config",
7119
7479
  },
@@ -7139,6 +7499,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
7139
7499
  "package_id is required when creating a new portal",
7140
7500
  "publish=false only guarantees draft and base-info updates; it does not claim live has changed",
7141
7501
  "chart_ref resolves by chart_id first, then exact unique chart_name",
7502
+ "inline_chart internally creates or updates an app-source QingBI report, then uses the returned chart_id for this portal section",
7503
+ "use chart_ref when an existing report already matches; use inline_chart when the portal needs a new or adjusted report",
7142
7504
  "view_ref resolves by view_key first, then exact unique view_name",
7143
7505
  "pc layout uses a 24-column grid; mobile layout uses a 6-column grid",
7144
7506
  "if unsure about layout, omit position or use layout_preset=auto/dashboard_2col/dashboard_3col",
@@ -7166,7 +7528,12 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
7166
7528
  {
7167
7529
  "title": "经营概览",
7168
7530
  "source_type": "chart",
7169
- "chart_ref": {"app_key": "APP_KEY", "chart_name": "数据总量"},
7531
+ "inline_chart": {
7532
+ "app_key": "APP_KEY",
7533
+ "chart_name": "数据总量",
7534
+ "chart_type": "summary",
7535
+ "metric": {"field": "数据ID", "agg": "count"},
7536
+ },
7170
7537
  "position": {
7171
7538
  "pc": {"x": 0, "y": 0, "cols": 12, "rows": 6},
7172
7539
  "mobile": {"x": 0, "y": 0, "cols": 6, "rows": 6},