@qingflow-tech/qingflow-cli 1.1.50 → 1.1.53

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.
Files changed (71) hide show
  1. package/README.md +3 -3
  2. package/package.json +1 -1
  3. package/pyproject.toml +8 -2
  4. package/skills/qingflow-cli/SKILL.md +115 -3
  5. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md +3 -9
  6. package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +3 -2
  7. package/skills/qingflow-cli/reference/builder/30-schema-fields.md +9 -5
  8. package/skills/qingflow-cli/reference/builder/50-views.md +192 -191
  9. package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +2 -0
  10. package/skills/qingflow-cli/reference/builder/README.md +1 -1
  11. package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +6 -6
  12. package/skills/qingflow-cli/reference/data/README.md +1 -1
  13. package/skills/qingflow-cli/reference/examples/README.md +1 -1
  14. package/skills/qingflow-cli/reference/examples/views/views_apply_v1alpha1.example.json +14 -0
  15. package/skills/qingflow-cli/reference/examples/views/views_upsert_table_minimal.example.json +30 -36
  16. package/src/qingflow_mcp/app_form_compat.py +3 -0
  17. package/src/qingflow_mcp/app_form_validator.py +69 -17
  18. package/src/qingflow_mcp/builder_facade/app_form.py +497 -100
  19. package/src/qingflow_mcp/builder_facade/models.py +227 -17
  20. package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +1 -0
  21. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +64 -30
  22. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +4 -8
  23. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +4 -8
  24. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +6 -10
  25. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +6 -10
  26. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +11 -18
  27. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +4 -8
  28. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +4 -8
  29. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +6 -10
  30. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +4 -8
  31. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +4 -8
  32. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +4 -8
  33. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +6 -10
  34. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +4 -8
  35. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +4 -8
  36. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +4 -8
  37. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +6 -10
  38. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +46 -15
  39. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/rich_text.json +146 -0
  40. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +4 -8
  41. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +4 -8
  42. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +61 -27
  43. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +4 -8
  44. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/formula.json +43 -0
  45. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +7 -12
  46. package/src/qingflow_mcp/builder_facade/schemas/views/manifest.json +42 -0
  47. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/complete.json +1245 -0
  48. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/button-config.json +53 -0
  49. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/data-permission.json +60 -0
  50. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/display.json +73 -0
  51. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/operation-permission.json +48 -0
  52. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/query-condition.json +47 -0
  53. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/summary.json +319 -0
  54. package/src/qingflow_mcp/builder_facade/service.py +1666 -509
  55. package/src/qingflow_mcp/builder_facade/views_extract.py +592 -0
  56. package/src/qingflow_mcp/builder_facade/views_schema.py +400 -0
  57. package/src/qingflow_mcp/cli/commands/builder.py +76 -55
  58. package/src/qingflow_mcp/config.py +1 -0
  59. package/src/qingflow_mcp/public_surface.py +3 -0
  60. package/src/qingflow_mcp/response_trim.py +11 -2
  61. package/src/qingflow_mcp/server_app_builder.py +34 -10
  62. package/src/qingflow_mcp/solution/compiler/view_compiler.py +2 -1
  63. package/src/qingflow_mcp/solution/executor.py +96 -8
  64. package/src/qingflow_mcp/solution/spec_models.py +1 -0
  65. package/src/qingflow_mcp/tools/ai_builder_tools.py +218 -398
  66. package/src/qingflow_mcp/tools/base.py +42 -10
  67. package/src/qingflow_mcp/tools/solution_tools.py +2 -0
  68. package/src/qingflow_mcp/tools/view_tools.py +0 -6
  69. package/src/qingflow_mcp/tools/view_writer.py +44 -0
  70. package/skills/qingflow-cli/reference/examples/views/views_batch_full.example.json +0 -159
  71. package/skills/qingflow-cli/reference/views_upsert_table_minimal.example.json +0 -42
@@ -56,8 +56,8 @@ def build_builder_server() -> FastMCP:
56
56
  "For builder delete/remove apply results, separate delete execution from readback verification: after DELETE is sent, resources expose delete_executed, readback_status, and safe_to_retry_delete=false. If readback_status is unavailable or still_exists, do not blindly repeat the delete; confirm later with app_get/view_get/chart_get or the relevant apply readback. Views/buttons use single-item readback; associated resources use one app-level resource-pool readback because there is no confirmed single-item GET. "
