@qingflow-tech/qingflow-cli 1.1.61-beta.5 → 1.1.61

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 (85) hide show
  1. package/README.md +3 -3
  2. package/package.json +1 -1
  3. package/pyproject.toml +1 -2
  4. package/skills/qingflow-cli/SKILL.md +2 -104
  5. package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +2 -2
  6. package/skills/qingflow-cli/reference/builder/30-schema-fields.md +5 -22
  7. package/skills/qingflow-cli/reference/builder/70-portal.md +16 -37
  8. package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +3 -3
  9. package/skills/qingflow-cli/reference/examples/portal/portal_sections_all_types.example.json +7 -6
  10. package/src/qingflow_mcp/app_form_compat.py +0 -3
  11. package/src/qingflow_mcp/app_form_validator.py +20 -89
  12. package/src/qingflow_mcp/builder_facade/app_form.py +112 -629
  13. package/src/qingflow_mcp/builder_facade/models.py +13 -164
  14. package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +1 -15
  15. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +38 -112
  16. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +8 -31
  17. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +10 -6
  18. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +10 -33
  19. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +10 -6
  20. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +18 -11
  21. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +8 -4
  22. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +8 -4
  23. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +10 -6
  24. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +8 -4
  25. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +8 -4
  26. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +8 -4
  27. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +10 -6
  28. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +8 -4
  29. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +10 -6
  30. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +8 -4
  31. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +12 -8
  32. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +15 -73
  33. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +8 -4
  34. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +8 -4
  35. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +29 -90
  36. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +8 -4
  37. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/formula.json +0 -43
  38. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +19 -27
  39. package/src/qingflow_mcp/builder_facade/service.py +72 -1143
  40. package/src/qingflow_mcp/cli/commands/builder.py +5 -114
  41. package/src/qingflow_mcp/cli/main.py +1 -34
  42. package/src/qingflow_mcp/cloud_context.py +1 -16
  43. package/src/qingflow_mcp/public_surface.py +8 -69
  44. package/src/qingflow_mcp/response_trim.py +1 -2
  45. package/src/qingflow_mcp/server.py +6 -12
  46. package/src/qingflow_mcp/server_app_builder.py +6 -41
  47. package/src/qingflow_mcp/server_app_user.py +5 -9
  48. package/src/qingflow_mcp/server_cloud.py +7 -43
  49. package/src/qingflow_mcp/solution/compiler/icon_utils.py +0 -73
  50. package/src/qingflow_mcp/solution/executor.py +2 -5
  51. package/src/qingflow_mcp/tools/ai_builder_tools.py +12 -110
  52. package/src/qingflow_mcp/tools/portal_tools.py +54 -4
  53. package/src/qingflow_mcp/tools/solution_tools.py +0 -2
  54. package/skills/qingflow-cli/reference/examples/portal/portal_apply_v1alpha1.example.json +0 -100
  55. package/src/qingflow_mcp/builder_facade/portals_schema.py +0 -397
  56. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_content_classification.json +0 -146
  57. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_content_extraction.json +0 -146
  58. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_content_generation.json +0 -146
  59. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_document_parsing.json +0 -146
  60. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_image_generation.json +0 -146
  61. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_image_recognition.json +0 -146
  62. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_machine_translation.json +0 -146
  63. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_text_summary.json +0 -146
  64. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date_duration.json +0 -146
  65. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/embed_field.json +0 -146
  66. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/image_select.json +0 -146
  67. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/location.json +0 -146
  68. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ocr.json +0 -146
  69. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/rich_text.json +0 -146
  70. package/src/qingflow_mcp/builder_facade/schemas/portals/manifest.json +0 -59
  71. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/complete.json +0 -1397
  72. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/carousel.json +0 -193
  73. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/chart.json +0 -643
  74. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/favourite.json +0 -203
  75. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/filter.json +0 -268
  76. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/grid.json +0 -265
  77. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/link.json +0 -131
  78. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/task.json +0 -199
  79. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/text.json +0 -123
  80. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/view.json +0 -196
  81. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/summary.json +0 -800
  82. package/src/qingflow_mcp/tool_catalog.py +0 -387
  83. package/src/qingflow_mcp/tool_catalog_i18n.py +0 -185
  84. package/src/qingflow_mcp/tool_policy.py +0 -197
  85. package/src/qingflow_mcp/tools/portal_writer.py +0 -59
@@ -6,15 +6,11 @@ from typing import Any
6
6
 
7
7
  import anyio
8
8
  import uvicorn
9
+ from mcp.server.fastmcp import FastMCP
10
+
9
11
  from .backend_client import BackendClient
