@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.
- package/README.md +3 -3
- package/package.json +1 -1
- package/pyproject.toml +1 -2
- package/skills/qingflow-cli/SKILL.md +2 -104
- package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +2 -2
- package/skills/qingflow-cli/reference/builder/30-schema-fields.md +5 -22
- package/skills/qingflow-cli/reference/builder/70-portal.md +16 -37
- package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +3 -3
- package/skills/qingflow-cli/reference/examples/portal/portal_sections_all_types.example.json +7 -6
- package/src/qingflow_mcp/app_form_compat.py +0 -3
- package/src/qingflow_mcp/app_form_validator.py +20 -89
- package/src/qingflow_mcp/builder_facade/app_form.py +112 -629
- package/src/qingflow_mcp/builder_facade/models.py +13 -164
- package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +1 -15
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +38 -112
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +8 -31
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +10 -6
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +10 -33
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +10 -6
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +18 -11
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +10 -6
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +10 -6
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +10 -6
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +12 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +15 -73
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +29 -90
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/formula.json +0 -43
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +19 -27
- package/src/qingflow_mcp/builder_facade/service.py +72 -1143
- package/src/qingflow_mcp/cli/commands/builder.py +5 -114
- package/src/qingflow_mcp/cli/main.py +1 -34
- package/src/qingflow_mcp/cloud_context.py +1 -16
- package/src/qingflow_mcp/public_surface.py +8 -69
- package/src/qingflow_mcp/response_trim.py +1 -2
- package/src/qingflow_mcp/server.py +6 -12
- package/src/qingflow_mcp/server_app_builder.py +6 -41
- package/src/qingflow_mcp/server_app_user.py +5 -9
- package/src/qingflow_mcp/server_cloud.py +7 -43
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +0 -73
- package/src/qingflow_mcp/solution/executor.py +2 -5
- package/src/qingflow_mcp/tools/ai_builder_tools.py +12 -110
- package/src/qingflow_mcp/tools/portal_tools.py +54 -4
- package/src/qingflow_mcp/tools/solution_tools.py +0 -2
- package/skills/qingflow-cli/reference/examples/portal/portal_apply_v1alpha1.example.json +0 -100
- package/src/qingflow_mcp/builder_facade/portals_schema.py +0 -397
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_content_classification.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_content_extraction.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_content_generation.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_document_parsing.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_image_generation.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_image_recognition.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_machine_translation.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_text_summary.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date_duration.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/embed_field.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/image_select.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/location.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ocr.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/rich_text.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/portals/manifest.json +0 -59
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/complete.json +0 -1397
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/carousel.json +0 -193
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/chart.json +0 -643
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/favourite.json +0 -203
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/filter.json +0 -268
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/grid.json +0 -265
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/link.json +0 -131
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/task.json +0 -199
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/text.json +0 -123
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/view.json +0 -196
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/summary.json +0 -800
- package/src/qingflow_mcp/tool_catalog.py +0 -387
- package/src/qingflow_mcp/tool_catalog_i18n.py +0 -185
- package/src/qingflow_mcp/tool_policy.py +0 -197
- package/src/qingflow_mcp/tools/portal_writer.py +0 -59
|
@@ -26,7 +26,7 @@ from ..resource_uri import validate_positive_resource_id, validate_resource_iden
|
|
|
26
26
|
from ..solution.build_assembly_store import BuildAssemblyStore, default_artifacts, default_manifest
|
|
27
27
|
from ..solution.compiler.chart_compiler import qingbi_workspace_visible_auth
|
|
28
28
|
from ..solution.compiler.form_compiler import build_question, default_form_payload, default_member_auth
|
|
29
|
-
from ..solution.compiler.icon_utils import encode_workspace_icon_with_defaults,
|
|
29
|
+
from ..solution.compiler.icon_utils import encode_workspace_icon_with_defaults, workspace_icon_config
|
|
30
30
|
from ..solution.compiler.view_compiler import VIEW_TYPE_MAP
|
|
31
31
|
from ..solution.executor import _build_viewgraph_questions, _compact_dict, _flatten_viewgraph_questions, extract_field_map
|
|
32
32
|
from ..solution.spec_models import FieldType, FormLayoutRowSpec, FormLayoutSectionSpec, ViewSpec
|
|
@@ -35,7 +35,6 @@ from ..tools.custom_button_tools import CustomButtonTools
|
|
|
35
35
|
from ..tools.directory_tools import DirectoryTools
|
|
36
36
|
from ..tools.package_tools import PackageTools
|
|
37
37
|
from ..tools.portal_tools import PortalTools
|
|
38
|
-
from ..tools.portal_writer import PortalWriter
|
|
39
38
|
from ..tools.qingbi_report_tools import QingbiReportTools
|
|
40
39
|
from ..tools.role_tools import RoleTools
|
|
41
40
|
from ..tools.solution_tools import SolutionTools
|
|
@@ -46,7 +45,6 @@ from . import workflow_spec as workflow_spec_api
|
|
|
46
45
|
from .button_style_catalog import button_style_catalog_payload
|
|
47
46
|
from ..tools.view_writer import ViewWriter
|
|
48
47
|
from .views_schema import ViewsContractError, compact_none_values, views_schema_registry
|
|
49
|
-
from .portals_schema import PortalsContractError, PortalsSchemaContext, portals_schema_registry
|
|
50
48
|
from .models import (
|
|
51
49
|
AppChartsReadResponse,
|
|
52
50
|
AppFieldsReadResponse,
|
|
@@ -83,7 +81,6 @@ from .models import (
|
|
|
83
81
|
LayoutSectionPatch,
|
|
84
82
|
LayoutPreset,
|
|
85
83
|
PortalApplyRequest,
|
|
86
|
-
public_portal_apply_payload,
|
|
87
84
|
PortalGetResponse,
|
|
88
85
|
PortalListResponse,
|
|
89
86
|
PortalReadSummaryResponse,
|
|
@@ -166,16 +163,9 @@ FIELD_TYPE_TO_QUESTION_TYPE: dict[str, int] = {
|
|
|
166
163
|
FieldType.relation.value: 25,
|
|
167
164
|
}
|
|
168
165
|
|
|
169
|
-
# code_block / q_linker 输出目标字段类型。后端写链无类型白名单(FormSvcImpl 原样
|
|
170
|
-
# 落库),运行时 DataSvcImpl.getAnswerValueByString:attachment 按拆分 URL 写入、
|
|
171
|
-
# subtable 整表组装(均有专门分支),rich_text/data_relation 走 Question.canImport
|
|
172
|
-
# 兜底按字符串写入(不报错)——四者为存量合法配置,与 validator 侧白名单同口径。
|
|
173
|
-
# r10 再放行 link/member(存量实证:92nl6blfk402 link×2、ab9fjvj33001 member;
|
|
174
|
-
# 运行时同为字符串写入路径)。
|
|
175
166
|
INTEGRATION_OUTPUT_TARGET_FIELD_TYPES: tuple[str, ...] = (
|
|
176
167
|
FieldType.text.value,
|
|
177
168
|
FieldType.long_text.value,
|
|
178
|
-
"rich_text",
|
|
179
169
|
FieldType.number.value,
|
|
180
170
|
FieldType.amount.value,
|
|
181
171
|
FieldType.date.value,
|
|
@@ -183,11 +173,6 @@ INTEGRATION_OUTPUT_TARGET_FIELD_TYPES: tuple[str, ...] = (
|
|
|
183
173
|
FieldType.single_select.value,
|
|
184
174
|
FieldType.multi_select.value,
|
|
185
175
|
FieldType.boolean.value,
|
|
186
|
-
FieldType.attachment.value,
|
|
187
|
-
"data_relation",
|
|
188
|
-
FieldType.subtable.value,
|
|
189
|
-
"link",
|
|
190
|
-
"member",
|
|
191
176
|
)
|
|
192
177
|
|
|
193
178
|
MATCH_TYPE_ACCURACY = 1
|
|
@@ -198,9 +183,6 @@ JUDGE_INCLUDE = 2
|
|
|
198
183
|
JUDGE_GREATER_OR_EQUAL = 5
|
|
199
184
|
JUDGE_LESS_OR_EQUAL = 7
|
|
200
185
|
JUDGE_EQUAL_ANY = 9
|
|
201
|
-
# 成员/部门归属判定(Constants.JUDGE_BELONG_TO):轮播筛选存量数据出现,后端公式引擎
|
|
202
|
-
# JUDGE_FUNCTION[13]="JUDGE_BELONG_TO" 全支持,声明侧以 belongs_to 表达保真回写。
|
|
203
|
-
JUDGE_BELONG_TO = 13
|
|
204
186
|
JUDGE_FUZZY_MATCH = 19
|
|
205
187
|
JUDGE_INCLUDE_ANY = 20
|
|
206
188
|
DEFAULT_TYPE_RELATION = 2
|
|
@@ -315,7 +297,6 @@ class AiBuilderFacade:
|
|
|
315
297
|
self._view_writer = ViewWriter(views.sessions, views.backend)
|
|
316
298
|
self.workflows = workflows
|
|
317
299
|
self.portals = portals
|
|
318
|
-
self._portal_writer = PortalWriter(portals.sessions, portals.backend)
|
|
319
300
|
self.charts = charts
|
|
320
301
|
self.roles = roles
|
|
321
302
|
self.directory = directory
|
|
@@ -3101,11 +3082,6 @@ class AiBuilderFacade:
|
|
|
3101
3082
|
# 这类失败降级为非阻断——只有 uid/dept_id 直通也覆盖不了的引用才真正阻断。
|
|
3102
3083
|
_SOFT_RESOLUTION_KINDS = {"member_email", "member_name", "department_name", "role", "external_member_email"}
|
|
3103
3084
|
_NOT_FOUND_RESOLUTION_CODES = {"MEMBER_NOT_FOUND", "AMBIGUOUS_MEMBER", "ROLE_NOT_FOUND", "AMBIGUOUS_ROLE", "DEPARTMENT_NOT_FOUND", "AMBIGUOUS_DEPARTMENT"}
|
|
3104
|
-
# 目录查询失败类(隔离 eval 环境外部登录资源不可达 → 后端成员/角色搜索 49500 超时,
|
|
3105
|
-
# 实测确定性环境限制非瞬态):在 uid/dept_id 直通已解析出至少一个主体、即主体集合
|
|
3106
|
-
# 不依赖搜索通道时,搜索失败同样降级为非阻断(主体集合由直通锚完整保留);
|
|
3107
|
-
# 纯手写声明(仅 email/name 无 uid)仍阻断,避免静默丢成员。
|
|
3108
|
-
uid_direct_count = len(member_resolution.get("member_entries") or []) + len(department_resolution.get("department_entries") or [])
|
|
3109
3085
|
blocking_issues = [
|
|
3110
3086
|
issue
|
|
3111
3087
|
for issue in [
|
|
@@ -3115,13 +3091,10 @@ class AiBuilderFacade:
|
|
|
3115
3091
|
*external_member_resolution["issues"],
|
|
3116
3092
|
]
|
|
3117
3093
|
# 冗余 email/name 引用「找不到」降级为非阻断(uid/id 直通已保主体不丢);
|
|
3118
|
-
# 权限拒绝/查询失败类(LOOKUP_FAILED/PERMISSION_DENIED)
|
|
3094
|
+
# 权限拒绝/查询失败类(LOOKUP_FAILED/PERMISSION_DENIED)仍阻断
|
|
3119
3095
|
if not (
|
|
3120
3096
|
str(issue.get("kind") or "") in _SOFT_RESOLUTION_KINDS
|
|
3121
|
-
and (
|
|
3122
|
-
str(issue.get("error_code") or "") in _NOT_FOUND_RESOLUTION_CODES
|
|
3123
|
-
or (str(issue.get("error_code") or "").endswith("_SEARCH_FAILED") and uid_direct_count > 0)
|
|
3124
|
-
)
|
|
3097
|
+
and str(issue.get("error_code") or "") in _NOT_FOUND_RESOLUTION_CODES
|
|
3125
3098
|
)
|
|
3126
3099
|
]
|
|
3127
3100
|
self._raise_visibility_resolution_issues(blocking_issues)
|
|
@@ -8780,25 +8753,7 @@ class AiBuilderFacade:
|
|
|
8780
8753
|
"warnings": warnings,
|
|
8781
8754
|
}
|
|
8782
8755
|
|
|
8783
|
-
def portal_get(
|
|
8784
|
-
self,
|
|
8785
|
-
*,
|
|
8786
|
-
profile: str,
|
|
8787
|
-
dash_key: str,
|
|
8788
|
-
being_draft: bool = True,
|
|
8789
|
-
schema_kind: str = "raw",
|
|
8790
|
-
schema_version: str | None = None,
|
|
8791
|
-
) -> JSONObject:
|
|
8792
|
-
"""读取门户详情;schema_kind=summary|complete 时返回 canonical 声明投影,raw 为原始归一响应。"""
|
|
8793
|
-
normalized_kind = str(schema_kind or "raw").strip().lower() or "raw"
|
|
8794
|
-
if normalized_kind not in {"raw", "summary", "complete"}:
|
|
8795
|
-
return _failed(
|
|
8796
|
-
"UNSUPPORTED_SCHEMA_KIND",
|
|
8797
|
-
f"unsupported portal get schema kind: {normalized_kind}",
|
|
8798
|
-
normalized_args={"dash_key": dash_key, "being_draft": being_draft, "schema_kind": normalized_kind},
|
|
8799
|
-
details={"supportedKinds": ["raw", "summary", "complete"]},
|
|
8800
|
-
suggested_next_call={"tool_name": "portal_get", "arguments": {"profile": profile, "dash_key": dash_key, "schema_kind": "raw"}},
|
|
8801
|
-
)
|
|
8756
|
+
def portal_get(self, *, profile: str, dash_key: str, being_draft: bool = True) -> JSONObject:
|
|
8802
8757
|
try:
|
|
8803
8758
|
result = self.portals.portal_get(profile=profile, dash_key=dash_key, being_draft=being_draft).get("result") or {}
|
|
8804
8759
|
except (QingflowApiError, RuntimeError) as error:
|
|
@@ -8806,52 +8761,10 @@ class AiBuilderFacade:
|
|
|
8806
8761
|
return _failed_from_api_error(
|
|
8807
8762
|
"PORTAL_GET_FAILED",
|
|
8808
8763
|
api_error,
|
|
8809
|
-
normalized_args={"dash_key": dash_key, "being_draft": being_draft
|
|
8764
|
+
normalized_args={"dash_key": dash_key, "being_draft": being_draft},
|
|
8810
8765
|
details={"dash_key": dash_key, "being_draft": being_draft},
|
|
8811
8766
|
suggested_next_call={"tool_name": "portal_get", "arguments": {"profile": profile, "dash_key": dash_key, "being_draft": being_draft}},
|
|
8812
8767
|
)
|
|
8813
|
-
if normalized_kind in {"summary", "complete"}:
|
|
8814
|
-
schema_context = None
|
|
8815
|
-
if schema_version is not None:
|
|
8816
|
-
try:
|
|
8817
|
-
schema_context = portals_schema_registry().resolve_schema_version(schema_version)
|
|
8818
|
-
except PortalsContractError as error:
|
|
8819
|
-
return _failed(
|
|
8820
|
-
error.error_code,
|
|
8821
|
-
str(error.message),
|
|
8822
|
-
normalized_args={"dash_key": dash_key, "being_draft": being_draft, "schema_kind": normalized_kind, "schema_version": schema_version},
|
|
8823
|
-
details={"reason_path": error.path or "", **(error.details or {})},
|
|
8824
|
-
suggested_next_call={"tool_name": "portal_schema", "arguments": {"schema_kind": "summary"}},
|
|
8825
|
-
)
|
|
8826
|
-
declaration, read_gaps = _portal_declaration_from_dash_detail(result, schema_kind=normalized_kind)
|
|
8827
|
-
response = {
|
|
8828
|
-
"status": "success",
|
|
8829
|
-
"error_code": None,
|
|
8830
|
-
"recoverable": False,
|
|
8831
|
-
"message": f"read portal declaration ({normalized_kind})",
|
|
8832
|
-
"normalized_args": {
|
|
8833
|
-
"dash_key": dash_key,
|
|
8834
|
-
"being_draft": being_draft,
|
|
8835
|
-
"schema_kind": normalized_kind,
|
|
8836
|
-
"schema_version": schema_version,
|
|
8837
|
-
},
|
|
8838
|
-
"missing_fields": [],
|
|
8839
|
-
"allowed_values": {"schema_kind": ["raw", "summary", "complete"]},
|
|
8840
|
-
"details": {"read_gaps": read_gaps} if read_gaps else {},
|
|
8841
|
-
"request_id": None,
|
|
8842
|
-
"suggested_next_call": None,
|
|
8843
|
-
"noop": False,
|
|
8844
|
-
"warnings": [],
|
|
8845
|
-
"verification": {"portal_exists": True, "being_draft": being_draft},
|
|
8846
|
-
"verified": True,
|
|
8847
|
-
"schemaVersion": schema_context.schema_version if schema_context is not None else portals_schema_registry().latest_version,
|
|
8848
|
-
"latestVersion": portals_schema_registry().latest_version,
|
|
8849
|
-
"apiVersion": schema_context.api_version if schema_context is not None else "builder.qingflow.com/v1alpha1",
|
|
8850
|
-
"schemaKind": normalized_kind,
|
|
8851
|
-
"declaration": declaration,
|
|
8852
|
-
"read_gap_count": len(read_gaps),
|
|
8853
|
-
}
|
|
8854
|
-
return response
|
|
8855
8768
|
dash_icon = str(result.get("dashIcon") or "").strip() or None
|
|
8856
8769
|
response = PortalGetResponse(
|
|
8857
8770
|
dash_key=dash_key,
|
|
@@ -8946,104 +8859,6 @@ class AiBuilderFacade:
|
|
|
8946
8859
|
**response.model_dump(mode="json"),
|
|
8947
8860
|
}
|
|
8948
8861
|
|
|
8949
|
-
def portal_schema(
|
|
8950
|
-
self,
|
|
8951
|
-
*,
|
|
8952
|
-
schema_version: str | None = None,
|
|
8953
|
-
schema_kind: str | None = None,
|
|
8954
|
-
) -> JSONObject:
|
|
8955
|
-
"""读取版本化门户声明 schema;组件专项 kind 用于获取自包含专项配置定义。"""
|
|
8956
|
-
normalized_args = {"schema_version": schema_version, "schema_kind": schema_kind}
|
|
8957
|
-
try:
|
|
8958
|
-
payload = portals_schema_registry().schema(schema_version=schema_version, schema_kind=schema_kind)
|
|
8959
|
-
except PortalsContractError as error:
|
|
8960
|
-
return _failed(
|
|
8961
|
-
error.error_code,
|
|
8962
|
-
str(error.message),
|
|
8963
|
-
normalized_args=normalized_args,
|
|
8964
|
-
details={"reason_path": error.path or "", **(error.details or {})},
|
|
8965
|
-
suggested_next_call={"tool_name": "portal_schema", "arguments": {"schema_kind": "summary"}},
|
|
8966
|
-
)
|
|
8967
|
-
return {
|
|
8968
|
-
"status": "success",
|
|
8969
|
-
"error_code": None,
|
|
8970
|
-
"recoverable": False,
|
|
8971
|
-
"message": "read portals declaration schema",
|
|
8972
|
-
"normalized_args": normalized_args,
|
|
8973
|
-
"missing_fields": [],
|
|
8974
|
-
"allowed_values": {"schema_kind": payload["supportedSchemaKinds"]},
|
|
8975
|
-
"details": {},
|
|
8976
|
-
"request_id": None,
|
|
8977
|
-
"suggested_next_call": None,
|
|
8978
|
-
"warnings": [],
|
|
8979
|
-
"verification": {"schema_loaded": True},
|
|
8980
|
-
**payload,
|
|
8981
|
-
}
|
|
8982
|
-
|
|
8983
|
-
def portal_validate(
|
|
8984
|
-
self,
|
|
8985
|
-
*,
|
|
8986
|
-
declaration: JSONObject,
|
|
8987
|
-
schema_version: str | None = None,
|
|
8988
|
-
) -> JSONObject:
|
|
8989
|
-
"""纯离线校验一条门户声明;不访问后端。"""
|
|
8990
|
-
normalized_args = {"schema_version": schema_version}
|
|
8991
|
-
resolved_version = (
|
|
8992
|
-
str(schema_version or (declaration.get("schema_version") if isinstance(declaration, dict) else None) or "").strip() or None
|
|
8993
|
-
)
|
|
8994
|
-
try:
|
|
8995
|
-
context = portals_schema_registry().resolve_schema_version(resolved_version)
|
|
8996
|
-
except PortalsContractError as error:
|
|
8997
|
-
return _failed(
|
|
8998
|
-
error.error_code,
|
|
8999
|
-
str(error.message),
|
|
9000
|
-
normalized_args=normalized_args,
|
|
9001
|
-
details={"reason_path": error.path or "", **(error.details or {})},
|
|
9002
|
-
suggested_next_call={"tool_name": "portal_schema", "arguments": {"schema_kind": "complete"}},
|
|
9003
|
-
)
|
|
9004
|
-
issues: list[dict[str, Any]] = []
|
|
9005
|
-
if not isinstance(declaration, dict):
|
|
9006
|
-
issues.append({"path": "/", "code": "INVALID_SPEC", "message": "portals declaration must be a JSON object"})
|
|
9007
|
-
else:
|
|
9008
|
-
declaration_item = {
|
|
9009
|
-
key: value
|
|
9010
|
-
for key, value in declaration.items()
|
|
9011
|
-
if key not in {"schema_version", "schemaVersion", "apiVersion"}
|
|
9012
|
-
}
|
|
9013
|
-
try:
|
|
9014
|
-
parsed = PortalApplyRequest.model_validate(declaration_item)
|
|
9015
|
-
context.validate_upsert(compact_none_values(public_portal_apply_payload(parsed)))
|
|
9016
|
-
except ValidationError as exc:
|
|
9017
|
-
first_error = exc.errors()[0]
|
|
9018
|
-
issues.append(
|
|
9019
|
-
{
|
|
9020
|
-
"path": f"/{'/'.join(str(part) for part in first_error.get('loc', ()))}".rstrip("/"),
|
|
9021
|
-
"code": "INVALID_SPEC",
|
|
9022
|
-
"message": str(first_error.get("msg") or exc),
|
|
9023
|
-
}
|
|
9024
|
-
)
|
|
9025
|
-
except PortalsContractError as error:
|
|
9026
|
-
issues.append({"path": error.path or "/", "code": error.error_code, "message": str(error.message)})
|
|
9027
|
-
ok = not issues
|
|
9028
|
-
return {
|
|
9029
|
-
"status": "success" if ok else "failed",
|
|
9030
|
-
"error_code": None if ok else issues[0]["code"],
|
|
9031
|
-
"recoverable": True,
|
|
9032
|
-
"message": "portals declaration is valid" if ok else "portals declaration failed validation",
|
|
9033
|
-
"normalized_args": normalized_args,
|
|
9034
|
-
"missing_fields": [],
|
|
9035
|
-
"allowed_values": {},
|
|
9036
|
-
"details": {"issues": issues},
|
|
9037
|
-
"request_id": None,
|
|
9038
|
-
"suggested_next_call": None,
|
|
9039
|
-
"warnings": [],
|
|
9040
|
-
"verification": {"validated_entries": 1 if ok else 0},
|
|
9041
|
-
"ok": ok,
|
|
9042
|
-
"schemaVersion": context.schema_version,
|
|
9043
|
-
"apiVersion": context.api_version,
|
|
9044
|
-
"issues": issues,
|
|
9045
|
-
}
|
|
9046
|
-
|
|
9047
8862
|
def view_get(self, *, profile: str, view_key: str) -> JSONObject:
|
|
9048
8863
|
try:
|
|
9049
8864
|
config = self.views.view_get_config(profile=profile, viewgraph_key=view_key).get("result") or {}
|
|
@@ -15263,13 +15078,7 @@ class AiBuilderFacade:
|
|
|
15263
15078
|
"safe_to_retry": not write_executed,
|
|
15264
15079
|
})
|
|
15265
15080
|
|
|
15266
|
-
def portal_apply(
|
|
15267
|
-
self,
|
|
15268
|
-
*,
|
|
15269
|
-
profile: str,
|
|
15270
|
-
request: PortalApplyRequest,
|
|
15271
|
-
schema_version: str | None = None,
|
|
15272
|
-
) -> JSONObject:
|
|
15081
|
+
def portal_apply(self, *, profile: str, request: PortalApplyRequest) -> JSONObject:
|
|
15273
15082
|
apply_started_at = time.perf_counter()
|
|
15274
15083
|
portal_initial_read_ms: int | None = None
|
|
15275
15084
|
portal_create_ms: int | None = None
|
|
@@ -15279,39 +15088,16 @@ class AiBuilderFacade:
|
|
|
15279
15088
|
portal_draft_readback_ms: int | None = None
|
|
15280
15089
|
portal_publish_ms: int | None = None
|
|
15281
15090
|
portal_live_readback_ms: int | None = None
|
|
15282
|
-
effective_schema_version = str(schema_version or request.schema_version or "").strip() or None
|
|
15283
15091
|
normalized_args = request.model_dump(mode="json")
|
|
15284
|
-
normalized_args["schema_version"] = effective_schema_version
|
|
15285
|
-
schema_context: PortalsSchemaContext | None = None
|
|
15286
|
-
if effective_schema_version is not None:
|
|
15287
|
-
try:
|
|
15288
|
-
schema_context = portals_schema_registry().resolve_schema_version(effective_schema_version)
|
|
15289
|
-
schema_context.validate_upsert(
|
|
15290
|
-
compact_none_values(public_portal_apply_payload(request))
|
|
15291
|
-
)
|
|
15292
|
-
except PortalsContractError as error:
|
|
15293
|
-
return _failed(
|
|
15294
|
-
error.error_code,
|
|
15295
|
-
str(error.message),
|
|
15296
|
-
normalized_args=normalized_args,
|
|
15297
|
-
details={"reason_path": error.path or "", **(error.details or {})},
|
|
15298
|
-
suggested_next_call={"tool_name": "portal_schema", "arguments": {"schema_version": effective_schema_version, "schema_kind": "complete"}},
|
|
15299
|
-
)
|
|
15300
15092
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
15301
15093
|
dash_key = str(request.dash_key or "").strip()
|
|
15302
15094
|
creating = not dash_key
|
|
15303
|
-
|
|
15304
|
-
# 未传(不在 model_fields_set)才是纯 base-info 更新。模型缺省 sections=[] 与
|
|
15305
|
-
# 显式空列表同值,只能用 fields_set 区分。空 sections=[] 是合法存量形态
|
|
15306
|
-
# (无组件门户):后端 updateDash 对空组件列表=删全部后不重建(getComponents
|
|
15307
|
-
# 非空才重建),createDash 不建组件——把空列表当「未传」会把携带
|
|
15308
|
-
# global_config/hide_copyright 的空门户拒之门外。
|
|
15309
|
-
sections_requested = creating or ("sections" in request.model_fields_set)
|
|
15095
|
+
sections_requested = creating or bool(request.sections)
|
|
15310
15096
|
verify_dash_name = creating or request.dash_name is not None
|
|
15311
15097
|
verify_dash_icon = bool(request.icon or request.color)
|
|
15312
15098
|
verify_auth = request.visibility is not None or request.auth is not None
|
|
15313
15099
|
verify_hide_copyright = request.hide_copyright is not None and sections_requested
|
|
15314
|
-
verify_dash_global_config = request.
|
|
15100
|
+
verify_dash_global_config = request.dash_global_config is not None and sections_requested
|
|
15315
15101
|
verify_tags = creating or request.package_tag_id is not None
|
|
15316
15102
|
requested_visibility = request.visibility
|
|
15317
15103
|
if requested_visibility is None and isinstance(request.auth, dict) and request.auth:
|
|
@@ -15361,8 +15147,6 @@ class AiBuilderFacade:
|
|
|
15361
15147
|
permission_outcomes.append(portal_permission_outcome)
|
|
15362
15148
|
|
|
15363
15149
|
def finalize(response: JSONObject) -> JSONObject:
|
|
15364
|
-
if schema_context is not None:
|
|
15365
|
-
response["schemaVersion"] = schema_context.schema_version
|
|
15366
15150
|
_merge_duration_breakdown(
|
|
15367
15151
|
response,
|
|
15368
15152
|
portal_initial_read_ms=portal_initial_read_ms,
|
|
@@ -15400,8 +15184,8 @@ class AiBuilderFacade:
|
|
|
15400
15184
|
unsupported_base_only_keys: list[str] = []
|
|
15401
15185
|
if request.hide_copyright is not None:
|
|
15402
15186
|
unsupported_base_only_keys.append("hide_copyright")
|
|
15403
|
-
if request.
|
|
15404
|
-
unsupported_base_only_keys.append("
|
|
15187
|
+
if request.dash_global_config is not None:
|
|
15188
|
+
unsupported_base_only_keys.append("dash_global_config")
|
|
15405
15189
|
if request.config:
|
|
15406
15190
|
unsupported_base_only_keys.append("config")
|
|
15407
15191
|
if unsupported_base_only_keys:
|
|
@@ -15424,20 +15208,18 @@ class AiBuilderFacade:
|
|
|
15424
15208
|
layout_diagnostics: dict[str, Any] = _empty_portal_layout_diagnostics()
|
|
15425
15209
|
if creating:
|
|
15426
15210
|
create_payload = _build_public_portal_base_payload(
|
|
15427
|
-
dash_name=request.dash_name
|
|
15211
|
+
dash_name=request.dash_name or "未命名门户",
|
|
15428
15212
|
package_tag_id=target_package_tag_id,
|
|
15429
15213
|
icon=request.icon,
|
|
15430
15214
|
color=request.color,
|
|
15431
15215
|
auth=desired_auth,
|
|
15432
15216
|
hide_copyright=request.hide_copyright,
|
|
15433
|
-
dash_global_config=request.
|
|
15434
|
-
style_template_id=request.style_template_id,
|
|
15435
|
-
overall_style=request.overall_style,
|
|
15217
|
+
dash_global_config=request.dash_global_config,
|
|
15436
15218
|
config=request.config,
|
|
15437
15219
|
base_payload=None,
|
|
15438
15220
|
)
|
|
15439
15221
|
portal_create_started_at = time.perf_counter()
|
|
15440
|
-
create_result = self.
|
|
15222
|
+
create_result = self.portals.portal_create(profile=profile, payload=create_payload)
|
|
15441
15223
|
portal_create_ms = _duration_ms(portal_create_started_at)
|
|
15442
15224
|
write_executed = True
|
|
15443
15225
|
created = create_result.get("result") if isinstance(create_result.get("result"), dict) else {}
|
|
@@ -15460,15 +15242,13 @@ class AiBuilderFacade:
|
|
|
15460
15242
|
}
|
|
15461
15243
|
base_payload = {}
|
|
15462
15244
|
update_payload = _build_public_portal_base_payload(
|
|
15463
|
-
dash_name=request.dash_name
|
|
15245
|
+
dash_name=request.dash_name or str(base_payload.get("dashName") or "").strip() or "未命名门户",
|
|
15464
15246
|
package_tag_id=target_package_tag_id,
|
|
15465
15247
|
icon=request.icon,
|
|
15466
15248
|
color=request.color,
|
|
15467
15249
|
auth=desired_auth,
|
|
15468
15250
|
hide_copyright=request.hide_copyright,
|
|
15469
|
-
dash_global_config=request.
|
|
15470
|
-
style_template_id=request.style_template_id,
|
|
15471
|
-
overall_style=request.overall_style,
|
|
15251
|
+
dash_global_config=request.dash_global_config,
|
|
15472
15252
|
config=request.config,
|
|
15473
15253
|
base_payload=base_payload,
|
|
15474
15254
|
)
|
|
@@ -15484,7 +15264,7 @@ class AiBuilderFacade:
|
|
|
15484
15264
|
layout_diagnostics = _portal_layout_diagnostics(request.sections, component_payload, layout_metadata=layout_metadata)
|
|
15485
15265
|
update_payload["components"] = component_payload
|
|
15486
15266
|
portal_update_started_at = time.perf_counter()
|
|
15487
|
-
self.
|
|
15267
|
+
self.portals.portal_update(profile=profile, dash_key=dash_key, payload=update_payload)
|
|
15488
15268
|
portal_update_ms = _duration_ms(portal_update_started_at)
|
|
15489
15269
|
write_executed = True
|
|
15490
15270
|
else:
|
|
@@ -15501,22 +15281,15 @@ class AiBuilderFacade:
|
|
|
15501
15281
|
)
|
|
15502
15282
|
if base_info_update_requested:
|
|
15503
15283
|
portal_base_info_update_started_at = time.perf_counter()
|
|
15504
|
-
|
|
15505
|
-
"dashName": update_payload.get("dashName"),
|
|
15506
|
-
"dashIcon": update_payload.get("dashIcon"),
|
|
15507
|
-
"auth": deepcopy(update_payload.get("auth")),
|
|
15508
|
-
}
|
|
15509
|
-
# tags 空列表省略键:后端 updateDashBaseInfo(DashSvcImpl)对非 null 的空 tags
|
|
15510
|
-
# 按 ws 过滤后判空抛 PARAMS_ERROR → 整个 baseInfo 写失败(事务回滚,name/icon/auth
|
|
15511
|
-
# 全部静默失效,r8 实测 16 门)。省略键 → getTags()==null → 后端跳过 tags 校验与更新,
|
|
15512
|
-
# 其余字段正常写。声明显式给 package_tag_id 时 tags 非空,不受影响。
|
|
15513
|
-
base_info_tags = deepcopy(update_payload.get("tags") or [])
|
|
15514
|
-
if base_info_tags:
|
|
15515
|
-
base_info_payload["tags"] = base_info_tags
|
|
15516
|
-
self._portal_writer.update_portal_base_info(
|
|
15284
|
+
self.portals.portal_update_base_info(
|
|
15517
15285
|
profile=profile,
|
|
15518
15286
|
dash_key=dash_key,
|
|
15519
|
-
payload=
|
|
15287
|
+
payload={
|
|
15288
|
+
"dashName": update_payload.get("dashName"),
|
|
15289
|
+
"dashIcon": update_payload.get("dashIcon"),
|
|
15290
|
+
"auth": deepcopy(update_payload.get("auth")),
|
|
15291
|
+
"tags": deepcopy(update_payload.get("tags") or []),
|
|
15292
|
+
},
|
|
15520
15293
|
)
|
|
15521
15294
|
portal_base_info_update_ms = _duration_ms(portal_base_info_update_started_at)
|
|
15522
15295
|
write_executed = True
|
|
@@ -16831,58 +16604,25 @@ class AiBuilderFacade:
|
|
|
16831
16604
|
resolved_chart = inline_chart_resolutions.get(section_index)
|
|
16832
16605
|
if not isinstance(resolved_chart, dict):
|
|
16833
16606
|
raise ValueError(f"inline chart '{section.chart.name}' could not be created or updated for portal section '{section.title}'")
|
|
16834
|
-
chart_type = str(
|
|
16835
|
-
resolved_chart.get("chart_type")
|
|
16836
|
-
or _public_chart_type_from_backend(section.config.get("chartType") or section.config.get("chart_type"))
|
|
16837
|
-
or ""
|
|
16838
|
-
).strip()
|
|
16839
|
-
chart_config = {
|
|
16840
|
-
"biChartId": resolved_chart["chart_id"],
|
|
16841
|
-
# title=None(存量空标题)不落库——_compact_dict 剔除,后端透传 NULL 保真;
|
|
16842
|
-
# beingShowTitle 以声明 show_title 为准(读面提取必有值),缺省回落产品默认 True。
|
|
16843
|
-
"chartComponentTitle": section.title,
|
|
16844
|
-
"beingShowTitle": section.show_title if section.show_title is not None else True,
|
|
16845
|
-
**deepcopy(section.config),
|
|
16846
|
-
}
|
|
16847
|
-
component = {"type": 9, "position": position_payload, "chartConfig": _compact_dict(chart_config)}
|
|
16848
|
-
layout_metadata.append({"source_type": section.source_type, "chart_type": chart_type, "role": section.role})
|
|
16849
|
-
elif section.chart_ref is not None and str(getattr(section.chart_ref, "ref_kind", "") or "") == "chart":
|
|
16850
|
-
# 轻流报表引用面(后端 DASH_CONTENT_TYPE_CHART=1):chart_id 携带的是
|
|
16851
|
-
# chart 表唯一键 chartKey,写链 addChartInfos 按 chartKey 解析,不走
|
|
16852
|
-
# BI 报表列表解析;不携带运行态派生键(chartId/chartName/formTitle 等读面回填)。
|
|
16853
|
-
chart_key = str(getattr(section.chart_ref, "chart_id", "") or "").strip()
|
|
16854
|
-
if not chart_key:
|
|
16855
|
-
raise ValueError(f"chart_ref with ref_kind=chart requires chart_id for portal section '{section.title}'")
|
|
16856
|
-
chart_config = {
|
|
16857
|
-
"chartKey": chart_key,
|
|
16858
|
-
# 同上:title=None 不落库、beingShowTitle 声明优先、缺省 True。
|
|
16859
|
-
"chartComponentTitle": section.title,
|
|
16860
|
-
"beingShowTitle": section.show_title if section.show_title is not None else True,
|
|
16861
|
-
**deepcopy(section.config),
|
|
16862
|
-
}
|
|
16863
|
-
component = {"type": 1, "position": position_payload, "chartConfig": _compact_dict(chart_config)}
|
|
16864
|
-
layout_metadata.append({"source_type": section.source_type, "chart_type": str(_public_chart_type_from_backend(section.config.get("chartType") or section.config.get("chart_type")) or "").strip(), "role": section.role})
|
|
16865
16607
|
else:
|
|
16866
16608
|
resolved_chart = _resolve_chart_reference(
|
|
16867
16609
|
charts=self.charts,
|
|
16868
16610
|
profile=profile,
|
|
16869
16611
|
ref=section.chart_ref,
|
|
16870
16612
|
)
|
|
16871
|
-
|
|
16872
|
-
|
|
16873
|
-
|
|
16874
|
-
|
|
16875
|
-
|
|
16876
|
-
|
|
16877
|
-
|
|
16878
|
-
|
|
16879
|
-
|
|
16880
|
-
|
|
16881
|
-
|
|
16882
|
-
|
|
16883
|
-
|
|
16884
|
-
component = {"type": 9, "position": position_payload, "chartConfig": _compact_dict(chart_config)}
|
|
16885
|
-
layout_metadata.append({"source_type": section.source_type, "chart_type": chart_type, "role": section.role})
|
|
16613
|
+
chart_type = str(
|
|
16614
|
+
resolved_chart.get("chart_type")
|
|
16615
|
+
or _public_chart_type_from_backend(section.config.get("chartType") or section.config.get("chart_type"))
|
|
16616
|
+
or ""
|
|
16617
|
+
).strip()
|
|
16618
|
+
chart_config = {
|
|
16619
|
+
"biChartId": resolved_chart["chart_id"],
|
|
16620
|
+
"chartComponentTitle": section.title,
|
|
16621
|
+
"beingShowTitle": True,
|
|
16622
|
+
**deepcopy(section.config),
|
|
16623
|
+
}
|
|
16624
|
+
component = {"type": 9, "position": position_payload, "chartConfig": _compact_dict(chart_config)}
|
|
16625
|
+
layout_metadata.append({"source_type": section.source_type, "chart_type": chart_type, "role": section.role})
|
|
16886
16626
|
elif section.source_type == "view":
|
|
16887
16627
|
resolved_view = _resolve_view_reference(
|
|
16888
16628
|
facade=self,
|
|
@@ -16896,58 +16636,34 @@ class AiBuilderFacade:
|
|
|
16896
16636
|
view_config = {
|
|
16897
16637
|
"appKey": resolved_view["app_key"],
|
|
16898
16638
|
"viewgraphKey": resolved_view["view_key"],
|
|
16899
|
-
|
|
16900
|
-
# 显示标题(title=None 的存量空标题形态经 _compact_dict 剔除,落库 NULL 保真,
|
|
16901
|
-
# 旧逻辑把视图名兑底进 componentTitle 会覆盖读链 chartName 派生分支)。
|
|
16902
|
-
"viewgraphName": getattr(section.view_ref, "view_name", None) or section.title,
|
|
16639
|
+
"viewgraphName": section.title,
|
|
16903
16640
|
"formTitle": app_title_cache[resolved_view["app_key"]],
|
|
16904
16641
|
"componentTitle": section.title,
|
|
16905
16642
|
"viewgraphType": resolved_view["view_type"],
|
|
16906
|
-
|
|
16907
|
-
# 存量 false 被硬 True 覆写会漂移库表);缺省保持产品默认 True。
|
|
16908
|
-
"beingShowTitle": section.show_title if section.show_title is not None else True,
|
|
16643
|
+
"beingShowTitle": True,
|
|
16909
16644
|
"dataManageStatus": True,
|
|
16910
16645
|
**deepcopy(section.config),
|
|
16911
16646
|
}
|
|
16912
|
-
if section.view_display:
|
|
16913
|
-
display = _compile_portal_view_display(section.view_display)
|
|
16914
|
-
for key, value in display.items():
|
|
16915
|
-
view_config[key] = value
|
|
16916
16647
|
component = {"type": 10, "position": position_payload, "viewgraphConfig": _compact_dict(view_config)}
|
|
16917
16648
|
layout_metadata.append({"source_type": section.source_type, "role": section.role})
|
|
16918
16649
|
elif section.source_type == "grid":
|
|
16919
16650
|
component = {
|
|
16920
16651
|
"type": 2,
|
|
16921
16652
|
"position": position_payload,
|
|
16922
|
-
# gridTitle=None(存量空标题)剔除不落库;beingShowTitle 缺省 True、声明 config 内
|
|
16923
|
-
# beingShowTitle 键后置覆盖(提取侧 grid config 保留该键,存量 false 保真)。
|
|
16924
16653
|
"gridConfig": _compact_dict({"gridTitle": section.title, "beingShowTitle": True, **deepcopy(section.config)}),
|
|
16925
16654
|
}
|
|
16926
16655
|
layout_metadata.append({"source_type": section.source_type, "role": section.role})
|
|
16927
16656
|
elif section.source_type == "filter":
|
|
16928
|
-
component = {"type": 6, "position": position_payload, "filterConfig":
|
|
16657
|
+
component = {"type": 6, "position": position_payload, "filterConfig": deepcopy(section.config)}
|
|
16929
16658
|
layout_metadata.append({"source_type": section.source_type, "role": section.role})
|
|
16930
16659
|
elif section.source_type == "text":
|
|
16931
16660
|
component = {"type": 5, "position": position_payload, "textConfig": {"text": section.text or "", **deepcopy(section.config)}}
|
|
16932
16661
|
layout_metadata.append({"source_type": section.source_type, "role": section.role})
|
|
16933
|
-
elif section.source_type == "carousel":
|
|
16934
|
-
carousel_config = self._compile_portal_carousel_config(profile=profile, section=section)
|
|
16935
|
-
component = {"type": 3, "position": position_payload, "carouselConfig": _compact_dict(carousel_config)}
|
|
16936
|
-
layout_metadata.append({"source_type": section.source_type, "role": section.role})
|
|
16937
|
-
elif section.source_type == "task":
|
|
16938
|
-
task_config = _compile_portal_task_config(section.title, section.task or {})
|
|
16939
|
-
component = {"type": 8, "position": position_payload, "taskConfig": _compact_dict(task_config)}
|
|
16940
|
-
layout_metadata.append({"source_type": section.source_type, "role": section.role})
|
|
16941
|
-
elif section.source_type == "favourite":
|
|
16942
|
-
favourite_config = _compile_portal_favourite_config(section.title, section.favourite or {})
|
|
16943
|
-
component = {"type": 7, "position": position_payload, "favouriteConfig": _compact_dict(favourite_config)}
|
|
16944
|
-
layout_metadata.append({"source_type": section.source_type, "role": section.role})
|
|
16945
16662
|
else:
|
|
16946
|
-
link_config = {"url": section.url or "", "beingLoginAuth": bool(section.login_auth) if section.login_auth is not None else False, **deepcopy(section.config)}
|
|
16947
16663
|
component = {
|
|
16948
16664
|
"type": 4,
|
|
16949
16665
|
"position": position_payload,
|
|
16950
|
-
"linkConfig":
|
|
16666
|
+
"linkConfig": {"url": section.url or "", "beingLoginAuth": False, **deepcopy(section.config)},
|
|
16951
16667
|
}
|
|
16952
16668
|
layout_metadata.append({"source_type": section.source_type, "role": section.role})
|
|
16953
16669
|
if dash_style is not None:
|
|
@@ -16955,113 +16671,6 @@ class AiBuilderFacade:
|
|
|
16955
16671
|
resolved_components.append(component)
|
|
16956
16672
|
return resolved_components, layout_metadata, inline_chart_results
|
|
16957
16673
|
|
|
16958
|
-
def _compile_portal_carousel_config(self, *, profile: str, section: PortalSectionPatch) -> dict[str, Any]:
|
|
16959
|
-
"""Compile a carousel section declaration into the backend carouselConfig shape."""
|
|
16960
|
-
raw = section.carousel or {}
|
|
16961
|
-
if not isinstance(raw, dict):
|
|
16962
|
-
raise ValueError("carousel section config must be an object")
|
|
16963
|
-
app_key = str(raw.get("source_app_key") or raw.get("sourceAppKey") or "").strip()
|
|
16964
|
-
carousel_config: dict[str, Any] = {}
|
|
16965
|
-
fields_by_que_id: dict[int, dict[str, Any]] = {}
|
|
16966
|
-
field_name_to_field: dict[str, dict[str, Any]] = {}
|
|
16967
|
-
if app_key:
|
|
16968
|
-
carousel_config["sourceApp"] = {"appKey": app_key}
|
|
16969
|
-
if raw.get("source_form_title") or raw.get("sourceFormTitle"):
|
|
16970
|
-
carousel_config["sourceApp"]["formTitle"] = str(raw.get("source_form_title") or raw.get("sourceFormTitle"))
|
|
16971
|
-
# 字段索引从原始 form schema 面构建(_read_schema_with_fallback + schema 分支,
|
|
16972
|
-
# 与 views apply 同源):app_read_fields 解析面的字段不带 option_details,
|
|
16973
|
-
# _resolve_view_filter_values 的选项文本→ID 解析分支不生效,选项文本被
|
|
16974
|
-
# 原样透传进 formula,后端写后自读 Integer.parseInt("文本") 崩溃回滚
|
|
16975
|
-
# (r6 实测 9b91e277;七条约束之四:两读面锚定同一数据源)。
|
|
16976
|
-
schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
16977
|
-
fields_by_que_id = _collect_fields_by_que_id([], schema=schema)
|
|
16978
|
-
field_name_to_field = {
|
|
16979
|
-
str(field.get("name") or ""): field
|
|
16980
|
-
for field in fields_by_que_id.values()
|
|
16981
|
-
if str(field.get("name") or "")
|
|
16982
|
-
}
|
|
16983
|
-
# 悬空轮播(存量形态:源表已删,related_form_id NULL,读面 sourceApp 空):后端写链
|
|
16984
|
-
# DraftComponentCarousel 构造对 sourceApp 空/无 appKey 不写 relatedFormId,读面回空
|
|
16985
|
-
# sourceApp——round-trip 保真;无源表则无从解析字段引用,仅保留运行参数(jump_mode 等)。
|
|
16986
|
-
|
|
16987
|
-
def _resolve_que_ref(field_name: Any, *, config_key: str) -> dict[str, Any] | None:
|
|
16988
|
-
raw_name = str(field_name or "").strip()
|
|
16989
|
-
if not raw_name:
|
|
16990
|
-
return None
|
|
16991
|
-
field: dict[str, Any] | None = None
|
|
16992
|
-
if raw_name.isdigit():
|
|
16993
|
-
que_id = _coerce_positive_int(raw_name)
|
|
16994
|
-
field = fields_by_que_id.get(que_id) if que_id is not None else None
|
|
16995
|
-
else:
|
|
16996
|
-
field = field_name_to_field.get(raw_name)
|
|
16997
|
-
if field is None:
|
|
16998
|
-
raise ValueError(
|
|
16999
|
-
f"carousel section '{section.title}' references unknown {config_key} '{raw_name}' in app '{app_key}'"
|
|
17000
|
-
)
|
|
17001
|
-
que_id = _coerce_positive_int(field.get("que_id"))
|
|
17002
|
-
if que_id is None:
|
|
17003
|
-
raise ValueError(
|
|
17004
|
-
f"carousel section '{section.title}' {config_key} '{raw_name}' has no que_id in app '{app_key}'"
|
|
17005
|
-
)
|
|
17006
|
-
return {"queId": que_id}
|
|
17007
|
-
|
|
17008
|
-
title_que = _resolve_que_ref(raw.get("title_field") or raw.get("titleField"), config_key="title_field")
|
|
17009
|
-
image_que = _resolve_que_ref(raw.get("image_field") or raw.get("imageField"), config_key="image_field")
|
|
17010
|
-
link_que = _resolve_que_ref(raw.get("link_field") or raw.get("linkField"), config_key="link_field")
|
|
17011
|
-
if title_que is not None:
|
|
17012
|
-
carousel_config["titleQue"] = title_que
|
|
17013
|
-
if image_que is not None:
|
|
17014
|
-
carousel_config["imageQue"] = image_que
|
|
17015
|
-
if link_que is not None:
|
|
17016
|
-
carousel_config["linkQue"] = link_que
|
|
17017
|
-
jump_mode = _coerce_positive_int(raw.get("jump_mode") or raw.get("jumpMode"))
|
|
17018
|
-
if jump_mode is not None:
|
|
17019
|
-
carousel_config["jumpMode"] = jump_mode
|
|
17020
|
-
if "scaling" in raw or "beingScaling" in raw:
|
|
17021
|
-
carousel_config["beingScaling"] = bool(raw.get("scaling", raw.get("beingScaling")))
|
|
17022
|
-
show_latest_limit = _coerce_positive_int(raw.get("show_latest_limit") or raw.get("showLatestLimit"))
|
|
17023
|
-
if show_latest_limit is not None:
|
|
17024
|
-
carousel_config["showLatestLimit"] = show_latest_limit
|
|
17025
|
-
scroll_interval = _coerce_positive_int(raw.get("scroll_interval") or raw.get("scrollInterval"))
|
|
17026
|
-
if scroll_interval is not None:
|
|
17027
|
-
carousel_config["scrollInterval"] = scroll_interval
|
|
17028
|
-
raw_filters = raw.get("filters")
|
|
17029
|
-
if isinstance(raw_filters, list) and raw_filters:
|
|
17030
|
-
translated_rules: list[dict[str, Any]] = []
|
|
17031
|
-
for raw_rule in raw_filters:
|
|
17032
|
-
if hasattr(raw_rule, "model_dump"):
|
|
17033
|
-
raw_rule = raw_rule.model_dump(mode="json")
|
|
17034
|
-
if not isinstance(raw_rule, dict):
|
|
17035
|
-
continue
|
|
17036
|
-
field_name = str(raw_rule.get("field_name") or raw_rule.get("fieldName") or "").strip()
|
|
17037
|
-
field: dict[str, Any] | None = None
|
|
17038
|
-
if field_name.isdigit():
|
|
17039
|
-
# 与 _resolve_que_ref 同型:queId 数字串优先按稳定锚解析(提取器 canonical 形态)
|
|
17040
|
-
que_id = _coerce_positive_int(field_name)
|
|
17041
|
-
field = fields_by_que_id.get(que_id) if que_id is not None else None
|
|
17042
|
-
if field is None:
|
|
17043
|
-
field = field_name_to_field.get(field_name)
|
|
17044
|
-
if field is None:
|
|
17045
|
-
raise ValueError(
|
|
17046
|
-
f"carousel section '{section.title}' filters reference unknown field '{field_name}' in app '{app_key}'"
|
|
17047
|
-
)
|
|
17048
|
-
rule_payload = {
|
|
17049
|
-
"que_id": field.get("que_id"),
|
|
17050
|
-
"operator": raw_rule.get("operator") or raw_rule.get("op") or "eq",
|
|
17051
|
-
"values": list(raw_rule.get("values") or []),
|
|
17052
|
-
}
|
|
17053
|
-
groups, filter_issues = _build_view_filter_groups(fields_by_que_id=fields_by_que_id, filters=[rule_payload])
|
|
17054
|
-
blocking = [issue for issue in filter_issues if issue.get("error_code")]
|
|
17055
|
-
if blocking:
|
|
17056
|
-
raise ValueError(
|
|
17057
|
-
f"carousel section '{section.title}' filters failed to compile: {blocking[0]}"
|
|
17058
|
-
)
|
|
17059
|
-
if groups:
|
|
17060
|
-
translated_rules.extend(groups[0])
|
|
17061
|
-
if translated_rules:
|
|
17062
|
-
carousel_config["filterConfig"] = [translated_rules]
|
|
17063
|
-
return carousel_config
|
|
17064
|
-
|
|
17065
16674
|
def _resolve_current_user_identity(self, *, profile: str) -> JSONObject:
|
|
17066
16675
|
session_profile = self.apps.sessions.get_profile(profile)
|
|
17067
16676
|
backend_session = self.apps.sessions.get_backend_session(profile)
|
|
@@ -20635,142 +20244,6 @@ def _chart_item_dataset_source_type(item: dict[str, Any]) -> str:
|
|
|
20635
20244
|
return ""
|
|
20636
20245
|
|
|
20637
20246
|
|
|
20638
|
-
def _compile_portal_global_config(raw: dict[str, Any] | None) -> dict[str, Any]:
|
|
20639
|
-
"""Compile declaration-named global_config keys to backend dashGlobalConfig keys."""
|
|
20640
|
-
config: dict[str, Any] = dict(raw or {})
|
|
20641
|
-
if "auto_refresh" in config:
|
|
20642
|
-
config["beingAutoRefresh"] = config.pop("auto_refresh")
|
|
20643
|
-
if "export" in config:
|
|
20644
|
-
config["beingExport"] = config.pop("export")
|
|
20645
|
-
return config
|
|
20646
|
-
|
|
20647
|
-
|
|
20648
|
-
def _compile_portal_view_display(raw: dict[str, Any] | None) -> dict[str, Any]:
|
|
20649
|
-
"""Compile declaration view_display keys to backend viewgraphConfig keys."""
|
|
20650
|
-
display: dict[str, Any] = {}
|
|
20651
|
-
payload = raw or {}
|
|
20652
|
-
if "show_title" in payload:
|
|
20653
|
-
display["beingShowTitle"] = bool(payload["show_title"])
|
|
20654
|
-
elif "beingShowTitle" in payload:
|
|
20655
|
-
display["beingShowTitle"] = bool(payload["beingShowTitle"])
|
|
20656
|
-
if payload.get("component_title") or payload.get("componentTitle"):
|
|
20657
|
-
display["componentTitle"] = str(payload.get("component_title") or payload.get("componentTitle"))
|
|
20658
|
-
return display
|
|
20659
|
-
|
|
20660
|
-
|
|
20661
|
-
def _compile_portal_task_config(section_title: str, raw: dict[str, Any]) -> dict[str, Any]:
|
|
20662
|
-
"""Compile a task section declaration into the backend taskConfig shape."""
|
|
20663
|
-
payload = raw or {}
|
|
20664
|
-
task_config: dict[str, Any] = {
|
|
20665
|
-
"componentTaskTitle": section_title,
|
|
20666
|
-
}
|
|
20667
|
-
if "show_title" in payload:
|
|
20668
|
-
task_config["beingShowTitle"] = bool(payload["show_title"])
|
|
20669
|
-
elif "beingShowTitle" in payload:
|
|
20670
|
-
task_config["beingShowTitle"] = bool(payload["beingShowTitle"])
|
|
20671
|
-
if "hint" in payload or "componentTaskHint" in payload:
|
|
20672
|
-
task_config["componentTaskHint"] = str(payload.get("hint") or payload.get("componentTaskHint") or "")
|
|
20673
|
-
if "show_hint" in payload:
|
|
20674
|
-
task_config["beingShowHint"] = bool(payload["show_hint"])
|
|
20675
|
-
elif "beingShowHint" in payload:
|
|
20676
|
-
task_config["beingShowHint"] = bool(payload["beingShowHint"])
|
|
20677
|
-
if "enabled" in payload:
|
|
20678
|
-
task_config["beingUsing"] = bool(payload["enabled"])
|
|
20679
|
-
elif "beingUsing" in payload:
|
|
20680
|
-
task_config["beingUsing"] = bool(payload["beingUsing"])
|
|
20681
|
-
raw_tasks = payload.get("tasks")
|
|
20682
|
-
if isinstance(raw_tasks, list) and raw_tasks:
|
|
20683
|
-
task_items: list[dict[str, Any]] = []
|
|
20684
|
-
for index, raw_item in enumerate(raw_tasks):
|
|
20685
|
-
if not isinstance(raw_item, dict):
|
|
20686
|
-
continue
|
|
20687
|
-
item: dict[str, Any] = {
|
|
20688
|
-
"title": str(raw_item.get("title") or ""),
|
|
20689
|
-
"type": _coerce_positive_int(raw_item.get("task_type") or raw_item.get("type")) or 1,
|
|
20690
|
-
"ordinal": _coerce_positive_int(raw_item.get("ordinal")) or index,
|
|
20691
|
-
"beingCheck": bool(raw_item.get("checked", raw_item.get("beingCheck", False))),
|
|
20692
|
-
}
|
|
20693
|
-
task_items.append(item)
|
|
20694
|
-
if task_items:
|
|
20695
|
-
task_config["dashTaskConfigList"] = task_items
|
|
20696
|
-
return task_config
|
|
20697
|
-
|
|
20698
|
-
|
|
20699
|
-
def _compile_portal_filter_config(raw: dict[str, Any] | None) -> dict[str, Any]:
|
|
20700
|
-
"""Compile a filter section config into the backend ComponentFilter shape.
|
|
20701
|
-
|
|
20702
|
-
Declaration form: ``{graphs: [{chart_key, chart_name}], rules: [...]}``.
|
|
20703
|
-
Legacy raw form (``{graphList: [...], filterConfig: [...]}`` 后端键) 透传兼容。
|
|
20704
|
-
"""
|
|
20705
|
-
payload = raw or {}
|
|
20706
|
-
compiled: dict[str, Any] = {}
|
|
20707
|
-
raw_graphs = payload.get("graphs")
|
|
20708
|
-
if isinstance(raw_graphs, list):
|
|
20709
|
-
graph_list: list[dict[str, Any]] = []
|
|
20710
|
-
for item in raw_graphs:
|
|
20711
|
-
if not isinstance(item, dict):
|
|
20712
|
-
continue
|
|
20713
|
-
graph: dict[str, Any] = {
|
|
20714
|
-
"graphKey": str(item.get("chart_key") or item.get("chartKey") or item.get("graphKey") or "").strip(),
|
|
20715
|
-
"graphName": str(item.get("chart_name") or item.get("chartName") or item.get("graphName") or "").strip(),
|
|
20716
|
-
"graphType": item.get("graph_type") or item.get("graphType") or "CHART",
|
|
20717
|
-
}
|
|
20718
|
-
if graph["graphKey"] or graph["graphName"]:
|
|
20719
|
-
graph_list.append(graph)
|
|
20720
|
-
compiled["graphList"] = graph_list
|
|
20721
|
-
elif isinstance(payload.get("graphList"), list):
|
|
20722
|
-
compiled["graphList"] = deepcopy(payload.get("graphList"))
|
|
20723
|
-
raw_rules = payload.get("rules")
|
|
20724
|
-
if isinstance(raw_rules, list):
|
|
20725
|
-
compiled["filterConfig"] = deepcopy(raw_rules)
|
|
20726
|
-
elif isinstance(payload.get("filterConfig"), list):
|
|
20727
|
-
compiled["filterConfig"] = deepcopy(payload.get("filterConfig"))
|
|
20728
|
-
return compiled
|
|
20729
|
-
|
|
20730
|
-
|
|
20731
|
-
def _compile_portal_favourite_config(section_title: str, raw: dict[str, Any]) -> dict[str, Any]:
|
|
20732
|
-
"""Compile a favourite section declaration into the backend favouriteConfig shape."""
|
|
20733
|
-
payload = raw or {}
|
|
20734
|
-
favourite_config: dict[str, Any] = {
|
|
20735
|
-
"favouriteTitle": section_title,
|
|
20736
|
-
}
|
|
20737
|
-
if "show_title" in payload:
|
|
20738
|
-
favourite_config["beingShowTitle"] = bool(payload["show_title"])
|
|
20739
|
-
elif "beingShowTitle" in payload:
|
|
20740
|
-
favourite_config["beingShowTitle"] = bool(payload["beingShowTitle"])
|
|
20741
|
-
if "enabled" in payload:
|
|
20742
|
-
favourite_config["beingUsing"] = bool(payload["enabled"])
|
|
20743
|
-
elif "beingUsing" in payload:
|
|
20744
|
-
favourite_config["beingUsing"] = bool(payload["beingUsing"])
|
|
20745
|
-
raw_items = payload.get("items")
|
|
20746
|
-
if isinstance(raw_items, list) and raw_items:
|
|
20747
|
-
favourite_items: list[dict[str, Any]] = []
|
|
20748
|
-
for index, raw_item in enumerate(raw_items):
|
|
20749
|
-
if not isinstance(raw_item, dict):
|
|
20750
|
-
continue
|
|
20751
|
-
item: dict[str, Any] = {
|
|
20752
|
-
"title": str(raw_item.get("title") or ""),
|
|
20753
|
-
"type": _coerce_positive_int(raw_item.get("item_type") or raw_item.get("type")) or 1,
|
|
20754
|
-
"ordinal": _coerce_positive_int(raw_item.get("ordinal")) or index,
|
|
20755
|
-
}
|
|
20756
|
-
link_key = str(raw_item.get("link_key") or raw_item.get("linkKey") or "").strip()
|
|
20757
|
-
if link_key:
|
|
20758
|
-
item["linkKey"] = link_key
|
|
20759
|
-
link_id = _coerce_positive_int(raw_item.get("link_id") or raw_item.get("linkId"))
|
|
20760
|
-
if link_id is not None:
|
|
20761
|
-
item["linkId"] = link_id
|
|
20762
|
-
icon_url = str(raw_item.get("icon_url") or raw_item.get("iconUrl") or "").strip()
|
|
20763
|
-
if icon_url:
|
|
20764
|
-
item["iconUrl"] = icon_url
|
|
20765
|
-
jump_mode = _coerce_positive_int(raw_item.get("jump_mode") or raw_item.get("jumpMode"))
|
|
20766
|
-
if jump_mode is not None:
|
|
20767
|
-
item["jumpMode"] = jump_mode
|
|
20768
|
-
favourite_items.append(item)
|
|
20769
|
-
if favourite_items:
|
|
20770
|
-
favourite_config["items"] = favourite_items
|
|
20771
|
-
return favourite_config
|
|
20772
|
-
|
|
20773
|
-
|
|
20774
20247
|
def _build_public_portal_base_payload(
|
|
20775
20248
|
*,
|
|
20776
20249
|
dash_name: str,
|
|
@@ -20780,49 +20253,19 @@ def _build_public_portal_base_payload(
|
|
|
20780
20253
|
auth: dict[str, Any] | None,
|
|
20781
20254
|
hide_copyright: bool | None,
|
|
20782
20255
|
dash_global_config: dict[str, Any] | None,
|
|
20783
|
-
style_template_id: int | None = None,
|
|
20784
|
-
overall_style: dict[str, Any] | None = None,
|
|
20785
20256
|
config: dict[str, Any] | None,
|
|
20786
20257
|
base_payload: dict[str, Any] | None,
|
|
20787
20258
|
) -> dict[str, Any]:
|
|
20788
20259
|
data = deepcopy(base_payload) if isinstance(base_payload, dict) else {}
|
|
20789
|
-
|
|
20790
|
-
# 空串=显式空名(后端 dash_name 允许空串,生产存量存在;round-trip 保真必须透传)。
|
|
20791
|
-
if dash_name is not None:
|
|
20792
|
-
effective_name = str(dash_name).strip()
|
|
20793
|
-
else:
|
|
20794
|
-
effective_name = str(data.get("dashName") or "").strip() or "未命名门户"
|
|
20260
|
+
effective_name = str(dash_name or data.get("dashName") or "").strip() or "未命名门户"
|
|
20795
20261
|
effective_hide_copyright = hide_copyright if hide_copyright is not None else bool(data.get("hideCopyright", False))
|
|
20796
|
-
|
|
20797
|
-
# - 声明显式给 icon/color → 重编码(存量形态走最小 patch);
|
|
20798
|
-
# - 声明未指定 + 创建语义(base_payload 无) → 合成默认(创建行为不变);
|
|
20799
|
-
# - 声明未指定 + 更新语义 → 不碰 dashIcon:存量有则保留原值,存量空则**不合成**。
|
|
20800
|
-
# 后端 updateDashBaseInfo 对 dashIcon 无校验直接透传;合成默认会让「B0 无图标」
|
|
20801
|
-
# 门户在 apply 后长出默认图标/默认色(r4 实测 50 处 color diff 根因)。
|
|
20802
|
-
if icon or color:
|
|
20803
|
-
if base_payload is not None and isinstance(base_payload.get("dashIcon"), str):
|
|
20804
|
-
data["dashIcon"] = update_workspace_icon_from_existing(
|
|
20805
|
-
icon=icon,
|
|
20806
|
-
color=color,
|
|
20807
|
-
title=effective_name,
|
|
20808
|
-
existing_icon=str(data.get("dashIcon") or "").strip() or None,
|
|
20809
|
-
fallback_icon_name="view-grid",
|
|
20810
|
-
)
|
|
20811
|
-
else:
|
|
20812
|
-
data["dashIcon"] = encode_workspace_icon_with_defaults(
|
|
20813
|
-
icon=icon,
|
|
20814
|
-
color=color,
|
|
20815
|
-
title=effective_name,
|
|
20816
|
-
fallback_icon_name="view-grid",
|
|
20817
|
-
existing_icon=str(data.get("dashIcon") or "").strip() or None,
|
|
20818
|
-
)
|
|
20819
|
-
elif base_payload is None:
|
|
20262
|
+
if icon or color or not data.get("dashIcon"):
|
|
20820
20263
|
data["dashIcon"] = encode_workspace_icon_with_defaults(
|
|
20821
|
-
icon=
|
|
20822
|
-
color=
|
|
20264
|
+
icon=icon,
|
|
20265
|
+
color=color,
|
|
20823
20266
|
title=effective_name,
|
|
20824
20267
|
fallback_icon_name="view-grid",
|
|
20825
|
-
existing_icon=None,
|
|
20268
|
+
existing_icon=str(data.get("dashIcon") or "").strip() or None,
|
|
20826
20269
|
)
|
|
20827
20270
|
data["dashName"] = effective_name
|
|
20828
20271
|
data["auth"] = deepcopy(auth if auth is not None else data.get("auth") or default_member_auth())
|
|
@@ -20832,15 +20275,10 @@ def _build_public_portal_base_payload(
|
|
|
20832
20275
|
elif "tags" not in data:
|
|
20833
20276
|
data["tags"] = []
|
|
20834
20277
|
global_config = deepcopy(dash_global_config if isinstance(dash_global_config, dict) else data.get("dashGlobalConfig") or {})
|
|
20835
|
-
global_config = _compile_portal_global_config(global_config)
|
|
20836
20278
|
global_config.pop("layout", None)
|
|
20837
20279
|
global_config["interval"] = global_config.get("interval") or 60
|
|
20838
20280
|
global_config["beingAutoRefresh"] = bool(global_config.get("beingAutoRefresh", False))
|
|
20839
20281
|
data["dashGlobalConfig"] = global_config
|
|
20840
|
-
if style_template_id is not None:
|
|
20841
|
-
data["styleTemplateId"] = style_template_id
|
|
20842
|
-
if overall_style is not None:
|
|
20843
|
-
data["overallConfigBO"] = deepcopy(overall_style)
|
|
20844
20282
|
if isinstance(config, dict):
|
|
20845
20283
|
data.update(deepcopy(config))
|
|
20846
20284
|
return data
|
|
@@ -20946,37 +20384,18 @@ def _find_chart_by_name(items: Any, *, chart_name: str, chart_type: str | None =
|
|
|
20946
20384
|
|
|
20947
20385
|
def _portal_position_payload(position: Any, *, inferred_mobile_y: int = 0) -> dict[str, Any]:
|
|
20948
20386
|
mobile_provided = bool(getattr(position, "mobile_provided", False))
|
|
20949
|
-
|
|
20950
|
-
def _coord(raw: Any, *, explicit: bool, default: int) -> int | None:
|
|
20951
|
-
# explicit=False(mobile 未提供):忽略模型默认值,沿用调用方 default(布局推断),
|
|
20952
|
-
# 与旧实现 `int(v) if provided else default` 行为一致;
|
|
20953
|
-
# explicit=True:显式 null 原样透传(后端 DashPosition null 语义,筛选组件等原生
|
|
20954
|
-
# NULL 位置读写链双向透传),仅键缺省时才回落布局默认值。
|
|
20955
|
-
if not explicit:
|
|
20956
|
-
return default
|
|
20957
|
-
if raw is None:
|
|
20958
|
-
return None
|
|
20959
|
-
return int(raw)
|
|
20960
|
-
|
|
20961
|
-
mobile_h_raw = getattr(position, "mobile_h", None) if mobile_provided else None
|
|
20962
|
-
pc_h_raw = getattr(position, "pc_h", None)
|
|
20963
|
-
if mobile_provided:
|
|
20964
|
-
# mobile 显式提供:rows 以 mobile_h 为准,null 透传(后端语义)
|
|
20965
|
-
mobile_rows = None if mobile_h_raw is None else int(mobile_h_raw)
|
|
20966
|
-
else:
|
|
20967
|
-
# mobile 未提供:rows 跟随 pc 高度(与旧实现一致);pc_h 显式 null 时回落布局默认 8
|
|
20968
|
-
mobile_rows = int(pc_h_raw) if pc_h_raw is not None else 8
|
|
20387
|
+
mobile_rows = int(getattr(position, "mobile_h", 8)) if mobile_provided else int(getattr(position, "pc_h", 8))
|
|
20969
20388
|
return {
|
|
20970
20389
|
"pc": {
|
|
20971
|
-
"x":
|
|
20972
|
-
"y":
|
|
20973
|
-
"cols":
|
|
20974
|
-
"rows":
|
|
20390
|
+
"x": int(getattr(position, "pc_x", 0)),
|
|
20391
|
+
"y": int(getattr(position, "pc_y", 0)),
|
|
20392
|
+
"cols": int(getattr(position, "pc_w", 12)),
|
|
20393
|
+
"rows": int(getattr(position, "pc_h", 8)),
|
|
20975
20394
|
},
|
|
20976
20395
|
"mobile": {
|
|
20977
|
-
"x":
|
|
20978
|
-
"y":
|
|
20979
|
-
"cols":
|
|
20396
|
+
"x": int(getattr(position, "mobile_x", 0)) if mobile_provided else 0,
|
|
20397
|
+
"y": int(getattr(position, "mobile_y", 0)) if mobile_provided else int(inferred_mobile_y),
|
|
20398
|
+
"cols": int(getattr(position, "mobile_w", 6)) if mobile_provided else 6,
|
|
20980
20399
|
"rows": mobile_rows,
|
|
20981
20400
|
},
|
|
20982
20401
|
}
|
|
@@ -21007,15 +20426,6 @@ def _portal_component_position_public(
|
|
|
21007
20426
|
elif source_name == "link":
|
|
21008
20427
|
cols = 12
|
|
21009
20428
|
rows = 2
|
|
21010
|
-
elif source_name == "carousel":
|
|
21011
|
-
cols = 24
|
|
21012
|
-
rows = 7
|
|
21013
|
-
elif source_name == "task":
|
|
21014
|
-
cols = 12
|
|
21015
|
-
rows = 8
|
|
21016
|
-
elif source_name == "favourite":
|
|
21017
|
-
cols = 12
|
|
21018
|
-
rows = 6
|
|
21019
20429
|
else:
|
|
21020
20430
|
if layout_preset == "dashboard_2col":
|
|
21021
20431
|
cols = 12
|
|
@@ -23827,23 +23237,6 @@ def _apply_field_mutation(field: dict[str, Any], mutation: Any) -> None:
|
|
|
23827
23237
|
field.pop("_question_rebuild_required", None)
|
|
23828
23238
|
|
|
23829
23239
|
|
|
23830
|
-
class AmbiguousFieldSelector(ValueError):
|
|
23831
|
-
"""name-only selector 命中多个同名字段。
|
|
23832
|
-
|
|
23833
|
-
方案 A(duplicate-field-name):后端以 queId 为字段身份、允许同父级同名;
|
|
23834
|
-
name 命中多个字段时禁止静默取第一个,必须显式失败并给出候选 queId。
|
|
23835
|
-
compiler 包装层(app_form)把它转写为 AMBIGUOUS_FIELD_SELECTOR 错误码。
|
|
23836
|
-
"""
|
|
23837
|
-
|
|
23838
|
-
def __init__(self, location: str, candidates: list[int]) -> None:
|
|
23839
|
-
self.location = location
|
|
23840
|
-
self.candidates = candidates
|
|
23841
|
-
super().__init__(
|
|
23842
|
-
f"{location} matches multiple fields with the same name; "
|
|
23843
|
-
f"use queId to disambiguate (candidate queIds: {sorted(candidates)})"
|
|
23844
|
-
)
|
|
23845
|
-
|
|
23846
|
-
|
|
23847
23240
|
def _resolve_field_selector_with_uniqueness(
|
|
23848
23241
|
*,
|
|
23849
23242
|
fields: list[dict[str, Any]],
|
|
@@ -23859,12 +23252,7 @@ def _resolve_field_selector_with_uniqueness(
|
|
|
23859
23252
|
name = str(selector.name or "").strip()
|
|
23860
23253
|
matched = [field for field in fields if str(field.get("name") or "").strip() == name]
|
|
23861
23254
|
if len(matched) > 1:
|
|
23862
|
-
|
|
23863
|
-
ref
|
|
23864
|
-
for candidate in matched
|
|
23865
|
-
if (ref := _coerce_any_int(candidate.get("que_id"))) is not None
|
|
23866
|
-
]
|
|
23867
|
-
raise AmbiguousFieldSelector(location, candidate_ids)
|
|
23255
|
+
raise ValueError(f"{location} matched multiple fields with the same name; use field_id or que_id")
|
|
23868
23256
|
else:
|
|
23869
23257
|
matched = []
|
|
23870
23258
|
if len(matched) != 1:
|
|
@@ -24166,24 +23554,6 @@ def _ensure_field_temp_ids(fields: list[dict[str, Any]]) -> None:
|
|
|
24166
23554
|
temp_id -= 100
|
|
24167
23555
|
|
|
24168
23556
|
|
|
24169
|
-
def _field_index_by_ref(fields: list[dict[str, Any]]) -> dict[int, dict[str, Any]]:
|
|
24170
|
-
"""按稳定引用(que_id,新字段退化 que_temp_id)建索引——方案 A 同名场景
|
|
24171
|
-
下 by_name 会把不同 queId 的同名字段折叠成一个,relation 目标错位(实测
|
|
24172
|
-
a9lfp40imk02:两个同名 code_block 各自输出到同名不同 queId 的 text 字段,
|
|
24173
|
-
by_name 把两条 relation 撞到同一目标,触发 multiple default-value relations)。
|
|
24174
|
-
"""
|
|
24175
|
-
index: dict[int, dict[str, Any]] = {}
|
|
24176
|
-
for field in fields:
|
|
24177
|
-
if not isinstance(field, dict):
|
|
24178
|
-
continue
|
|
24179
|
-
ref = _coerce_positive_int(field.get("que_id"))
|
|
24180
|
-
if ref is None:
|
|
24181
|
-
ref = _coerce_any_int(field.get("que_temp_id"))
|
|
24182
|
-
if ref is not None and ref not in index:
|
|
24183
|
-
index[ref] = field
|
|
24184
|
-
return index
|
|
24185
|
-
|
|
24186
|
-
|
|
24187
23557
|
def _compile_code_block_binding_fields(
|
|
24188
23558
|
*,
|
|
24189
23559
|
fields: list[dict[str, Any]],
|
|
@@ -24196,7 +23566,6 @@ def _compile_code_block_binding_fields(
|
|
|
24196
23566
|
for field in next_fields
|
|
24197
23567
|
if isinstance(field, dict) and str(field.get("name") or "")
|
|
24198
23568
|
}
|
|
24199
|
-
by_ref = _field_index_by_ref(next_fields)
|
|
24200
23569
|
existing_relations = current_schema.get("questionRelations") if isinstance(current_schema.get("questionRelations"), list) else []
|
|
24201
23570
|
relation_specs: list[dict[str, Any]] = []
|
|
24202
23571
|
affected_source_refs: set[int] = set()
|
|
@@ -24312,12 +23681,7 @@ def _compile_code_block_binding_fields(
|
|
|
24312
23681
|
target_payload = output_item.get("target_field")
|
|
24313
23682
|
if not isinstance(target_payload, dict):
|
|
24314
23683
|
continue
|
|
24315
|
-
|
|
24316
|
-
# 不折叠);以 ref 定位目标,que_id 缺失(新字段 temp 形态)才退化
|
|
24317
|
-
# by_name——by_name 对同名目标会错位到任意一个同名字段。
|
|
24318
|
-
target_field = by_ref.get(_coerce_any_int(target_payload.get("que_id")) or -1)
|
|
24319
|
-
if not isinstance(target_field, dict):
|
|
24320
|
-
target_field = by_name.get(str(target_payload.get("name") or ""))
|
|
23684
|
+
target_field = by_name.get(str(target_payload.get("name") or ""))
|
|
24321
23685
|
if not isinstance(target_field, dict):
|
|
24322
23686
|
continue
|
|
24323
23687
|
target_ref = _coerce_positive_int(target_field.get("que_id"))
|
|
@@ -24385,7 +23749,6 @@ def _materialize_code_block_question_relations(
|
|
|
24385
23749
|
relation_specs: list[dict[str, Any]],
|
|
24386
23750
|
) -> list[dict[str, Any]]:
|
|
24387
23751
|
by_name = {str(field.get("name") or ""): field for field in fields if isinstance(field, dict) and str(field.get("name") or "")}
|
|
24388
|
-
by_ref = _field_index_by_ref(fields)
|
|
24389
23752
|
materialized: list[dict[str, Any]] = []
|
|
24390
23753
|
for relation in relation_specs:
|
|
24391
23754
|
if not isinstance(relation, dict):
|
|
@@ -24393,15 +23756,8 @@ def _materialize_code_block_question_relations(
|
|
|
24393
23756
|
if _coerce_positive_int(relation.get("relationType")) == RELATION_TYPE_CODE_BLOCK:
|
|
24394
23757
|
materialized.append(deepcopy(relation))
|
|
24395
23758
|
continue
|
|
24396
|
-
|
|
24397
|
-
|
|
24398
|
-
# 同名字段场景 by_name 会折叠错位(与 _compile_* 侧同口径)。
|
|
24399
|
-
source_field = by_ref.get(_coerce_any_int(relation.get("source_field_ref")) or -1)
|
|
24400
|
-
if not isinstance(source_field, dict):
|
|
24401
|
-
source_field = by_name.get(str(relation.get("source_field_name") or ""))
|
|
24402
|
-
target_field = by_ref.get(_coerce_any_int(relation.get("target_field_ref")) or -1)
|
|
24403
|
-
if not isinstance(target_field, dict):
|
|
24404
|
-
target_field = by_name.get(str(relation.get("target_field_name") or ""))
|
|
23759
|
+
source_field = by_name.get(str(relation.get("source_field_name") or ""))
|
|
23760
|
+
target_field = by_name.get(str(relation.get("target_field_name") or ""))
|
|
24405
23761
|
if not isinstance(source_field, dict) or not isinstance(target_field, dict):
|
|
24406
23762
|
continue
|
|
24407
23763
|
source_ref = _coerce_positive_int(source_field.get("que_id"))
|
|
@@ -24525,7 +23881,6 @@ def _compile_q_linker_binding_fields(
|
|
|
24525
23881
|
for field in next_fields
|
|
24526
23882
|
if isinstance(field, dict) and str(field.get("name") or "")
|
|
24527
23883
|
}
|
|
24528
|
-
by_ref = _field_index_by_ref(next_fields)
|
|
24529
23884
|
existing_relations = current_schema.get("questionRelations") if isinstance(current_schema.get("questionRelations"), list) else []
|
|
24530
23885
|
relation_specs: list[dict[str, Any]] = []
|
|
24531
23886
|
affected_source_refs: set[int] = set()
|
|
@@ -24622,39 +23977,21 @@ def _compile_q_linker_binding_fields(
|
|
|
24622
23977
|
compiled_json_value = None if request.get("json_value") is None else str(request.get("json_value") or "")
|
|
24623
23978
|
compiled_xml_value = None if request.get("xml_value") is None else str(request.get("xml_value") or "")
|
|
24624
23979
|
for index, input_item in enumerate(cast(list[dict[str, Any]], binding.get("inputs") or [])):
|
|
24625
|
-
|
|
24626
|
-
|
|
24627
|
-
|
|
24628
|
-
|
|
24629
|
-
|
|
24630
|
-
|
|
24631
|
-
selector_payload=selector_payload,
|
|
24632
|
-
location=f"q_linker_binding.inputs[{index}].field",
|
|
24633
|
-
)
|
|
24634
|
-
except AmbiguousFieldSelector:
|
|
24635
|
-
raise
|
|
24636
|
-
except ValueError:
|
|
24637
|
-
if declared_que_id is None:
|
|
24638
|
-
raise
|
|
24639
|
-
# 透传:queId 已锚定但字段不在当前表单(存量悬空/改名引用)——后端
|
|
24640
|
-
# 写链对 params_que_id 无存在性校验,按声明 queId 直写,名称用声明名
|
|
24641
|
-
source_field = None
|
|
24642
|
-
if source_field is None:
|
|
24643
|
-
source_que_id = declared_que_id
|
|
24644
|
-
else:
|
|
24645
|
-
source_que_id = _coerce_positive_int(source_field.get("que_id"))
|
|
24646
|
-
if source_que_id is None:
|
|
24647
|
-
source_que_id = _coerce_any_int(source_field.get("que_temp_id"))
|
|
23980
|
+
source_field = _resolve_field_selector_with_uniqueness(
|
|
23981
|
+
fields=next_fields,
|
|
23982
|
+
selector_payload=cast(dict[str, Any], input_item.get("field") or {}),
|
|
23983
|
+
location=f"q_linker_binding.inputs[{index}].field",
|
|
23984
|
+
)
|
|
23985
|
+
source_que_id = _coerce_positive_int(source_field.get("que_id"))
|
|
24648
23986
|
if source_que_id is None:
|
|
24649
|
-
|
|
24650
|
-
|
|
24651
|
-
|
|
24652
|
-
compose_name = declared_name or str((source_field or {}).get("name") or "")
|
|
23987
|
+
source_que_id = _coerce_any_int(source_field.get("que_temp_id"))
|
|
23988
|
+
if source_que_id is None:
|
|
23989
|
+
raise ValueError(f"q_linker input field '{source_field.get('name')}' does not have a stable que_id")
|
|
24653
23990
|
key = str(input_item.get("key") or "").strip()
|
|
24654
23991
|
source_kind = str(input_item.get("source") or "").strip().lower()
|
|
24655
23992
|
compiled_inputs.append(
|
|
24656
23993
|
{
|
|
24657
|
-
"field": {"name":
|
|
23994
|
+
"field": {"name": source_field.get("name"), "que_id": source_que_id},
|
|
24658
23995
|
"key": key,
|
|
24659
23996
|
"source": source_kind,
|
|
24660
23997
|
}
|
|
@@ -24668,7 +24005,7 @@ def _compile_q_linker_binding_fields(
|
|
|
24668
24005
|
compiled_url_encoded.append(kv_item)
|
|
24669
24006
|
elif source_kind == "json_path":
|
|
24670
24007
|
token = "{{" + key + "}}"
|
|
24671
|
-
reference = _compose_qlinker_value_reference(field_name=
|
|
24008
|
+
reference = _compose_qlinker_value_reference(field_name=str(source_field.get("name") or ""), que_id=source_que_id)
|
|
24672
24009
|
compiled_url = compiled_url.replace(token, reference)
|
|
24673
24010
|
if compiled_json_value is not None:
|
|
24674
24011
|
compiled_json_value = compiled_json_value.replace(token, reference)
|
|
@@ -24754,12 +24091,7 @@ def _compile_q_linker_binding_fields(
|
|
|
24754
24091
|
target_payload = output_item.get("target_field")
|
|
24755
24092
|
if not isinstance(target_payload, dict):
|
|
24756
24093
|
continue
|
|
24757
|
-
|
|
24758
|
-
# 不折叠);以 ref 定位目标,que_id 缺失(新字段 temp 形态)才退化
|
|
24759
|
-
# by_name——by_name 对同名目标会错位到任意一个同名字段。
|
|
24760
|
-
target_field = by_ref.get(_coerce_any_int(target_payload.get("que_id")) or -1)
|
|
24761
|
-
if not isinstance(target_field, dict):
|
|
24762
|
-
target_field = by_name.get(str(target_payload.get("name") or ""))
|
|
24094
|
+
target_field = by_name.get(str(target_payload.get("name") or ""))
|
|
24763
24095
|
if not isinstance(target_field, dict):
|
|
24764
24096
|
continue
|
|
24765
24097
|
target_ref = _coerce_positive_int(target_field.get("que_id"))
|
|
@@ -24817,20 +24149,12 @@ def _materialize_q_linker_question_relations(
|
|
|
24817
24149
|
relation_specs: list[dict[str, Any]],
|
|
24818
24150
|
) -> list[dict[str, Any]]:
|
|
24819
24151
|
by_name = {str(field.get("name") or ""): field for field in fields if isinstance(field, dict) and str(field.get("name") or "")}
|
|
24820
|
-
by_ref = _field_index_by_ref(fields)
|
|
24821
24152
|
materialized: list[dict[str, Any]] = []
|
|
24822
24153
|
for relation in relation_specs:
|
|
24823
24154
|
if not isinstance(relation, dict):
|
|
24824
24155
|
continue
|
|
24825
|
-
|
|
24826
|
-
|
|
24827
|
-
# 同名字段场景 by_name 会折叠错位(与 _compile_* 侧同口径)。
|
|
24828
|
-
source_field = by_ref.get(_coerce_any_int(relation.get("source_field_ref")) or -1)
|
|
24829
|
-
if not isinstance(source_field, dict):
|
|
24830
|
-
source_field = by_name.get(str(relation.get("source_field_name") or ""))
|
|
24831
|
-
target_field = by_ref.get(_coerce_any_int(relation.get("target_field_ref")) or -1)
|
|
24832
|
-
if not isinstance(target_field, dict):
|
|
24833
|
-
target_field = by_name.get(str(relation.get("target_field_name") or ""))
|
|
24156
|
+
source_field = by_name.get(str(relation.get("source_field_name") or ""))
|
|
24157
|
+
target_field = by_name.get(str(relation.get("target_field_name") or ""))
|
|
24834
24158
|
if not isinstance(source_field, dict) or not isinstance(target_field, dict):
|
|
24835
24159
|
continue
|
|
24836
24160
|
source_ref = _coerce_positive_int(source_field.get("que_id"))
|
|
@@ -28460,385 +27784,6 @@ def _normalize_portal_components(components: Any) -> list[dict[str, Any]]:
|
|
|
28460
27784
|
return items
|
|
28461
27785
|
|
|
28462
27786
|
|
|
28463
|
-
def _portal_component_source_type_by_backend(value: Any) -> str:
|
|
28464
|
-
raw = value if isinstance(value, (int, str)) else str(value or "").strip()
|
|
28465
|
-
mapping = {
|
|
28466
|
-
1: "chart",
|
|
28467
|
-
2: "grid",
|
|
28468
|
-
3: "carousel",
|
|
28469
|
-
4: "link",
|
|
28470
|
-
5: "text",
|
|
28471
|
-
6: "filter",
|
|
28472
|
-
7: "favourite",
|
|
28473
|
-
8: "task",
|
|
28474
|
-
9: "chart",
|
|
28475
|
-
10: "view",
|
|
28476
|
-
}
|
|
28477
|
-
if isinstance(raw, int):
|
|
28478
|
-
return mapping.get(raw, "unknown")
|
|
28479
|
-
raw_str = str(raw or "").strip()
|
|
28480
|
-
if raw_str.isdigit():
|
|
28481
|
-
return mapping.get(int(raw_str), "unknown")
|
|
28482
|
-
return _normalize_portal_component_source_type(raw)
|
|
28483
|
-
|
|
28484
|
-
|
|
28485
|
-
def _portal_section_from_component(
|
|
28486
|
-
component: dict[str, Any],
|
|
28487
|
-
*,
|
|
28488
|
-
index: int,
|
|
28489
|
-
complete: bool,
|
|
28490
|
-
) -> tuple[dict[str, Any] | None, dict[str, Any] | None]:
|
|
28491
|
-
"""Reverse-compile one DashComponentInfo into a canonical section."""
|
|
28492
|
-
source_type = _portal_component_source_type_by_backend(component.get("type"))
|
|
28493
|
-
config_key_map = {
|
|
28494
|
-
"grid": "gridConfig",
|
|
28495
|
-
"link": "linkConfig",
|
|
28496
|
-
"text": "textConfig",
|
|
28497
|
-
"filter": "filterConfig",
|
|
28498
|
-
"chart": "chartConfig",
|
|
28499
|
-
"view": "viewgraphConfig",
|
|
28500
|
-
"carousel": "carouselConfig",
|
|
28501
|
-
"task": "taskConfig",
|
|
28502
|
-
"favourite": "favouriteConfig",
|
|
28503
|
-
}
|
|
28504
|
-
if source_type == "unknown":
|
|
28505
|
-
return None, {
|
|
28506
|
-
"index": index,
|
|
28507
|
-
"code": "UNKNOWN_COMPONENT_TYPE",
|
|
28508
|
-
"message": f"component type {component.get('type')} is not part of the portals declaration contract",
|
|
28509
|
-
"blocking_field": "source_type",
|
|
28510
|
-
}
|
|
28511
|
-
config = component.get(config_key_map.get(source_type, "")) if isinstance(component.get(config_key_map.get(source_type, "")), dict) else {}
|
|
28512
|
-
section: dict[str, Any] = {"source_type": source_type}
|
|
28513
|
-
title = _extract_portal_component_title(component, source_type=source_type)
|
|
28514
|
-
gaps: list[dict[str, Any]] = []
|
|
28515
|
-
if source_type == "carousel":
|
|
28516
|
-
title = str(config.get("sourceApp", {}).get("formTitle") or "").strip() or f"轮播 {index + 1}"
|
|
28517
|
-
elif source_type == "task":
|
|
28518
|
-
title = str(config.get("componentTaskTitle") or f"待办 {index + 1}")
|
|
28519
|
-
elif source_type == "favourite":
|
|
28520
|
-
title = str(config.get("favouriteTitle") or f"收藏 {index + 1}")
|
|
28521
|
-
# chart/grid/view 的 title 语义=组件显示标题(component_title/grid_title 落库列),
|
|
28522
|
-
# 生产存量大量存在「显示标题开但标题空」形态(读链渲染时回退图表名/视图名):
|
|
28523
|
-
# 合成「组件 N」会把不存在的标题落库(读链 chartName 派生被覆盖 + 库表漂移),
|
|
28524
|
-
# 如实保留 None(编译不落库,round-trip 保真)。carousel/task/favourite 的
|
|
28525
|
-
# 读面自有真名回填(formTitle/componentTaskTitle/favouriteTitle),其合成兑底保留。
|
|
28526
|
-
# filter 同样不合成:后端 addComponentInfos 对筛选组件不落任何标题(无标题存储),
|
|
28527
|
-
# 读面按代理键 HashMap 迭代序返回——合成「组件 {index+1}」的标签是列表序号的
|
|
28528
|
-
# 函数,apply 删重建致代理键位移后标题跟着置换(r6 幂等差 3 的唯一根因)。
|
|
28529
|
-
if source_type not in {"chart", "grid", "view", "filter"}:
|
|
28530
|
-
title = title or f"组件 {index + 1}"
|
|
28531
|
-
section["title"] = title
|
|
28532
|
-
if source_type in {"chart", "view"} and complete:
|
|
28533
|
-
# beingShowTitle 落库列(draft_dash_chart.being_show_title):读面必有值;
|
|
28534
|
-
# 提取必须保真(存量 false 被「硬编码 True」覆写会漂移库表)。grid 的开关
|
|
28535
|
-
# 在 grid_config.beingShowTitle 键内随 config 保留,无需单独建模。
|
|
28536
|
-
if "beingShowTitle" in config:
|
|
28537
|
-
section["show_title"] = bool(config.get("beingShowTitle"))
|
|
28538
|
-
position = component.get("position")
|
|
28539
|
-
if isinstance(position, dict) and position:
|
|
28540
|
-
section["position"] = deepcopy(position)
|
|
28541
|
-
if complete:
|
|
28542
|
-
style = component.get("dashStyleConfigBO")
|
|
28543
|
-
if isinstance(style, dict) and style:
|
|
28544
|
-
section["style_config"] = deepcopy(style)
|
|
28545
|
-
if source_type == "chart":
|
|
28546
|
-
# 后端两类图表组件引用面不同(DashSvcImpl 写链):type=1 轻流报表锚 chart 表唯一键
|
|
28547
|
-
# chartKey(addChartInfos);type=9 轻析 BI 报表锚 bi_chart 整型 chart_id(addBiChartInfo)。
|
|
28548
|
-
# 提取时以读面 type 判别 ref_kind,保证 apply 回写同一引用面。
|
|
28549
|
-
backend_type = component.get("type")
|
|
28550
|
-
is_qingflow_chart = backend_type == 1 or (
|
|
28551
|
-
backend_type != 9 and str(config.get("chartKey") or "").strip() and not config.get("biChartId")
|
|
28552
|
-
)
|
|
28553
|
-
if is_qingflow_chart:
|
|
28554
|
-
chart_ref: dict[str, Any] = {
|
|
28555
|
-
"ref_kind": "chart",
|
|
28556
|
-
"chart_id": str(config.get("chartKey") or "").strip() or None,
|
|
28557
|
-
"chart_name": str(config.get("chartName") or title or "").strip() or None,
|
|
28558
|
-
}
|
|
28559
|
-
else:
|
|
28560
|
-
chart_ref = {
|
|
28561
|
-
"chart_id": str(config.get("biChartId") or "").strip() or None,
|
|
28562
|
-
"chart_name": str(config.get("chartComponentTitle") or title or "").strip() or None,
|
|
28563
|
-
}
|
|
28564
|
-
chart_ref = {key: value for key, value in chart_ref.items() if value is not None}
|
|
28565
|
-
if not chart_ref.get("chart_id"):
|
|
28566
|
-
gaps.append({
|
|
28567
|
-
"index": index,
|
|
28568
|
-
"code": "CHART_REF_MISSING",
|
|
28569
|
-
"message": "chart component has no chartKey/biChartId in the read face",
|
|
28570
|
-
"blocking_field": "chart_ref.chart_id",
|
|
28571
|
-
})
|
|
28572
|
-
return None, gaps[0]
|
|
28573
|
-
section["chart_ref"] = chart_ref
|
|
28574
|
-
elif source_type == "view":
|
|
28575
|
-
view_ref: dict[str, Any] = {
|
|
28576
|
-
"app_key": str(config.get("appKey") or "").strip() or None,
|
|
28577
|
-
"view_key": str(config.get("viewgraphKey") or "").strip() or None,
|
|
28578
|
-
"view_name": str(config.get("viewgraphName") or title or "").strip() or None,
|
|
28579
|
-
}
|
|
28580
|
-
view_ref = {key: value for key, value in view_ref.items() if value is not None}
|
|
28581
|
-
if not view_ref.get("view_key"):
|
|
28582
|
-
gaps.append({
|
|
28583
|
-
"index": index,
|
|
28584
|
-
"code": "VIEW_REF_MISSING",
|
|
28585
|
-
"message": "view component has no viewgraphKey in the read face",
|
|
28586
|
-
"blocking_field": "view_ref.view_key",
|
|
28587
|
-
})
|
|
28588
|
-
return None, gaps[0]
|
|
28589
|
-
section["view_ref"] = view_ref
|
|
28590
|
-
if complete and ("beingShowTitle" in config or config.get("componentTitle")):
|
|
28591
|
-
display: dict[str, Any] = {}
|
|
28592
|
-
if "beingShowTitle" in config:
|
|
28593
|
-
display["show_title"] = bool(config.get("beingShowTitle"))
|
|
28594
|
-
if config.get("componentTitle"):
|
|
28595
|
-
display["component_title"] = str(config.get("componentTitle"))
|
|
28596
|
-
if display:
|
|
28597
|
-
section["view_display"] = display
|
|
28598
|
-
elif source_type == "grid":
|
|
28599
|
-
grid_config: dict[str, Any] = {}
|
|
28600
|
-
for key in ("gridTitle", "beingShowTitle", "items"):
|
|
28601
|
-
if key in config:
|
|
28602
|
-
grid_config[key] = deepcopy(config.get(key))
|
|
28603
|
-
if isinstance(grid_config.get("items"), list):
|
|
28604
|
-
# 归一到声明规范键集(DraftComponentGridItem 落库列 + 读面派生 title):
|
|
28605
|
-
# 丢弃运行态/代理键(id/gridId/beingShowTitle/beingTrial/trialExpireDate——
|
|
28606
|
-
# 前者是自增代理键(比对器另剔),后两者读面运行态不入库,title/iconUrl 由
|
|
28607
|
-
# setTitleAndIconByType 按引用派生);空 title 条目仍过滤(后端读面同型)。
|
|
28608
|
-
kept_item_keys = {
|
|
28609
|
-
"type", "jumpMode", "linkAppKey", "linkFormType", "linkDashKey",
|
|
28610
|
-
"linkTagId", "linkUrl", "title", "customTitle", "beingCustomTitle",
|
|
28611
|
-
"iconUrl", "ordinal",
|
|
28612
|
-
}
|
|
28613
|
-
normalized_items: list[dict[str, Any]] = []
|
|
28614
|
-
for item in grid_config["items"]:
|
|
28615
|
-
if not isinstance(item, dict) or not str(item.get("title") or "").strip():
|
|
28616
|
-
continue
|
|
28617
|
-
normalized = {
|
|
28618
|
-
key: deepcopy(value)
|
|
28619
|
-
for key, value in item.items()
|
|
28620
|
-
if key in kept_item_keys and value is not None
|
|
28621
|
-
}
|
|
28622
|
-
# iconUrl 语义按条目类型分裂(Java ComponentGridItemInfo#setTitleAndIconByType):
|
|
28623
|
-
# type 1/2/3(应用/门户/应用包)的 iconUrl 与 title 是读面按引用目标当前
|
|
28624
|
-
# 图标实时派生的投影——门户声明不拥有也无法保真(写侧不回写生效值),
|
|
28625
|
-
# 提取侧剔除;自定义条目 iconUrl 落库原样读回,保留 round-trip。
|
|
28626
|
-
# 空串 iconUrl 统一剔除(与 favourite 提取对称,消 ""↔缺键不对称)。
|
|
28627
|
-
if _coerce_positive_int(normalized.get("type")) in {1, 2, 3} or not str(normalized.get("iconUrl") or "").strip():
|
|
28628
|
-
normalized.pop("iconUrl", None)
|
|
28629
|
-
normalized_items.append(normalized)
|
|
28630
|
-
grid_config["items"] = normalized_items
|
|
28631
|
-
section["config"] = grid_config
|
|
28632
|
-
elif source_type == "text":
|
|
28633
|
-
section["text"] = str(config.get("text") or "")
|
|
28634
|
-
if complete and config:
|
|
28635
|
-
extra_config = {key: deepcopy(value) for key, value in config.items() if key not in {"text"}}
|
|
28636
|
-
if extra_config:
|
|
28637
|
-
section["config"] = extra_config
|
|
28638
|
-
elif source_type == "link":
|
|
28639
|
-
section["url"] = str(config.get("url") or "")
|
|
28640
|
-
if complete and "beingLoginAuth" in config:
|
|
28641
|
-
section["login_auth"] = bool(config.get("beingLoginAuth"))
|
|
28642
|
-
elif source_type == "filter":
|
|
28643
|
-
graph_list = config.get("graphList") if isinstance(config.get("graphList"), list) else []
|
|
28644
|
-
graphs = [
|
|
28645
|
-
{
|
|
28646
|
-
"chart_key": str(item.get("graphKey") or "").strip() or None,
|
|
28647
|
-
"chart_name": str(item.get("graphName") or "").strip() or None,
|
|
28648
|
-
}
|
|
28649
|
-
for item in graph_list
|
|
28650
|
-
if isinstance(item, dict)
|
|
28651
|
-
]
|
|
28652
|
-
graphs = [
|
|
28653
|
-
{key: value for key, value in item.items() if value is not None}
|
|
28654
|
-
for item in graphs
|
|
28655
|
-
]
|
|
28656
|
-
rules: list[dict[str, Any]] = []
|
|
28657
|
-
for group in config.get("filterConfig") or []:
|
|
28658
|
-
if not isinstance(group, dict):
|
|
28659
|
-
continue
|
|
28660
|
-
# 剔除后端删除重建的代理键(filterGroupId/filterItemId,自增 id 每次位移,七条约束之六):
|
|
28661
|
-
# 声明级比以此归一,规则语义真源是 filterGroupName/filterItemName/judge 系内容列。
|
|
28662
|
-
rules.append({
|
|
28663
|
-
key: deepcopy(value)
|
|
28664
|
-
for key, value in group.items()
|
|
28665
|
-
if key not in {"filterGroupId"}
|
|
28666
|
-
})
|
|
28667
|
-
for group in rules:
|
|
28668
|
-
for item in group.get("filterGroupConfig") or []:
|
|
28669
|
-
if isinstance(item, dict):
|
|
28670
|
-
item.pop("filterItemId", None)
|
|
28671
|
-
section["config"] = {"graphs": graphs, "rules": rules}
|
|
28672
|
-
elif source_type == "carousel":
|
|
28673
|
-
carousel: dict[str, Any] = {}
|
|
28674
|
-
source_app = config.get("sourceApp") if isinstance(config.get("sourceApp"), dict) else {}
|
|
28675
|
-
if source_app.get("appKey"):
|
|
28676
|
-
carousel["source_app_key"] = str(source_app.get("appKey"))
|
|
28677
|
-
if source_app.get("formTitle"):
|
|
28678
|
-
carousel["source_form_title"] = str(source_app.get("formTitle"))
|
|
28679
|
-
for raw_key, target_key in (("titleQue", "title_field"), ("imageQue", "image_field"), ("linkQue", "link_field")):
|
|
28680
|
-
que = config.get(raw_key) if isinstance(config.get(raw_key), dict) else {}
|
|
28681
|
-
if que.get("queId") is not None:
|
|
28682
|
-
carousel[target_key] = str(que.get("queId"))
|
|
28683
|
-
if "jumpMode" in config:
|
|
28684
|
-
carousel["jump_mode"] = config.get("jumpMode")
|
|
28685
|
-
if "beingScaling" in config:
|
|
28686
|
-
carousel["scaling"] = bool(config.get("beingScaling"))
|
|
28687
|
-
if "showLatestLimit" in config:
|
|
28688
|
-
carousel["show_latest_limit"] = config.get("showLatestLimit")
|
|
28689
|
-
if "scrollInterval" in config:
|
|
28690
|
-
carousel["scroll_interval"] = config.get("scrollInterval")
|
|
28691
|
-
filter_config = config.get("filterConfig")
|
|
28692
|
-
if isinstance(filter_config, list) and filter_config:
|
|
28693
|
-
# ComponentCarouselConfig.filterConfig 是单组 AND 的 MatchRule 列表。
|
|
28694
|
-
# DashSvcImpl#getDashDetail 会补 judgeValueDetails/judgeType;复用门户既有
|
|
28695
|
-
# match-rule 映射,优先以 details 作为选项值真源,避免只回传派生 judgeValues。
|
|
28696
|
-
raw_rules = filter_config[0] if isinstance(filter_config[0], list) else filter_config
|
|
28697
|
-
filters: list[dict[str, Any]] = []
|
|
28698
|
-
for rule in raw_rules:
|
|
28699
|
-
if not isinstance(rule, dict):
|
|
28700
|
-
continue
|
|
28701
|
-
que_id = _coerce_positive_int(rule.get("queId"))
|
|
28702
|
-
if que_id is None:
|
|
28703
|
-
gaps.append({
|
|
28704
|
-
"index": index,
|
|
28705
|
-
"code": "CAROUSEL_FILTER_FIELD_UNRESOLVED",
|
|
28706
|
-
"message": "carousel filter rule has no resolvable queId",
|
|
28707
|
-
"blocking_field": "carousel.filters",
|
|
28708
|
-
})
|
|
28709
|
-
continue
|
|
28710
|
-
field = {"que_id": que_id, "name": str(rule.get("queTitle") or que_id)}
|
|
28711
|
-
values = _public_view_filter_rule_values(rule, field=field)
|
|
28712
|
-
operator = _public_view_filter_operator_from_judge_type(rule.get("judgeType"), values=values)
|
|
28713
|
-
if operator.startswith("judge_type:"):
|
|
28714
|
-
gaps.append({
|
|
28715
|
-
"index": index,
|
|
28716
|
-
"code": "CAROUSEL_FILTER_OPERATOR_UNSUPPORTED",
|
|
28717
|
-
"message": "carousel filter rule judgeType is not expressible in v1alpha1",
|
|
28718
|
-
"blocking_field": "carousel.filters",
|
|
28719
|
-
"judge_type": rule.get("judgeType"),
|
|
28720
|
-
})
|
|
28721
|
-
continue
|
|
28722
|
-
item: dict[str, Any] = {"field_name": str(que_id), "operator": operator}
|
|
28723
|
-
if operator not in {"is_empty", "not_empty"}:
|
|
28724
|
-
item["values"] = values
|
|
28725
|
-
filters.append(item)
|
|
28726
|
-
if filters:
|
|
28727
|
-
carousel["filters"] = filters
|
|
28728
|
-
section["carousel"] = carousel
|
|
28729
|
-
elif source_type == "task":
|
|
28730
|
-
task: dict[str, Any] = {}
|
|
28731
|
-
if "beingShowTitle" in config:
|
|
28732
|
-
task["show_title"] = bool(config.get("beingShowTitle"))
|
|
28733
|
-
if config.get("componentTaskHint") is not None:
|
|
28734
|
-
task["hint"] = str(config.get("componentTaskHint") or "")
|
|
28735
|
-
if "beingShowHint" in config:
|
|
28736
|
-
task["show_hint"] = bool(config.get("beingShowHint"))
|
|
28737
|
-
if "beingUsing" in config:
|
|
28738
|
-
task["enabled"] = bool(config.get("beingUsing"))
|
|
28739
|
-
raw_tasks = config.get("dashTaskConfigList")
|
|
28740
|
-
if isinstance(raw_tasks, list) and raw_tasks:
|
|
28741
|
-
task["tasks"] = [
|
|
28742
|
-
{
|
|
28743
|
-
"title": str(item.get("title") or ""),
|
|
28744
|
-
"task_type": item.get("type"),
|
|
28745
|
-
"ordinal": item.get("ordinal"),
|
|
28746
|
-
"checked": bool(item.get("beingCheck")),
|
|
28747
|
-
}
|
|
28748
|
-
for item in raw_tasks
|
|
28749
|
-
if isinstance(item, dict)
|
|
28750
|
-
]
|
|
28751
|
-
section["task"] = task
|
|
28752
|
-
elif source_type == "favourite":
|
|
28753
|
-
favourite: dict[str, Any] = {}
|
|
28754
|
-
if "beingShowTitle" in config:
|
|
28755
|
-
favourite["show_title"] = bool(config.get("beingShowTitle"))
|
|
28756
|
-
if "beingUsing" in config:
|
|
28757
|
-
favourite["enabled"] = bool(config.get("beingUsing"))
|
|
28758
|
-
raw_items = config.get("items")
|
|
28759
|
-
if isinstance(raw_items, list) and raw_items:
|
|
28760
|
-
favourite["items"] = [
|
|
28761
|
-
{
|
|
28762
|
-
"title": str(item.get("title") or ""),
|
|
28763
|
-
"item_type": item.get("type"),
|
|
28764
|
-
"link_key": str(item.get("linkKey") or "") or None,
|
|
28765
|
-
"link_id": item.get("linkId"),
|
|
28766
|
-
"icon_url": str(item.get("iconUrl") or "") or None,
|
|
28767
|
-
"jump_mode": item.get("jumpMode"),
|
|
28768
|
-
"ordinal": item.get("ordinal"),
|
|
28769
|
-
}
|
|
28770
|
-
for item in raw_items
|
|
28771
|
-
if isinstance(item, dict)
|
|
28772
|
-
]
|
|
28773
|
-
favourite["items"] = [
|
|
28774
|
-
{key: value for key, value in item.items() if value is not None}
|
|
28775
|
-
for item in favourite["items"]
|
|
28776
|
-
]
|
|
28777
|
-
section["favourite"] = favourite
|
|
28778
|
-
return section, (gaps[0] if gaps else None)
|
|
28779
|
-
|
|
28780
|
-
|
|
28781
|
-
def _portal_declaration_from_dash_detail(
|
|
28782
|
-
detail: dict[str, Any],
|
|
28783
|
-
*,
|
|
28784
|
-
schema_kind: str,
|
|
28785
|
-
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
28786
|
-
"""Reverse-compile a DashDetailInfo read into a canonical portal declaration."""
|
|
28787
|
-
complete = schema_kind == "complete"
|
|
28788
|
-
gaps: list[dict[str, Any]] = []
|
|
28789
|
-
sections: list[dict[str, Any]] = []
|
|
28790
|
-
raw_components = detail.get("components") if isinstance(detail.get("components"), list) else []
|
|
28791
|
-
for index, component in enumerate(raw_components):
|
|
28792
|
-
if not isinstance(component, dict):
|
|
28793
|
-
continue
|
|
28794
|
-
section, gap = _portal_section_from_component(component, index=index, complete=complete)
|
|
28795
|
-
if gap is not None:
|
|
28796
|
-
gaps.append(gap)
|
|
28797
|
-
if section is not None:
|
|
28798
|
-
sections.append(section)
|
|
28799
|
-
declaration: dict[str, Any] = {"sections": sections}
|
|
28800
|
-
# 后端 dash_name 允许空串(生产存量存在,updateDashBaseInfo 无非空校验):
|
|
28801
|
-
# 提取侧始终写出 name 键,空名透传(round-trip 保真),不再把空名当成缺省。
|
|
28802
|
-
declaration["name"] = str(detail.get("dashName") or "").strip()
|
|
28803
|
-
tags = detail.get("tags") if isinstance(detail.get("tags"), list) else []
|
|
28804
|
-
first_tag = tags[0] if tags and isinstance(tags[0], dict) else {}
|
|
28805
|
-
tag_id = _coerce_positive_int(first_tag.get("tagId"))
|
|
28806
|
-
if tag_id is not None:
|
|
28807
|
-
declaration["package_tag_id"] = tag_id
|
|
28808
|
-
if not complete:
|
|
28809
|
-
return declaration, gaps
|
|
28810
|
-
dash_icon = str(detail.get("dashIcon") or "").strip() or None
|
|
28811
|
-
if dash_icon:
|
|
28812
|
-
icon_config = workspace_icon_config(dash_icon)
|
|
28813
|
-
icon = str(icon_config.get("icon_name") or "").strip() or None
|
|
28814
|
-
color = str(icon_config.get("icon_color") or "").strip() or None
|
|
28815
|
-
if icon:
|
|
28816
|
-
declaration["icon"] = icon
|
|
28817
|
-
if color:
|
|
28818
|
-
declaration["color"] = color
|
|
28819
|
-
if "hideCopyright" in detail:
|
|
28820
|
-
declaration["hide_copyright"] = bool(detail.get("hideCopyright"))
|
|
28821
|
-
global_config = detail.get("dashGlobalConfig") if isinstance(detail.get("dashGlobalConfig"), dict) else {}
|
|
28822
|
-
if global_config:
|
|
28823
|
-
declaration["global_config"] = {}
|
|
28824
|
-
if "interval" in global_config:
|
|
28825
|
-
declaration["global_config"]["interval"] = global_config.get("interval")
|
|
28826
|
-
if "beingAutoRefresh" in global_config:
|
|
28827
|
-
declaration["global_config"]["auto_refresh"] = bool(global_config.get("beingAutoRefresh"))
|
|
28828
|
-
if "beingExport" in global_config:
|
|
28829
|
-
declaration["global_config"]["export"] = bool(global_config.get("beingExport"))
|
|
28830
|
-
visibility = _public_visibility_from_member_auth(detail.get("auth"))
|
|
28831
|
-
if isinstance(visibility, dict) and visibility:
|
|
28832
|
-
declaration["visibility"] = visibility
|
|
28833
|
-
style_template_id = _coerce_positive_int(detail.get("styleTemplateId"))
|
|
28834
|
-
if style_template_id is not None:
|
|
28835
|
-
declaration["style_template_id"] = style_template_id
|
|
28836
|
-
overall = detail.get("overallConfigBO")
|
|
28837
|
-
if isinstance(overall, dict) and overall:
|
|
28838
|
-
declaration["overall_style"] = deepcopy(overall)
|
|
28839
|
-
return declaration, gaps
|
|
28840
|
-
|
|
28841
|
-
|
|
28842
27787
|
def _custom_button_view_configs_from_view_summaries(views: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
28843
27788
|
view_configs: list[dict[str, Any]] = []
|
|
28844
27789
|
for view in views:
|
|
@@ -28992,10 +27937,7 @@ def _extract_portal_component_title(component: dict[str, Any], *, source_type: s
|
|
|
28992
27937
|
"text": ["title", "textTitle"],
|
|
28993
27938
|
"filter": ["title", "filterTitle"],
|
|
28994
27939
|
"chart": ["chartComponentTitle", "componentTitle", "title"],
|
|
28995
|
-
|
|
28996
|
-
# (viewgraphName)是引用面锚进 view_ref.view_name,不再混入 title 候选
|
|
28997
|
-
# (旧候选把它兑底进 title,编译时覆写 componentTitle 造成库表漂移)。
|
|
28998
|
-
"view": ["componentTitle", "title"],
|
|
27940
|
+
"view": ["componentTitle", "viewgraphName", "title"],
|
|
28999
27941
|
}
|
|
29000
27942
|
for key in title_candidates.get(source_type, []):
|
|
29001
27943
|
title = str(config.get(key) or "").strip()
|
|
@@ -30990,14 +29932,6 @@ def _build_view_update_payload(
|
|
|
30990
29932
|
payload["viewgraphType"] = "calendarView"
|
|
30991
29933
|
payload["beingShowTitleQue"] = True
|
|
30992
29934
|
payload.pop("groupQueId", None)
|
|
30993
|
-
elif normalized_type == "hierarchy":
|
|
30994
|
-
payload["viewgraphType"] = "hierarchyView"
|
|
30995
|
-
payload["beingShowTitleQue"] = True
|
|
30996
|
-
payload.pop("groupQueId", None)
|
|
30997
|
-
elif normalized_type == "calendar":
|
|
30998
|
-
payload["viewgraphType"] = "calendarView"
|
|
30999
|
-
payload["beingShowTitleQue"] = True
|
|
31000
|
-
payload.pop("groupQueId", None)
|
|
31001
29935
|
return _hydrate_view_backend_payload(
|
|
31002
29936
|
payload=payload,
|
|
31003
29937
|
view_type=normalized_type,
|
|
@@ -31557,9 +30491,6 @@ def _translate_view_filter_rule(*, field: dict[str, Any], rule: dict[str, Any])
|
|
|
31557
30491
|
elif operator == "in":
|
|
31558
30492
|
payload["judgeType"] = JUDGE_INCLUDE_ANY if field_type in INCLUDE_ANY_FLOW_FIELD_TYPES else JUDGE_EQUAL_ANY
|
|
31559
30493
|
payload["judgeValues"] = judge_values
|
|
31560
|
-
elif operator == "belongs_to":
|
|
31561
|
-
payload["judgeType"] = JUDGE_BELONG_TO
|
|
31562
|
-
payload["judgeValues"] = judge_values
|
|
31563
30494
|
elif operator == "contains":
|
|
31564
30495
|
payload["judgeType"] = JUDGE_INCLUDE
|
|
31565
30496
|
payload["judgeValues"] = judge_values[:1] if judge_values else []
|
|
@@ -31881,8 +30812,6 @@ def _public_view_filter_operator_from_judge_type(judge_type: Any, *, values: lis
|
|
|
31881
30812
|
# ANY 判型+空值 = "该字段未选任何值"的合法存量形态(judgeType 保真回写,
|
|
31882
30813
|
# 转 is_empty 会丢判型——后端 9 与 0 是不同判定域,实测回读不同)
|
|
31883
30814
|
return "in"
|
|
31884
|
-
if normalized == JUDGE_BELONG_TO:
|
|
31885
|
-
return "belongs_to"
|
|
31886
30815
|
if normalized == JUDGE_GREATER_OR_EQUAL:
|
|
31887
30816
|
return "gte"
|
|
31888
30817
|
if normalized == JUDGE_LESS_OR_EQUAL:
|