57
57
  "For builder write/apply results, separate write execution from final readback verification: status=partial_success with write_executed=true and safe_to_retry=false means the write was sent or succeeded, while final readback, publish verification, or metadata confirmation is pending or permission-restricted. Report it as written but unverified; do not repeat the same write blindly. "
58
58
  "For app_form_apply, pin the version from app_form_schema, preserve the complete body and existing IDs from a draft get, and configure data display through app-level spec.dataTitleField/spec.dataCoverField selectors rather than booleans on fields; App Builder MCP validates the declaration inside app_form_apply, while the CLI route has a separate static validate command. dataTitleField must resolve to exactly one top-level field for a non-empty body, while dataCoverField is optional and must resolve to a top-level attachment field. A root appKey means update and may include packageId; no appKey means create and requires packageId plus spec.appName. AppForm does not accept spec.icon or spec.color; creation derives a stable default icon and color from spec.appName. "
59
- "For app_views_apply, keep fixed saved filters in filters and configure the frontend query panel separately with query_conditions; query_conditions.rows is a matrix of field names compiled to backend queryCondition queIds. New views default associated report/view display to visible with limit_type=all; existing views preserve their current associated display unless associated_resources is explicitly patched. View writes use ViewManagementAuth (beingViewManageStatus), falling back to DataManageAuth when advanced app permissions are not enabled. "
60
- "For ordinary business buttons, declare them on the target view with app_views_apply.views[].action_buttons; app_views_apply writes the view first, then creates/binds the button. Use app_custom_buttons_apply only when the user explicitly asks to maintain existing standalone buttons outside view design, such as custom style/icon changes, cross-view reuse, deleting button bodies, bulk binding reorder, or exact qRobot/wings config. Button body writes require EditAppAuth; view_configs placement writes require ViewManagementAuth, matching the backend viewConfig route. For addData buttons, prefer action_buttons.target_app_key + field_mappings/default_values on the view path; do not ask agents to write raw que_relation unless maintaining a legacy config. field_mappings.source_field accepts source schema fields and supported system fields: 数据ID/row_record_id/apply_id/_id means current record id (-17), 编号/record_number means visible record number (0). To fill a target relation with the current record, map {'source_field': '数据ID', 'target_field': '目标引用字段'}; default_values is only for static constants. View button bindings merge by default; standalone view_configs are only for explicit maintenance. Builder view_key arguments are raw keys from app_get.views[].view_key and must not be prefixed with custom:. "
59
+ "For app_views_apply, keep fixed saved filters in filters and configure the frontend query panel separately with query_conditions; query_conditions.rows accepts field names, field IDs, or queIds and compiles them to backend queryCondition queIds, with queIds preferred. New views default associated report/view display to visible with limit_type=all; existing views preserve their current associated display unless associated_resources is explicitly patched. View writes use ViewManagementAuth (beingViewManageStatus), falling back to DataManageAuth when advanced app permissions are not enabled. "
60
+ "For ordinary business buttons, declare them on a new or full view with app_views_apply.upsert_view.action_buttons; app_views_apply writes the view first, then creates/binds the button. For an existing view, use patch_views[].set.action_buttons. Use app_custom_buttons_apply only when the user explicitly asks to maintain existing standalone buttons outside view design, such as custom style/icon changes, cross-view reuse, deleting button bodies, bulk binding reorder, or exact qRobot/wings config. Button body writes require EditAppAuth; view_configs placement writes require ViewManagementAuth, matching the backend viewConfig route. For addData buttons, prefer action_buttons.target_app_key + field_mappings/default_values on the view path; do not ask agents to write raw que_relation unless maintaining a legacy config. field_mappings.source_field accepts source schema fields and supported system fields: 数据ID/row_record_id/apply_id/_id means current record id (-17), 编号/record_number means visible record number (0). To fill a target relation with the current record, map {'source_field': '数据ID', 'target_field': '目标引用字段'}; default_values is only for static constants. View button bindings merge by default; standalone view_configs are only for explicit maintenance. Builder view_key arguments are raw keys from app_get.views[].view_key and must not be prefixed with custom:. "
61
61
  "For BI reports, keep report-body development separate from Qingflow in-app display: use app_charts_apply to create, update, remove, or reorder app-source QingBI chart bodies/configs with dataSourceType=qingflow; chart dimension/metric/filter/query fields must come from app_get_fields.chart_fields, not record schema or form-only fields; dataset BI reports are not created or edited by app_charts_apply yet and should be created in QingBI first, then attached with app_associated_resources_apply using report_source=dataset. "