10
12
  from .cloud_auth import CloudAuthBroker
11
- from .cloud_context import (
12
- CloudMcpContext,
13
- current_call_platform,
14
- current_cloud_context,
15
- current_http_tool_policy,
16
- current_tool_policy_header,
17
- )
13
+ from .cloud_context import CloudMcpContext, current_call_platform, current_cloud_context
18
14
  from .cloud_oauth_proxy import (
19
15
  CloudOAuthProxyConfig,
20
16
  OAuthAccessTokenResolver,
@@ -25,8 +21,6 @@ from .config import get_cloud_http_allowed_hosts, get_cloud_http_host, get_cloud
25
21
  from .errors import QingflowApiError, is_auth_like_error
26
22
  from .server_app_builder import build_builder_server
27
23
  from .server_app_user import build_user_server
28
- from .tool_catalog import combined_server_bindings
29
- from .tool_policy import PolicyAwareFastMCP, configure_tool_policy, parse_http_tool_policy
30
24
 
31
25
 
32
26
  BUILDER_TOOL_PREFIX = "builder_"
@@ -170,18 +164,12 @@ class CloudMcpAuthApp:
170
164
  cloud_context.token,
171
165
  )
172
166
 
173
- tool_policy_header = _extract_header(scope, "x-qingflow-tool-policy")
174
- http_tool_policy = parse_http_tool_policy(tool_policy_header, bytes(scope.get("query_string") or b""))
175
167
  context_token = current_cloud_context.set(cloud_context)
176
168
  call_platform_token = current_call_platform.set(call_platform)
177
- tool_policy_token = current_tool_policy_header.set(tool_policy_header)
178
- http_tool_policy_token = current_http_tool_policy.set(http_tool_policy)
179
169
  try:
180
170
  scope = _ensure_streamable_http_accept(scope)
181
171
  await self._app(scope, receive, send)
182
172
  finally:
183
- current_http_tool_policy.reset(http_tool_policy_token)
184
- current_tool_policy_header.reset(tool_policy_token)
185
173
  current_call_platform.reset(call_platform_token)
186
174
  current_cloud_context.reset(context_token)
187
175
 
@@ -409,29 +397,16 @@ def build_cloud_app() -> CloudMcpAuthApp:
409
397
  )
410
398
 
411
399
 
412
- def build_combined_server() -> PolicyAwareFastMCP:
400
+ def build_combined_server() -> FastMCP:
413
401
  server = build_user_server(stateless_http=True)
414
402
  builder_server = build_builder_server()
415
- user_tool_names = set(server._tool_manager._tools)
416
- builder_tool_names = set(builder_server._tool_manager._tools)
417
403
  server._mcp_server.name = "Qingflow Combined MCP"
418
404
  _register_prefixed_builder_tools(server, builder_server)
419
405
  _set_combined_instructions(server, builder_server)
420
- return configure_tool_policy(
421
- server,
422
- combined_server_bindings(
423
- user_tool_names,
424
- builder_tool_names,
425
- builder_prefix=BUILDER_TOOL_PREFIX,
426
- ),
427
- source="request",
428
- )
406
+ return server
429
407
 
430
408
 
431
- def _register_prefixed_builder_tools(
432
- target_server: PolicyAwareFastMCP,
433
- builder_server: PolicyAwareFastMCP,
434
- ) -> None:
409
+ def _register_prefixed_builder_tools(target_server: FastMCP, builder_server: FastMCP) -> None:
435
410
  for original_name, tool in builder_server._tool_manager._tools.items():