62
62
  "For associated views/reports, use app_associated_resources_apply. Use match_mappings for filtering associated resources: dynamic current-record conditions use source_field, static conditions use value. match_mappings also supports 数据ID(-17) and 编号(0). Do not ask agents to write raw match_rules unless preserving a legacy backend config. "
63
63
  "For associated reports/views, use app_associated_resources_apply for both the app-level associated_resources pool and per-view display config; associated_item_id is the app-level form_asos_chart.id, and view_configs/remove/reorder may also pass an existing resource's chart_id/chart_key/view_key because the tool resolves those to the internal id. Before creating an associated resource, read app_get.associated_resources and reuse an existing matching target_app_key + view_key/chart_key through patch_resources; client_key only works inside one apply call and is not persisted. Do not ask agents to pass backend raw sourceType: views infer the internal Qingflow view source, reports default to BI app reports, and dataset reports use report_source=dataset. "
@@ -231,6 +231,32 @@ def build_builder_server() -> FastMCP:
231
231
  ) -> dict:
232
232
  return ai_builder.app_form_apply(profile=profile, spec=spec or {})
233
233
 
234
+ @server.tool()
235
+ def app_views_schema(
236
+ schema_version: str | None = None,
237
+ schema_kind: str | None = None,
238
+ ) -> dict:
239
+ """Get a complete views schema or a focused detail schema such as display or button-config."""
240
+ return ai_builder.app_views_schema(schema_version=schema_version, schema_kind=schema_kind)
241
+
242
+ @server.tool()
243
+ def app_views_get(
244
+ profile: str = DEFAULT_PROFILE,
245
+ app_key: str = "",
246
+ schema_version: str | None = None,
247
+ schema_kind: str | None = None,
248
+ ) -> dict:
249
+ return ai_builder.app_views_get(
250
+ profile=profile, app_key=app_key, schema_version=schema_version, schema_kind=schema_kind
251
+ )
252
+
253
+ @server.tool()
254
+ def app_views_validate(
255
+ declaration: dict | None = None,
256
+ schema_version: str | None = None,
257
+ ) -> dict:
258
+ return ai_builder.app_views_validate(declaration=declaration or {}, schema_version=schema_version)
259
+
234
260
  @server.tool(description=ai_builder._high_risk_tool_description(operation="delete", target="app configuration"))
235
261
  def app_delete(
236
262
  profile: str = DEFAULT_PROFILE,
@@ -445,8 +471,8 @@ def build_builder_server() -> FastMCP:
445
471
  return ai_builder.app_get_charts(profile=profile, app_key=app_key, app_keys=app_keys)
446
472
 
447
473
  @server.tool()
448
- def app_get_buttons(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> dict:
449
- return ai_builder.app_get_buttons(profile=profile, app_key=app_key, app_keys=app_keys)
474
+ def app_get_buttons(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None, button_text: str | None = None) -> dict:
475
+ return ai_builder.app_get_buttons(profile=profile, app_key=app_key, app_keys=app_keys, button_text=button_text)
450
476
 
451
477
  @server.tool()
452
478
  def app_get_associated_resources(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> dict:
@@ -546,21 +572,19 @@ def build_builder_server() -> FastMCP:
546
572
  profile: str = DEFAULT_PROFILE,
547
573
  app_key: str = "",
548
574
  publish: bool = True,
549
- upsert_views: list[dict] | None = None,
575
+ upsert_view: dict | None = None,
550
576
  patch_views: list[dict] | None = None,
551
577
  remove_views: list[str] | None = None,
552
- apps: list[dict] | None = None,
553
- views: list[dict] | None = None,
578
+ schema_version: str | None = None,
554
579
  ) -> dict:
555
580
  return ai_builder.app_views_apply(
556
581
  profile=profile,
557
582
  app_key=app_key,
558
583
  publish=publish,
559
- upsert_views=upsert_views or [],
584
+ upsert_view=upsert_view,
560
585
  patch_views=patch_views or [],
561
586
  remove_views=remove_views or [],
562
- apps=apps,
563
- views=views,
587
+ schema_version=schema_version,
564
588
  )
565
589
 
566
590
  @server.tool()
@@ -12,6 +12,7 @@ VIEW_TYPE_MAP = {
12
12
  ViewType.board: "boardView",
13
13
  ViewType.gantt: "ganttView",
14
14
  ViewType.hierarchy: "hierarchyView",
15
+ ViewType.calendar: "calendarView",
15
16
  }
16
17
 
17
18
 
@@ -32,7 +33,7 @@ def compile_views(entity: EntitySpec) -> list[dict[str, Any]]:
32
33
  "viewgraphQueIds": list(view.field_ids),
33
34
  "viewgraphQuestions": [],
34
35
  "ordinal": ordinal,
35
- "beingShowTitleQue": view.type in (ViewType.card, ViewType.board, ViewType.gantt, ViewType.hierarchy),
36
+ "beingShowTitleQue": view.type in (ViewType.card, ViewType.board, ViewType.gantt, ViewType.hierarchy, ViewType.calendar),
36
37
  "beingShowCover": False,
37
38
  "viewgraphLimitType": 1,
38
39
  "viewgraphLimit": view.filters,
@@ -36,6 +36,7 @@ class SolutionExecutor:
36
36
  app_tools: AppTools,
37
37
  record_tools: RecordTools,
38
38
  view_tools: ViewTools,
39
+ view_writer: "ViewWriter | None" = None,
39
40
  chart_tools: QingbiReportTools,
40
41
  portal_tools: PortalTools,
41
42
  navigation_tools: NavigationTools,
@@ -47,6 +48,7 @@ class SolutionExecutor:
47
48
  self.app_tools = app_tools
48
49
  self.record_tools = record_tools
49
50
  self.view_tools = view_tools
51
+ self.view_writer = view_writer
50
52
  self.chart_tools = chart_tools
51
53
  self.portal_tools = portal_tools
52
54
  self.navigation_tools = navigation_tools
@@ -460,6 +462,20 @@ class SolutionExecutor:
460
462
  "Pass {app_key, spec} to solution_build_flow or use qingflow builder flow apply."
461
463
  )
462
464
 
465
+ def _create_view_via_writer(self, *, profile: str, payload: dict[str, Any]) -> dict[str, Any]:
466
+ """视图创建唯一落点:共享 ViewWriter(POST /view/viewConfig)。
467
+
468
+ solution DSL 内部创建路径不再直接调用 ViewTools.view_create(该方法已删除);
469
+ 未注入 writer 的旧调用方退化为直接经 ViewTools 同一会话执行同一端点。
470
+ """
471
+ if self.view_writer is not None:
472
+ return self.view_writer.create_view(profile=profile, payload=payload)
473
+ from ..tools.view_writer import ViewWriter as _FallbackViewWriter
474
+
475
+ return _FallbackViewWriter(self.view_tools.sessions, self.view_tools.backend).create_view(
476
+ profile=profile, payload=payload
477
+ )
478
+
463
479
  def _build_views(self, profile: str, entity: CompiledEntity, store: RunArtifactStore) -> None:
464
480
  app_key = self._get_app_key(store, entity.entity_id)
465
481
  field_meta = store.get_artifact("field_maps", entity.entity_id, {})
@@ -474,7 +490,7 @@ class SolutionExecutor:
474
490
  schema=schema,
475
491
  group_by_field_id=view_plan["group_by_field_id"],
476
492
  )
477
- result = self.view_tools.view_create(profile=profile, payload=create_payload)
493
+ result = self._create_view_via_writer(profile=profile, payload=create_payload)
478
494
  view_result = result.get("result")
479
495
  view_key = ((view_result or {}) if isinstance(view_result, dict) else {}).get("viewgraphKey")
480
496
  if not view_key and isinstance(view_result, str):