436
411
  target_server._tool_manager.add_tool(
437
412
  tool.fn,
@@ -444,10 +419,7 @@ def _register_prefixed_builder_tools(
444
419
  )
445
420
 
446
421
 
447
- def _set_combined_instructions(
448
- user_server: PolicyAwareFastMCP,
449
- builder_server: PolicyAwareFastMCP,
450
- ) -> None:
422
+ def _set_combined_instructions(user_server: FastMCP, builder_server: FastMCP) -> None:
451
423
  user_instructions = user_server.instructions or ""
452
424
  builder_instructions = builder_server.instructions or ""
453
425
  user_server._mcp_server.instructions = f"""Use this combined Qingflow MCP server for both user operational workflows and builder workflows.
@@ -482,14 +454,6 @@ def _extract_host(scope: dict[str, Any]) -> str:
482
454
  return ""
483
455
 
484
456
 
485
- def _extract_header(scope: dict[str, Any], name: str) -> str | None:
486
- expected = name.lower().encode("latin1")
487
- for key, value in scope.get("headers", []):
488
- if key.lower() == expected:
489
- return value.decode("utf-8", errors="replace")
490
- return None
491
-
492
-
493
457
  async def _read_request_body(
494
458
  receive: Callable[[], Awaitable[dict[str, Any]]],
495
459
  limit: int,
@@ -312,13 +312,6 @@ def parse_workspace_icon(value: str | None) -> tuple[str | None, str | None, str
312
312
  stripped = str(value).strip()
313
313
  if not stripped:
314
314
  return None, None, None
315
- if _looks_like_icon_url(stripped):
316
- # URL 形态旧图标(如 https://…/dashboard_security.png)不是目录图标:目录名与
317
- # 色均无法表达。返回 (None, None, None) 而不是按关键字把 URL 归一成假目录名
318
- # ('dashboard' 命中 → 'view-grid')——后者经编译重编码会销毁存量 URL 图标,
319
- # 并派生出不存在的默认色(r5 实测 50 门户 color diff 根因)。声明面按「未表达」
320
- # 处理(读面 gap 记账);更新语义 apply 不触碰未指定图标,URL 原样存活。
321
- return None, None, None
322
315
  if _looks_like_icon_json(stripped):
323
316
  try:
324
317
  payload = json.loads(stripped)
@@ -331,11 +324,6 @@ def parse_workspace_icon(value: str | None) -> tuple[str | None, str | None, str
331
324
  return normalize_workspace_icon_name(stripped), None, None
332
325
 
333
326
 
334
- def _looks_like_icon_url(value: str) -> bool:
335
- lowered = value.strip().lower()
336
- return lowered.startswith(("http://", "https://", "//"))
337
-
338
-
339
327
  def workspace_icon_config(value: str | None) -> dict[str, str | None]:
340
328
  icon_name, icon_color, icon_text = parse_workspace_icon(value)
341
329
  raw = str(value).strip() if value not in (None, "") else None
@@ -454,67 +442,6 @@ def encode_workspace_icon_with_defaults(
454
442
  )
455
443
 
456
444
 
457
- def update_workspace_icon_from_existing(
458
- *,
459
- icon: str | None,
460
- color: str | None,
461
- title: str,
462
- existing_icon: str | None,
463
- fallback_icon_name: str | None = None,
464
- ) -> str:
465
- """更新语义的 dashIcon 重编码(round-trip 保真)。
466
-
467
- 后端 updateDashBaseInfo 对 dashIcon 无校验直接透传落库;生产存量存在三类非目录形态:
468
- 文字图标(iconName 空 + iconText=名字首字)、旧版 URL 图标(icon 键携带 URL)、
469
- 空(iconName/iconText 均空)。encode_workspace_icon_with_defaults 的 fallback 语义
470
- (为新建资源合成目录图标)会把「声明 icon 未指定」的存量形态重写为 fallback 图标。
471
-
472
- 更新语义规则:
473
- - 声明 icon 显式指定且与存量 iconName 归一后一致 → JSON 级最小 patch:仅当声明
474
- 显式指定了不同 color 才改写 iconColor,否则存量 JSON 原串透传(byte 级保真);
475
- iconText 等非目录形态键始终保留——生产存量存在 iconName+iconText 同设形态
476
- (r7 全量实测 48 门),全量重编码会把 iconText 清成 null。
477
- - 声明 icon 显式指定且为其他图标 → 走 encode(用户意图覆写);
478
- - 声明 icon 未指定且无存量 → 走 encode_with_defaults(创建/首次合成);
479
- - 声明 icon 未指定且有存量:
480
- - color 也未指定 → 原样保留存量 JSON;
481
- - color 指定 → JSON 级最小 patch(仅替换 iconColor,iconName/iconText/icon URL
482
- 等存量形态键原样保留)。
483
- """
484
- existing_payload = str(existing_icon or "").strip()
485
- if existing_payload and _looks_like_icon_json(existing_payload):
486
- try:
487
- parsed = json.loads(existing_payload)
488
- except ValueError:
489
- parsed = None
490
- if isinstance(parsed, dict):
491
- existing_name, existing_color, _ = parse_workspace_icon(existing_payload)
492
- if icon is None:
493
- if color is None:
494
- return existing_payload
495
- patched = dict(parsed)
496
- patched["iconColor"] = color
497
- return json.dumps(patched, ensure_ascii=False, separators=(",", ":"))
498
- if (
499
- existing_name is not None
500
- and not _looks_like_icon_json(icon)
501
- and not _looks_like_icon_url(icon)
502
- and normalize_workspace_icon_name(icon) == existing_name
503
- ):
504
- if color is None or color == existing_color:
505
- return existing_payload
506
- patched = dict(parsed)
507
- patched["iconColor"] = color
508
- return json.dumps(patched, ensure_ascii=False, separators=(",", ":"))
509
- return encode_workspace_icon_with_defaults(
510
- icon=icon,
511
- color=color,
512
- title=title,
513
- fallback_icon_name=fallback_icon_name,
514
- existing_icon=existing_icon,
515
- )
516
-
517
-
518
445
  def normalize_workspace_icon_name(icon: str | None) -> str | None:
519
446
  if not icon:
520
447
  return None
@@ -10,7 +10,6 @@ from ..tools.app_tools import AppTools
10
10
  from ..tools.navigation_tools import NavigationTools
11
11
  from ..tools.package_tools import PackageTools
12
12
  from ..tools.portal_tools import PortalTools
13
- from ..tools.portal_writer import PortalWriter
14
13
  from ..tools.qingbi_report_tools import QingbiReportTools
15
14
  from ..tools.record_tools import RecordTools
16
15
  from ..tools.role_tools import RoleTools
@@ -40,7 +39,6 @@ class SolutionExecutor:
40
39
  view_writer: "ViewWriter | None" = None,
41
40
  chart_tools: QingbiReportTools,
42
41
  portal_tools: PortalTools,
43
- portal_writer: "PortalWriter | None" = None,
44
42
  navigation_tools: NavigationTools,
45
43
  workflow_tools: Any | None = None,
46
44
  ) -> None:
@@ -53,7 +51,6 @@ class SolutionExecutor:
53
51
  self.view_writer = view_writer
54
52
  self.chart_tools = chart_tools
55
53
  self.portal_tools = portal_tools
56
- self.portal_writer = portal_writer or PortalWriter(portal_tools.sessions, portal_tools.backend)
57
54
  self.navigation_tools = navigation_tools
58
55
  self.workflow_tools = workflow_tools
59
56
 
@@ -627,7 +624,7 @@ class SolutionExecutor:
627
624
  result = portal_artifact
628
625
  if not dash_key:
629
626
  create_payload = self._resolve_portal_payload(compiled.portal_plan["create_payload"], store, base_payload=None)
630
- result = self.portal_writer.create_portal(profile=profile, payload=create_payload)
627
+ result = self.portal_tools.portal_create(profile=profile, payload=create_payload)
631
628
  portal_result = result.get("result") or {}
632
629
  dash_key = portal_result.get("dashKey")
633
630
  store.set_artifact("portal", "result", result)
@@ -658,7 +655,7 @@ class SolutionExecutor:
658
655
  ),
659
656
  )
660
657
  update_payload = self._resolve_portal_payload(compiled.portal_plan["update_payload"], store, base_payload=base_payload)
661
- self.portal_writer.update_portal(profile=profile, dash_key=dash_key, payload=update_payload)
658
+ self.portal_tools.portal_update(profile=profile, dash_key=dash_key, payload=update_payload)
662
659
  self._refresh_portal_artifact(profile=profile, store=store, being_draft=True, artifact_key="draft_result")
663
660
 
664
661
  def _build_navigation(self, profile: str, compiled: CompiledSolution, store: RunArtifactStore) -> None:
@@ -699,36 +699,13 @@ class AiBuilderTools(ToolBase):
699
699
  def portal_list(profile: str = DEFAULT_PROFILE) -> JSONObject:
700
700
  return self.portal_list(profile=profile)
701
701
 
702
- @mcp.tool()
703
- def portal_schema(
704
- schema_version: str | None = None,
705
- schema_kind: str | None = None,
706
- ) -> JSONObject:
707
- """Get a complete portals schema or a focused component schema such as chart, view, grid, or carousel."""
708
- return self.portal_schema(schema_version=schema_version, schema_kind=schema_kind)
709
-
710
- @mcp.tool()
711
- def portal_validate(
712
- declaration: JSONObject | None = None,
713
- schema_version: str | None = None,
714
- ) -> JSONObject:
715
- return self.portal_validate(declaration=declaration or {}, schema_version=schema_version)
716
-
717
702
  @mcp.tool()
718
703
  def portal_get(
719
704
  profile: str = DEFAULT_PROFILE,
720
705
  dash_key: str = "",
721
706
  being_draft: bool = True,
722
- schema_kind: str = "summary",
723
- schema_version: str | None = None,
724
707
  ) -> JSONObject:
725
- return self.portal_get(
726
- profile=profile,
727
- dash_key=dash_key,
728
- being_draft=being_draft,
729
- schema_kind=schema_kind,
730
- schema_version=schema_version,
731
- )
708
+ return self.portal_get(profile=profile, dash_key=dash_key, being_draft=being_draft)
732
709
 
733
710
  @mcp.tool()
734
711
  def view_get(profile: str = DEFAULT_PROFILE, view_key: str = "") -> JSONObject:
@@ -868,10 +845,6 @@ class AiBuilderTools(ToolBase):
868
845
  color: str | None = None,
869
846
  hide_copyright: bool | None = None,
870
847
  dash_global_config: JSONObject | None = None,
871
- global_config: JSONObject | None = None,
872
- style_template_id: int | None = None,
873
- overall_style: JSONObject | None = None,
874
- schema_version: str | None = None,
875
848
  config: JSONObject | None = None,
876
849
  payload: JSONObject | None = None,
877
850
  patch_sections: list[JSONObject] | None = None,
@@ -910,10 +883,6 @@ class AiBuilderTools(ToolBase):
910
883
  color=color,
911
884
  hide_copyright=hide_copyright,
912
885
  dash_global_config=dash_global_config,
913
- global_config=global_config,
914
- style_template_id=style_template_id,
915
- overall_style=overall_style,
916
- schema_version=schema_version,
917
886
  config=config or {},
918
887
  payload=payload,
919
888
  patch_sections=patch_sections,
@@ -2083,58 +2052,16 @@ class AiBuilderTools(ToolBase):
2083
2052
  suggested_next_call={"tool_name": "portal_list", "arguments": {"profile": profile}},
2084
2053
  ))
2085
2054
 
2086
- def portal_get(
2087
- self,
2088
- *,
2089
- profile: str,
2090
- dash_key: str,
2091
- being_draft: bool = True,
2092
- schema_kind: str = "summary",
2093
- schema_version: str | None = None,
2094
- ) -> JSONObject:
2095
- """读取门户详情;schema_kind=summary|complete 返回 canonical 声明,raw 为原始归一响应。"""
2096
- normalized_args = {"dash_key": dash_key, "being_draft": being_draft, "schema_kind": schema_kind, "schema_version": schema_version}
2055
+ def portal_get(self, *, profile: str, dash_key: str, being_draft: bool = True) -> JSONObject:
2056
+ """执行门户相关逻辑。"""
2057
+ normalized_args = {"dash_key": dash_key, "being_draft": being_draft}
2097
2058
  return _publicize_package_fields(_safe_tool_call(
2098
- lambda: self._facade.portal_get(
2099
- profile=profile,
2100
- dash_key=dash_key,
2101
- being_draft=being_draft,
2102
- schema_kind=schema_kind,
2103
- schema_version=schema_version,
2104
- ),
2059
+ lambda: self._facade.portal_get(profile=profile, dash_key=dash_key, being_draft=being_draft),
2105
2060
  error_code="PORTAL_GET_FAILED",
2106
2061
  normalized_args=normalized_args,
2107
2062
  suggested_next_call={"tool_name": "portal_get", "arguments": {"profile": profile, **normalized_args}},
2108
2063
  ))
2109
2064
 
2110
- def portal_schema(
2111
- self,
2112
- *,
2113
- schema_version: str | None = None,
2114
- schema_kind: str | None = None,
2115
- ) -> JSONObject:
2116
- """读取版本化门户声明 schema(纯离线)。"""
2117
- return _publicize_package_fields(_safe_tool_call(
2118
- lambda: self._facade.portal_schema(schema_version=schema_version, schema_kind=schema_kind),
2119
- error_code="PORTAL_SCHEMA_FAILED",
2120
- normalized_args={"schema_version": schema_version, "schema_kind": schema_kind},
2121
- suggested_next_call={"tool_name": "portal_schema", "arguments": {"schema_kind": "summary"}},
2122
- ))
2123
-
2124
- def portal_validate(
2125
- self,
2126
- *,
2127
- declaration: JSONObject,
2128
- schema_version: str | None = None,
2129
- ) -> JSONObject:
2130
- """纯离线校验一条门户声明(不访问后端)。"""
2131
- return _publicize_package_fields(_safe_tool_call(
2132
- lambda: self._facade.portal_validate(declaration=declaration, schema_version=schema_version),
2133
- error_code="PORTAL_VALIDATE_FAILED",
2134
- normalized_args={"schema_version": schema_version},
2135
- suggested_next_call={"tool_name": "portal_schema", "arguments": {"schema_kind": "complete"}},
2136
- ))
2137
-
2138
2065
  def portal_read_summary(self, *, profile: str, dash_key: str, being_draft: bool = True) -> JSONObject:
2139
2066
  """执行门户相关逻辑。"""
2140
2067
  normalized_args = {"dash_key": dash_key, "being_draft": being_draft}
@@ -4306,10 +4233,6 @@ class AiBuilderTools(ToolBase):
4306
4233
  color: str | None = None,
4307
4234
  hide_copyright: bool | None = None,
4308
4235
  dash_global_config: JSONObject | None = None,
4309
- global_config: JSONObject | None = None,
4310
- style_template_id: int | None = None,
4311
- overall_style: JSONObject | None = None,
4312
- schema_version: str | None = None,
4313
4236
  config: JSONObject | None = None,
4314
4237
  payload: JSONObject | None = None,
4315
4238
  patch_sections: list[JSONObject] | None = None,
@@ -4361,14 +4284,6 @@ class AiBuilderTools(ToolBase):
4361
4284
  request_payload["hide_copyright"] = hide_copyright
4362
4285
  if dash_global_config is not None:
4363
4286
  request_payload["dash_global_config"] = dash_global_config
4364
- if global_config is not None:
4365
- request_payload["global_config"] = global_config
4366
- if style_template_id is not None:
4367
- request_payload["style_template_id"] = style_template_id
4368
- if overall_style is not None:
4369
- request_payload["overall_style"] = overall_style
4370
- if schema_version is not None:
4371
- request_payload["schema_version"] = schema_version
4372
4287
  if config:
4373
4288
  merged_config = dict(request_payload.get("config") or {}) if isinstance(request_payload.get("config"), dict) else {}
4374
4289
  merged_config.update(config)
@@ -4400,24 +4315,16 @@ class AiBuilderTools(ToolBase):
4400
4315
  ))
4401
4316
  normalized_args = request.model_dump(mode="json")
4402
4317
  normalized_args["package_id"] = normalized_args.pop("package_tag_id", package_id)
4403
- portal_creating = not bool(str(request.dash_key or "").strip())
4404
- # icon/color 目录校验仅对创建语义生效(CLI 防呆,后端 createDash 亦无校验):
4405
- # 更新语义(dash_key 定位)后端 updateDashBaseInfo 对 dashIcon 无目录校验直接
4406
- # 透传落库,生产存量存在旧版 hex 色值(#13c2c2 等)——提取透传的重放声明会被
4407
- # 目录校验拒之门外(WORKSPACE_ICON_COLOR_NOT_FOUND),故更新语义不做目录校验。
4408
- if portal_creating:
4409
- icon_failure = _validate_workspace_icon_for_builder(
4410
- tool_name="portal_apply",
4411
- icon=str(request.icon or ""),
4412
- color=str(request.color or ""),
4413
- creating=True,
4414
- )
4415
- else:
4416
- icon_failure = None
4318
+ icon_failure = _validate_workspace_icon_for_builder(
4319
+ tool_name="portal_apply",
4320
+ icon=str(request.icon or ""),
4321
+ color=str(request.color or ""),
4322
+ creating=not bool(str(request.dash_key or "").strip()),
4323
+ )
4417
4324
  if icon_failure is not None:
4418
4325
  return self._attach_apply_envelope(profile, "portal_apply", icon_failure)
4419
4326
  result = _publicize_package_fields(_safe_tool_call(
4420
- lambda: self._facade.portal_apply(profile=profile, request=request, schema_version=schema_version),
4327
+ lambda: self._facade.portal_apply(profile=profile, request=request),
4421
4328
  error_code="PORTAL_APPLY_FAILED",
4422
4329
  normalized_args=normalized_args,
4423
4330
  suggested_next_call={"tool_name": "portal_apply", "arguments": {"profile": profile, **normalized_args}},
@@ -5831,11 +5738,6 @@ def _publicize_package_fields(value):
5831
5738
  }
5832
5739
  public: JSONObject = {}
5833
5740
  for key, item in value.items():
5834
- if key == "declaration":
5835
- # canonical 声明保持 schema 契约命名(package_tag_id 等),不做公开名重命名,
5836
- # 否则 get 投影的 declaration 无法直接回喂 validate/apply(round-trip 破坏)。
5837
- public[key] = item
5838
- continue
5839
5741
  public_key = key_map.get(key, key)
5840
5742
  public[public_key] = _publicize_package_fields(item)
5841
5743
  return public
@@ -14,10 +14,8 @@ class PortalTools(ToolBase):
14
14
  类型:门户配置工具。
15
15
  主要职责:
16
16
  1. 查询门户列表与详情;
17
- 2. 执行门户发布与删除操作。
18
-
19
- 创建/更新写入已下沉 ``PortalWriter``(tools/portal_writer.py),
20
- 公开层唯一入口是 ``builder portal apply`` / ``portal_apply``。
17
+ 2. 维护门户基础信息与配置;
18
+ 3. 执行门户发布与删除操作。
21
19
  """
22
20
 
23
21
  def register(self, mcp: FastMCP) -> None:
@@ -34,6 +32,18 @@ class PortalTools(ToolBase):
34
32
  def portal_get_base_info(profile: str = DEFAULT_PROFILE, dash_key: str = "") -> JSONObject:
35
33
  return self.portal_get_base_info(profile=profile, dash_key=dash_key)
36
34
 
35
+ @mcp.tool()
36
+ def portal_create(profile: str = DEFAULT_PROFILE, payload: JSONObject | None = None) -> JSONObject:
37
+ return self.portal_create(profile=profile, payload=payload or {})
38
+
39
+ @mcp.tool(description=self._high_risk_tool_description(operation="update", target="portal base settings"))
40
+ def portal_update_base_info(profile: str = DEFAULT_PROFILE, dash_key: str = "", payload: JSONObject | None = None) -> JSONObject:
41
+ return self.portal_update_base_info(profile=profile, dash_key=dash_key, payload=payload or {})
42
+
43
+ @mcp.tool(description=self._high_risk_tool_description(operation="update", target="portal configuration"))
44
+ def portal_update(profile: str = DEFAULT_PROFILE, dash_key: str = "", payload: JSONObject | None = None) -> JSONObject:
45
+ return self.portal_update(profile=profile, dash_key=dash_key, payload=payload or {})
46
+
37
47
  @mcp.tool(description=self._high_risk_tool_description(operation="delete", target="portal configuration"))
38
48
  def portal_delete(profile: str = DEFAULT_PROFILE, dash_key: str = "") -> JSONObject:
39
49
  return self.portal_delete(profile=profile, dash_key=dash_key)
@@ -60,6 +70,16 @@ class PortalTools(ToolBase):
60
70
 
61
71
  return self._run(profile, runner, tool_name='门户详情')
62
72
 
73
+ def portal_create(self, *, profile: str, payload: JSONObject) -> JSONObject:
74
+ """执行门户相关逻辑。"""
75
+ body = self._require_dict(payload)
76
+
77
+ def runner(session_profile, context):
78
+ result = self.backend.request("POST", context, "/dash", json_body=body)
79
+ return {"profile": profile, "ws_id": session_profile.selected_ws_id, "result": result}
80
+
81
+ return self._run(profile, runner, tool_name='创建门户')
82
+
63
83
  def portal_get_base_info(self, *, profile: str, dash_key: str) -> JSONObject:
64
84
  """执行门户相关逻辑。"""
65
85
  self._require_dash_key(dash_key)
@@ -99,6 +119,36 @@ class PortalTools(ToolBase):
99
119
 
100
120
  return self._run(profile, runner, tool_name='门户基础信息')
101
121
 
122
+ def portal_update_base_info(self, *, profile: str, dash_key: str, payload: JSONObject) -> JSONObject:
123
+ """执行门户相关逻辑。"""
124
+ self._require_dash_key(dash_key)
125
+ body = self._require_dict(payload)
126
+
127
+ def runner(session_profile, context):
128
+ result = self.backend.request("POST", context, f"/dash/{dash_key}/baseInfo", json_body=body)
129
+ return self._attach_human_review_notice(
130
+ {"profile": profile, "ws_id": session_profile.selected_ws_id, "dash_key": dash_key, "result": result},
131
+ operation="update",
132
+ target="portal base settings",
133
+ )
134
+
135
+ return self._run(profile, runner, tool_name='更新门户基础信息')
136
+
137
+ def portal_update(self, *, profile: str, dash_key: str, payload: JSONObject) -> JSONObject:
138
+ """执行门户相关逻辑。"""
139
+ self._require_dash_key(dash_key)
140
+ body = self._require_dict(payload)
141
+
142
+ def runner(session_profile, context):
143
+ result = self.backend.request("POST", context, f"/dash/{dash_key}", json_body=body)
144
+ return self._attach_human_review_notice(
145
+ {"profile": profile, "ws_id": session_profile.selected_ws_id, "dash_key": dash_key, "result": result},
146
+ operation="update",
147
+ target="portal configuration",
148
+ )
149
+
150
+ return self._run(profile, runner, tool_name='更新门户配置')
151
+
102
152
  def portal_delete(self, *, profile: str, dash_key: str) -> JSONObject:
103
153
  """执行门户相关逻辑。"""
104
154
  self._require_dash_key(dash_key)
@@ -39,7 +39,6 @@ from .record_tools import RecordTools
39
39
  from .role_tools import RoleTools
40
40
  from .view_tools import ViewTools
41
41
  from .view_writer import ViewWriter
42
- from .portal_writer import PortalWriter
43
42
  from .workspace_tools import WorkspaceTools
44
43
 
45
44
  STAGED_BUILD_MODES = {"preflight", "plan", "apply", "repair"}
@@ -1432,7 +1431,6 @@ class SolutionTools(ToolBase):
1432
1431
  view_writer=ViewWriter(self.sessions, self.backend),
1433
1432
  chart_tools=QingbiReportTools(self.sessions, self.backend),
1434
1433
  portal_tools=PortalTools(self.sessions, self.backend),
1435
- portal_writer=PortalWriter(self.sessions, self.backend),
1436
1434
  navigation_tools=NavigationTools(self.sessions, self.backend),
1437
1435
  )
1438
1436
 
@@ -1,100 +0,0 @@
1
- {
2
- "schema_version": "v1alpha1",
3
- "name": "业务工作台",
4
- "package_tag_id": 2030703,
5
- "icon": "view-grid",
6
- "color": "#2468F2",
7
- "hide_copyright": false,
8
- "global_config": {
9
- "auto_refresh": false,
10
- "interval": 60,
11
- "export": true
12
- },
13
- "visibility": {
14
- "mode": "workspace",
15
- "external_mode": "not"
16
- },
17
- "layout_preset": "auto",
18
- "sections": [
19
- {
20
- "title": "业务入口",
21
- "source_type": "grid",
22
- "position": {
23
- "pc": { "x": 0, "y": 0, "cols": 24, "rows": 4 }
24
- },
25
- "config": {
26
- "gridTitle": "业务入口",
27
- "beingShowTitle": true,
28
- "items": [
29
- { "type": 1, "jumpMode": 1, "linkAppKey": "APP_KEY_DEVICE", "linkFormType": 1, "title": "设备台账" },
30
- { "type": 1, "jumpMode": 1, "linkAppKey": "APP_KEY_WORKORDER", "linkFormType": 1, "title": "工单管理" }
31
- ]
32
- }
33
- },
34
- {
35
- "title": "设备总数",
36
- "source_type": "chart",
37
- "role": "metric",
38
- "position": {
39
- "pc": { "x": 0, "y": 4, "cols": 6, "rows": 5 }
40
- },
41
- "chart": {
42
- "app_key": "APP_KEY_DEVICE",
43
- "name": "设备总数",
44
- "chart_type": "target",
45
- "metric": "count(*)"
46
- }
47
- },
48
- {
49
- "title": "异常设备数",
50
- "source_type": "chart",
51
- "role": "metric",
52
- "position": {
53
- "pc": { "x": 6, "y": 4, "cols": 6, "rows": 5 }
54
- },
55
- "chart": {
56
- "app_key": "APP_KEY_DEVICE",
57
- "name": "异常设备数",
58
- "chart_type": "target",
59
- "metric": "count(*)",
60
- "filters": [
61
- { "field_name": "状态", "operator": "eq", "values": ["异常"] }
62
- ]
63
- }
64
- },
65
- {
66
- "title": "工单趋势",
67
- "source_type": "chart",
68
- "position": {
69
- "pc": { "x": 0, "y": 9, "cols": 12, "rows": 7 }
70
- },
71
- "chart": {
72
- "app_key": "APP_KEY_WORKORDER",
73
- "name": "工单趋势",
74
- "chart_type": "line",
75
- "metric": "count(*)",
76
- "group_by": ["创建日期"]
77
- }
78
- },
79
- {
80
- "title": "工单类型分布",
81
- "source_type": "chart",
82
- "position": {
83
- "pc": { "x": 12, "y": 9, "cols": 12, "rows": 7 }
84
- },
85
- "chart_ref": { "app_key": "APP_KEY_WORKORDER", "chart_name": "工单类型分布" }
86
- },
87
- {
88
- "title": "异常设备明细",
89
- "source_type": "view",
90
- "position": {
91
- "pc": { "x": 0, "y": 16, "cols": 24, "rows": 11 }
92
- },
93
- "view_ref": {
94
- "app_key": "APP_KEY_DEVICE",
95
- "view_key": "VIEW_KEY_ABNORMAL_DEVICES"
96
- },
97
- "view_display": { "show_title": true }
98
- }
99
- ]
100
- }