@@ -895,13 +911,14 @@ class SolutionExecutor:
895
911
  data.setdefault("asosChartConfig", {"limitType": 1, "asosChartIdList": []})
896
912
  data.setdefault("viewgraphGanttConfigVO", None)
897
913
  data.setdefault("viewgraphHierarchyConfigVO", None)
914
+ data.setdefault("viewgraphCalendarConfigVO", None)
898
915
  data.setdefault("viewgraphLimitFormula", "")
899
916
  data.setdefault("buttonConfigDTOList", [])
900
917
  if not data.get("viewgraphQuestions"):
901
918
  data["viewgraphQuestions"] = _build_viewgraph_questions(schema, visible_que_ids)
902
919
  if not data.get("viewgraphSorts"):
903
920
  data["viewgraphSorts"] = [{"queId": 0, "beingSortAscend": True, "queType": 8}]
904
- if data.get("viewgraphType") in {"cardView", "boardView", "ganttView", "hierarchyView"}:
921
+ if data.get("viewgraphType") in {"cardView", "boardView", "ganttView", "hierarchyView", "calendarView"}:
905
922
  data["titleQue"] = visible_que_ids[0] if visible_que_ids else None
906
923
  else:
907
924
  data.setdefault("titleQue", None)
@@ -2218,6 +2235,7 @@ def _portal_view_type(store: RunArtifactStore, entity_id: str, view_id: str) ->
2218
2235
  "board": "boardView",
2219
2236
  "gantt": "ganttView",
2220
2237
  "hierarchy": "hierarchyView",
2238
+ "calendar": "calendarView",
2221
2239
  }
2222
2240
  for entity in store.data.get("normalized_solution_spec", {}).get("entities", []):
2223
2241
  if entity["entity_id"] != entity_id:
@@ -2324,36 +2342,106 @@ def _resolve_package_item_insert_index(tag_items: list[dict[str, Any]], ordinal:
2324
2342
  return max(0, min(len(tag_items), ordinal - 1))
2325
2343
 
2326
2344
 
2327
- def _build_viewgraph_questions(schema: dict[str, Any], visible_que_ids: list[int]) -> list[dict[str, Any]]:
2345
+ def _flatten_viewgraph_questions(entries: list[Any] | None) -> dict[int, dict[str, Any]]:
2346
+ """把读面的 viewgraphQuestions(含 subQues/innerQues 嵌套)按 queId 展平。"""
2347
+ flat: dict[int, dict[str, Any]] = {}
2348
+
2349
+ def walk(entry: Any) -> None:
2350
+ if not isinstance(entry, dict):
2351
+ return
2352
+ que_id = entry.get("queId")
2353
+ if isinstance(que_id, int) and que_id not in flat:
2354
+ flat[que_id] = entry
2355
+ for key in ("subQues", "innerQues"):
2356
+ for child in entry.get(key) or []:
2357
+ if isinstance(child, list):
2358
+ for inner in child:
2359
+ walk(inner)
2360
+ else:
2361
+ walk(child)
2362
+
2363
+ for entry in entries or []:
2364
+ if isinstance(entry, list):
2365
+ for inner in entry:
2366
+ walk(inner)
2367
+ else:
2368
+ walk(entry)
2369
+ return flat
2370
+
2371
+
2372
+ def _build_viewgraph_questions(
2373
+ schema: dict[str, Any],
2374
+ visible_que_ids: list[int],
2375
+ *,
2376
+ existing_by_que_id: dict[int, dict[str, Any]] | None = None,
2377
+ ) -> list[dict[str, Any]]:
2378
+ """重建业务列行;更新时原样保留已配置的系统列。
2379
+
2380
+ existing_by_que_id(读面展平行)存在时,以读面行为基底只更新可见性/序号——
2381
+ 保留列级权限(queAuth/beingAddRow/beingDeleteRow/beingEditRow/beingDownload 等),
2382
+ 否则重建会把这些标志归默认值,破坏既有列权限配置。
2383
+ 系统列读面占位对象不写回;existing_by_que_id=None 表示新建。
2384
+ """
2328
2385
  visible_map = {que_id: index for index, que_id in enumerate(visible_que_ids, start=1)}
2386
+ existing = existing_by_que_id or {}
2329
2387
  questions: list[dict[str, Any]] = []
2330
2388
  for base_question in schema.get("baseQues", []):
2331
- question = _build_viewgraph_question(base_question, visible_map, default_auth=3)
2389
+ question = _build_viewgraph_question(base_question, visible_map, default_auth=3, existing=existing_by_que_id)
2332
2390
  if question is not None:
2333
2391
  questions.append(question)
2334
2392
  for line in schema.get("formQues", []):
2335
2393
  for question in line:
2336
- resolved = _build_viewgraph_question(question, visible_map, default_auth=1)
2394
+ resolved = _build_viewgraph_question(question, visible_map, default_auth=1, existing=existing_by_que_id)
2337
2395
  if resolved is not None:
2338
2396
  questions.append(resolved)
2397
+ # 某些系统列只在视图读面出现,不属于表单 schema。
2398
+ emitted_que_ids = _flatten_viewgraph_questions(questions)
2399
+ for que_id, prior in existing.items():
2400
+ if que_id <= 5 and que_id not in emitted_que_ids:
2401
+ preserved = _build_viewgraph_question(prior, visible_map, default_auth=3, existing=existing)
2402
+ if preserved is not None:
2403
+ questions.append(preserved)
2339
2404
  return questions
2340
2405
 
2341
2406
 
2342
- def _build_viewgraph_question(question: dict[str, Any], visible_map: dict[int, int], *, default_auth: int) -> dict[str, Any] | None:
2407
+ def _build_viewgraph_question(
2408
+ question: dict[str, Any],
2409
+ visible_map: dict[int, int],
2410
+ *,
2411
+ default_auth: int,
2412
+ existing: dict[int, dict[str, Any]] | None = None,
2413
+ ) -> dict[str, Any] | None:
2343
2414
  que_id = question.get("queId")
2344
2415
  if que_id is None:
2345
2416
  return None
2417
+ if isinstance(que_id, int) and que_id <= 5 and existing is not None:
2418
+ prior = existing.get(que_id)
2419
+ # 后端会生成未落库的系统字段占位对象;只有落库配置才带这些列状态。
2420
+ if prior is None or all(prior.get(key) is None for key in (
2421
+ "beingListDisplay", "displayOrdinal", "beingAddRow", "beingEditRow", "beingDeleteRow",
2422
+ )):
2423
+ return None
2424
+ return deepcopy(prior)
2346
2425
  sub_questions = [
2347
2426
  resolved
2348
2427
  for sub_question in question.get("subQuestions", []) or []
2349
- if (resolved := _build_viewgraph_question(sub_question, visible_map, default_auth=1)) is not None
2428
+ if (resolved := _build_viewgraph_question(sub_question, visible_map, default_auth=1, existing=existing)) is not None
2350
2429
  ]
2351
2430
  inner_questions = [
2352
2431
  resolved
2353
2432
  for inner_row in question.get("innerQuestions", []) or []
2354
2433
  for inner_question in inner_row or []
2355
- if (resolved := _build_viewgraph_question(inner_question, visible_map, default_auth=1)) is not None
2434
+ if (resolved := _build_viewgraph_question(inner_question, visible_map, default_auth=1, existing=existing)) is not None
2356
2435
  ]
2436
+ prior = (existing or {}).get(que_id) if isinstance(que_id, int) else None
2437
+ if prior is not None:
2438
+ # 读面行为基底:保留全部列级标志,只更新可见性/序号;嵌套子项同样融合
2439
+ merged = deepcopy(prior)
2440
+ merged["beingListDisplay"] = que_id in visible_map
2441
+ merged["displayOrdinal"] = visible_map.get(que_id, -1)
2442
+ merged["subQues"] = sub_questions
2443
+ merged["innerQues"] = inner_questions
2444
+ return merged
2357
2445
  return {
2358
2446
  "queId": que_id,
2359
2447
  "queTitle": question.get("queTitle"),
@@ -56,6 +56,7 @@ class ViewType(str, Enum):
56
56
  board = "board"
57
57
  gantt = "gantt"
58
58
  hierarchy = "hierarchy"
59
+ calendar = "calendar"
59
60
 
60
61
 
61
62
  class ChartType(str, Enum):