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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/README.md +3 -3
  2. package/package.json +1 -1
  3. package/pyproject.toml +2 -1
  4. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_CHARTS_WORKFLOW.md +1 -1
  5. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_PORTAL_WORKFLOW.md +6 -258
  6. package/skills/qingflow-cli/reference/builder/70-portal.md +31 -16
  7. package/src/qingflow_mcp/builder_facade/models.py +27 -4
  8. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/complete.json +7 -7
  9. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/carousel.json +1 -1
  10. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/chart.json +1 -1
  11. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/favourite.json +1 -1
  12. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/filter.json +45 -64
  13. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/grid.json +1 -1
  14. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/link.json +1 -1
  15. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/task.json +2 -2
  16. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/text.json +1 -1
  17. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/view.json +1 -1
  18. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/summary.json +4 -4
  19. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/complete.json +1 -1
  20. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/display.json +1 -1
  21. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/summary.json +1 -1
  22. package/src/qingflow_mcp/builder_facade/service.py +158 -28
  23. package/src/qingflow_mcp/cli/commands/builder.py +20 -2
  24. package/src/qingflow_mcp/server_app_builder.py +2 -2
  25. package/src/qingflow_mcp/tools/ai_builder_tools.py +8 -0
@@ -7,7 +7,7 @@
7
7
  "Position": {
8
8
  "type": "object",
9
9
  "additionalProperties": false,
10
- "positionShapeDescription": "网格位置。PC 是 24 列网格(cols≤24),mobile 是 6 列网格(cols≤6);同行组件共享同一 y 与 rows,一行 cols 合计 24。mobile 可省略(缺省与 pc 同布局)。平铺别名 pc_x/pc_y/pc_w/pc_h、mobile_* 也接受,归一为本结构。标准行高:指标卡 rows=5、BI 图表 rows=7、数据视图 rows=11、业务入口 rows=4。",
10
+ "positionShapeDescription": "网格位置。PC 是 24 列网格(cols≤24),mobile 是 6 列网格(cols≤6);同行组件共享同一 y 与 rows,一行 cols 合计 24。mobile 可省略(缺省与 pc 同布局)。平铺别名 pc_x/pc_y/pc_w/pc_h、mobile_* 也接受(x/y 与 w/h/cols/rows 混写均认),归一为本结构。标准行高:指标卡 rows=5、BI 图表 rows=7、数据视图 rows=11、业务入口 rows=4。",
11
11
  "properties": {
12
12
  "pc": {
13
13
  "$ref": "#/$defs/PositionDetail"
@@ -57,9 +57,9 @@
57
57
  "type": "object",
58
58
  "additionalProperties": false,
59
59
  "required": [
60
- "groups"
60
+ "rules"
61
61
  ],
62
- "filterConfigShapeDescription": "筛选组件配置。筛选语义 raw/unstable,不建议作为主自动化路径;条件明细为受控透传(键集以 ws40013 dataset 实测形态定稿)。",
62
+ "filterConfigShapeDescription": "筛选组件配置。rules 必须非空——后端读面(DashSvcImpl#getComponentFilter)按筛选组(dash_filter_group 行)反查筛选组件,空规则的 filter 组件行虽会写入但读面枚举不到,表现为「apply 成功、回读缺该组件」;规则键集为受控透传(filterGroupName/filterGroupConfig/filterItemName/que/judge 系,以 ws40013 dataset 实测形态定稿)。",
63
63
  "properties": {
64
64
  "graphs": {
65
65
  "type": "array",
@@ -69,12 +69,12 @@
69
69
  },
70
70
  "x-backend": "ComponentFilter.graphList(GraphVO.graphKey/graphName/graphType)"
71
71
  },
72
- "groups": {
72
+ "rules": {
73
73
  "type": "array",
74
74
  "minItems": 1,
75
- "filterGroupsShapeDescription": "筛选规则组。",
75
+ "filterRulesShapeDescription": "筛选规则组(至少 1 组)。组键:filterGroupName(组名,必填)、filterGroupConfig(组内筛选项列表);项键:filterItemName、que{queId}(绑定字段)、judge{judgeType,judgeValues}(默认条件)。代理键 filterGroupId/filterItemId 为后端自增位移键,读写两侧均剔除,不要携带。",
76
76
  "items": {
77
- "$ref": "#/$defs/FilterGroup"
77
+ "$ref": "#/$defs/FilterRuleGroup"
78
78
  },
79
79
  "x-backend": "ComponentFilter.filterConfig(List<FilterConfig>)"
80
80
  }
@@ -109,71 +109,49 @@
109
109
  }
110
110
  }
111
111
  },
112
- "FilterGroup": {
112
+ "FilterRuleGroup": {
113
113
  "type": "object",
114
- "additionalProperties": false,
115
114
  "required": [
116
- "items"
115
+ "filterGroupName"
117
116
  ],
118
- "filterGroupShapeDescription": "筛选规则组。",
117
+ "filterRuleGroupShapeDescription": "筛选规则组。组内筛选项走 filterGroupConfig;其余后端键受控透传。",
119
118
  "properties": {
120
- "group_id": {
121
- "type": "integer",
122
- "filterGroupIdShapeDescription": "组 id(更新已有组时锚定;缺省新建)。",
123
- "x-backend": "FilterConfig.filterGroupId"
124
- },
125
- "name": {
119
+ "filterGroupName": {
126
120
  "type": "string",
127
- "filterGroupNameShapeDescription": "组名称。",
121
+ "filterRuleGroupNameShapeDescription": "组名称(读面渲染为分组标题)。",
128
122
  "x-backend": "FilterConfig.filterGroupName"
129
123
  },
130
- "items": {
124
+ "filterGroupConfig": {
131
125
  "type": "array",
132
- "minItems": 1,
133
- "filterGroupItemsShapeDescription": "组内筛选项。",
126
+ "filterRuleGroupItemsShapeDescription": "组内筛选项(绑定字段与默认条件)。",
134
127
  "items": {
135
- "$ref": "#/$defs/FilterItem"
136
- }
128
+ "$ref": "#/$defs/FilterRuleItem"
129
+ },
130
+ "x-backend": "FilterConfig.filterGroupConfig(List<FilterGroupConfig>)"
137
131
  }
138
132
  }
139
133
  },
140
- "FilterItem": {
134
+ "FilterRuleItem": {
141
135
  "type": "object",
142
- "additionalProperties": false,
143
136
  "required": [
144
- "name"
137
+ "filterItemName"
145
138
  ],
146
- "filterItemShapeDescription": "单个筛选项(绑定一个字段)。",
139
+ "filterRuleItemShapeDescription": "单个筛选项(绑定一个字段)。que/judge 为后端受控透传形态;条件表达式(关联图表/字段)键集以 dataset 实测形态为准。",
147
140
  "properties": {
148
- "item_id": {
149
- "type": "integer",
150
- "filterItemIdShapeDescription": "筛选项 id(更新锚定)。",
151
- "x-backend": "FilterGroupConfig.filterItemId"
152
- },
153
- "name": {
141
+ "filterItemName": {
154
142
  "type": "string",
155
- "filterItemNameShapeDescription": "筛选项名称。",
143
+ "filterRuleItemNameShapeDescription": "筛选项名称。",
156
144
  "x-backend": "FilterGroupConfig.filterItemName"
157
145
  },
158
- "conditions": {
159
- "type": "array",
160
- "filterItemConditionsShapeDescription": "筛选条件(受控透传;条件表达式键集以 dataset 实测形态定稿后开放规范键)。",
161
- "x-backend": "FilterGroupConfig.filterCondition(List<FilterCondition>)"
162
- },
163
- "default_judge_type": {
164
- "type": "integer",
165
- "filterItemDftJudgeTypeShapeDescription": "默认比较符。",
166
- "x-backend": "FilterGroupConfig.dftJudgeType"
167
- },
168
- "default_values": {
169
- "type": "array",
170
- "filterItemDftValuesShapeDescription": "默认比较值(受控透传)。",
171
- "x-backend": "FilterGroupConfig.dftJudgeValue(List<AnswerValue>)"
146
+ "que": {
147
+ "type": "object",
148
+ "filterRuleItemQueShapeDescription": "绑定的字段引用(queId 为字段唯一键)。",
149
+ "x-backend": "FilterGroupConfig.que(Question)"
172
150
  },
173
- "filter_type": {
174
- "type": "string",
175
- "filterItemTypeShapeDescription": "筛选项交互类型(受控枚举,dataset 校准)。",
176
- "x-backend": "FilterGroupConfig.filterType"
151
+ "judge": {
152
+ "type": "object",
153
+ "filterRuleItemJudgeShapeDescription": "默认筛选条件:judgeType(比较符)+judgeValues(比较值列表)。",
154
+ "x-backend": "FilterGroupConfig.dftJudgeType/dftJudgeValue"
177
155
  }
178
156
  }
179
157
  },
@@ -181,16 +159,14 @@
181
159
  "type": "object",
182
160
  "additionalProperties": false,
183
161
  "required": [
184
- "title",
185
162
  "source_type",
186
163
  "config"
187
164
  ],
188
165
  "properties": {
189
166
  "title": {
190
167
  "type": "string",
191
- "minLength": 1,
192
- "sectionTitleShapeDescription": "组件标题(渲染在组件头部)。",
193
- "x-backend": "组件标题(DashChartInfo.title / gridConfig.gridTitle 等)"
168
+ "sectionTitleShapeDescription": "组件标题(可选)。存量筛选组件合法无标题——后端筛选组件无标题存储,提取产物不带 title,不要合成(如「组件 N」)。",
169
+ "x-backend": "组件标题(筛选组件无标题存储;其余组件见 DashChartInfo.title / gridConfig.gridTitle 等)"
194
170
  },
195
171
  "source_type": {
196
172
  "const": "filter",
@@ -211,7 +187,7 @@
211
187
  },
212
188
  "config": {
213
189
  "$ref": "#/$defs/FilterConfig",
214
- "filterConfigPropDescription": "筛选配置(必填,groups 非空)。"
190
+ "filterConfigPropDescription": "筛选配置(必填,rules 至少 1 组)。"
215
191
  }
216
192
  }
217
193
  }
@@ -230,7 +206,6 @@
230
206
  },
231
207
  "examples": [
232
208
  {
233
- "title": "数据筛选",
234
209
  "source_type": "filter",
235
210
  "position": {
236
211
  "pc": {
@@ -249,15 +224,21 @@
249
224
  "chart_name": "故障类型分布"
250
225
  }
251
226
  ],
252
- "groups": [
227
+ "rules": [
253
228
  {
254
- "name": "默认组",
255
- "items": [
229
+ "filterGroupName": "区域",
230
+ "filterGroupConfig": [
256
231
  {
257
- "name": "报修日期",
258
- "default_judge_type": 1,
259
- "default_values": [],
260
- "filter_type": "dateRange"
232
+ "filterItemName": "城市",
233
+ "que": {
234
+ "queId": 123456
235
+ },
236
+ "judge": {
237
+ "judgeType": 3,
238
+ "judgeValues": [
239
+ "上海"
240
+ ]
241
+ }
261
242
  }
262
243
  ]
263
244
  }
@@ -7,7 +7,7 @@
7
7
  "Position": {
8
8
  "type": "object",
9
9
  "additionalProperties": false,
10
- "positionShapeDescription": "网格位置。PC 是 24 列网格(cols≤24),mobile 是 6 列网格(cols≤6);同行组件共享同一 y 与 rows,一行 cols 合计 24。mobile 可省略(缺省与 pc 同布局)。平铺别名 pc_x/pc_y/pc_w/pc_h、mobile_* 也接受,归一为本结构。标准行高:指标卡 rows=5、BI 图表 rows=7、数据视图 rows=11、业务入口 rows=4。",
10
+ "positionShapeDescription": "网格位置。PC 是 24 列网格(cols≤24),mobile 是 6 列网格(cols≤6);同行组件共享同一 y 与 rows,一行 cols 合计 24。mobile 可省略(缺省与 pc 同布局)。平铺别名 pc_x/pc_y/pc_w/pc_h、mobile_* 也接受(x/y 与 w/h/cols/rows 混写均认),归一为本结构。标准行高:指标卡 rows=5、BI 图表 rows=7、数据视图 rows=11、业务入口 rows=4。",
11
11
  "properties": {
12
12
  "pc": {
13
13
  "$ref": "#/$defs/PositionDetail"
@@ -7,7 +7,7 @@
7
7
  "Position": {
8
8
  "type": "object",
9
9
  "additionalProperties": false,
10
- "positionShapeDescription": "网格位置。PC 是 24 列网格(cols≤24),mobile 是 6 列网格(cols≤6);同行组件共享同一 y 与 rows,一行 cols 合计 24。mobile 可省略(缺省与 pc 同布局)。平铺别名 pc_x/pc_y/pc_w/pc_h、mobile_* 也接受,归一为本结构。标准行高:指标卡 rows=5、BI 图表 rows=7、数据视图 rows=11、业务入口 rows=4。",
10
+ "positionShapeDescription": "网格位置。PC 是 24 列网格(cols≤24),mobile 是 6 列网格(cols≤6);同行组件共享同一 y 与 rows,一行 cols 合计 24。mobile 可省略(缺省与 pc 同布局)。平铺别名 pc_x/pc_y/pc_w/pc_h、mobile_* 也接受(x/y 与 w/h/cols/rows 混写均认),归一为本结构。标准行高:指标卡 rows=5、BI 图表 rows=7、数据视图 rows=11、业务入口 rows=4。",
11
11
  "properties": {
12
12
  "pc": {
13
13
  "$ref": "#/$defs/PositionDetail"
@@ -7,7 +7,7 @@
7
7
  "Position": {
8
8
  "type": "object",
9
9
  "additionalProperties": false,
10
- "positionShapeDescription": "网格位置。PC 是 24 列网格(cols≤24),mobile 是 6 列网格(cols≤6);同行组件共享同一 y 与 rows,一行 cols 合计 24。mobile 可省略(缺省与 pc 同布局)。平铺别名 pc_x/pc_y/pc_w/pc_h、mobile_* 也接受,归一为本结构。标准行高:指标卡 rows=5、BI 图表 rows=7、数据视图 rows=11、业务入口 rows=4。",
10
+ "positionShapeDescription": "网格位置。PC 是 24 列网格(cols≤24),mobile 是 6 列网格(cols≤6);同行组件共享同一 y 与 rows,一行 cols 合计 24。mobile 可省略(缺省与 pc 同布局)。平铺别名 pc_x/pc_y/pc_w/pc_h、mobile_* 也接受(x/y 与 w/h/cols/rows 混写均认),归一为本结构。标准行高:指标卡 rows=5、BI 图表 rows=7、数据视图 rows=11、业务入口 rows=4。",
11
11
  "properties": {
12
12
  "pc": {
13
13
  "$ref": "#/$defs/PositionDetail"
@@ -81,7 +81,7 @@
81
81
  },
82
82
  "tasks": {
83
83
  "type": "array",
84
- "taskItemsShapeDescription": "待办任务条目(ordinal 缺省按数组顺序)。",
84
+ "taskItemsShapeDescription": "待办任务条目(ordinal 缺省按数组顺序)。省略或空列表时编译器按前端新建缺省补 4 类标准任务(待办/已超时/即将超时/催办,全部勾选)——后端写链要求 dashTaskConfigList 非空键。",
85
85
  "items": {
86
86
  "$ref": "#/$defs/TaskItem"
87
87
  },
@@ -7,7 +7,7 @@
7
7
  "Position": {
8
8
  "type": "object",
9
9
  "additionalProperties": false,
10
- "positionShapeDescription": "网格位置。PC 是 24 列网格(cols≤24),mobile 是 6 列网格(cols≤6);同行组件共享同一 y 与 rows,一行 cols 合计 24。mobile 可省略(缺省与 pc 同布局)。平铺别名 pc_x/pc_y/pc_w/pc_h、mobile_* 也接受,归一为本结构。标准行高:指标卡 rows=5、BI 图表 rows=7、数据视图 rows=11、业务入口 rows=4。",
10
+ "positionShapeDescription": "网格位置。PC 是 24 列网格(cols≤24),mobile 是 6 列网格(cols≤6);同行组件共享同一 y 与 rows,一行 cols 合计 24。mobile 可省略(缺省与 pc 同布局)。平铺别名 pc_x/pc_y/pc_w/pc_h、mobile_* 也接受(x/y 与 w/h/cols/rows 混写均认),归一为本结构。标准行高:指标卡 rows=5、BI 图表 rows=7、数据视图 rows=11、业务入口 rows=4。",
11
11
  "properties": {
12
12
  "pc": {
13
13
  "$ref": "#/$defs/PositionDetail"
@@ -7,7 +7,7 @@
7
7
  "Position": {
8
8
  "type": "object",
9
9
  "additionalProperties": false,
10
- "positionShapeDescription": "网格位置。PC 是 24 列网格(cols≤24),mobile 是 6 列网格(cols≤6);同行组件共享同一 y 与 rows,一行 cols 合计 24。mobile 可省略(缺省与 pc 同布局)。平铺别名 pc_x/pc_y/pc_w/pc_h、mobile_* 也接受,归一为本结构。标准行高:指标卡 rows=5、BI 图表 rows=7、数据视图 rows=11、业务入口 rows=4。",
10
+ "positionShapeDescription": "网格位置。PC 是 24 列网格(cols≤24),mobile 是 6 列网格(cols≤6);同行组件共享同一 y 与 rows,一行 cols 合计 24。mobile 可省略(缺省与 pc 同布局)。平铺别名 pc_x/pc_y/pc_w/pc_h、mobile_* 也接受(x/y 与 w/h/cols/rows 混写均认),归一为本结构。标准行高:指标卡 rows=5、BI 图表 rows=7、数据视图 rows=11、业务入口 rows=4。",
11
11
  "properties": {
12
12
  "pc": {
13
13
  "$ref": "#/$defs/PositionDetail"
@@ -26,7 +26,7 @@
26
26
  "type": "integer",
27
27
  "minimum": 1,
28
28
  "packageTagIdDescription": "归属应用包 tagId;创建必填(缺 dash_key 时),更新缺省沿用读面归属。别名 packageTagId/package_id/packageId。 创建(无 dash_key)时必填——由 portal_apply 实现层校验(PACKAGE_TAG_REQUIRED + 包新增权限守卫);更新(带 dash_key)缺省沿用读面归属,无所属包的门户提取产物合法缺省本字段。",
29
- "x-backend": "dash.tags[0].tagId(POST /dash/{dashKey}/baseInfo)"
29
+ "x-backend": "dash.tags[0].tagId"
30
30
  },
31
31
  "sections": {
32
32
  "type": "array",
@@ -49,8 +49,8 @@
49
49
  "publish": {
50
50
  "type": "boolean",
51
51
  "default": true,
52
- "publishDescription": "apply 后是否发布(true=写后发布并回读线上态;false=仅落草稿)。这是生命周期动作参数,不属于声明内容,幂等重放时用 false。",
53
- "x-backend": "POST /dash/{dashKey}/publish"
52
+ "publishDescription": "apply 后是否发布(true=写后发布并回读线上态;false=仅落草稿)。这是生命周期动作参数,不属于声明内容,幂等重放时用 false。注意:对已发布门户用 false 更新会把发布状态降回草稿——需要保留线上态时不要用 false 试写。",
53
+ "x-backend": "(编排层,不落库)"
54
54
  }
55
55
  }
56
56
  },
@@ -232,7 +232,7 @@
232
232
  "Position": {
233
233
  "type": "object",
234
234
  "additionalProperties": false,
235
- "positionDescription": "网格位置。PC 是 24 列网格(cols≤24),mobile 是 6 列网格(cols≤6);同行组件共享同一 y 与 rows,一行 cols 合计 24。mobile 可省略(缺省与 pc 同布局)。平铺别名 pc_x/pc_y/pc_w/pc_h、mobile_x/…/mobile_h 也接受,归一为本结构。标准行高:指标卡 rows=5、BI 图表 rows=7、数据视图 rows=11、业务入口 rows=4。",
235
+ "positionDescription": "网格位置。PC 是 24 列网格(cols≤24),mobile 是 6 列网格(cols≤6);同行组件共享同一 y 与 rows,一行 cols 合计 24。mobile 可省略(缺省与 pc 同布局)。平铺别名 pc_x/pc_y/pc_w/pc_h、mobile_x/…/mobile_h 也接受(x/y 与 w/h/cols/rows 混写均认),归一为本结构。标准行高:指标卡 rows=5、BI 图表 rows=7、数据视图 rows=11、业务入口 rows=4。",
236
236
  "properties": {
237
237
  "pc": {
238
238
  "$ref": "#/$defs/PositionDetail"
@@ -20,7 +20,7 @@
20
20
  "pinned": {
21
21
  "type": "boolean",
22
22
  "pinnedDescription": "是否固定在应用的视图列表顶部。true=固定,false=取消固定;省略时保持当前状态。别名 fixed/is_pinned/isPinned。它与 display.pin_navigate(视图内导航固定)无关。",
23
- "x-backend": "POST /app/{appKey}/view/ordinal (FIXED_VIEW_LIST|VIEW_LIST)"
23
+ "x-backend": "视图列表固定状态(非 viewgraph 配置)"
24
24
  },
25
25
  "name": {
26
26
  "type": "string",
@@ -6,7 +6,7 @@
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
8
  "properties": {
9
- "pinned": { "type": "boolean", "pinnedDescription": "是否固定在应用的视图列表顶部。true=固定,false=取消固定;省略时保持当前状态。它与 display.pin_navigate 无关。", "x-backend": "POST /app/{appKey}/view/ordinal (FIXED_VIEW_LIST|VIEW_LIST)" },
9
+ "pinned": { "type": "boolean", "pinnedDescription": "是否固定在应用的视图列表顶部。true=固定,false=取消固定;省略时保持当前状态。它与 display.pin_navigate 无关。", "x-backend": "视图列表固定状态(非 viewgraph 配置)" },
10
10
  "columns": { "type": "array", "items": { "type": "integer" }, "minItems": 1, "columnsDescription": "表格或卡片展示字段,按数组顺序显示。" },
11
11
  "sort": { "type": "array", "items": { "$ref": "../complete.json#/$defs/SortRule" }, "sortDescription": "多级排序。空数组恢复默认排序。" },
12
12
  "card": { "$ref": "../complete.json#/$defs/CardConfig", "cardDescription": "卡片视图配置(封面/标题/字段标题/分组)。" },
@@ -20,7 +20,7 @@
20
20
  "pinned": {
21
21
  "type": "boolean",
22
22
  "pinnedDescription": "是否固定在应用的视图列表顶部。true=固定,false=取消固定;省略时保持当前状态。别名 fixed/is_pinned/isPinned。它与 display.pin_navigate(视图内导航固定)无关。",
23
- "x-backend": "POST /app/{appKey}/view/ordinal (FIXED_VIEW_LIST|VIEW_LIST)"
23
+ "x-backend": "视图列表固定状态(非 viewgraph 配置)"
24
24
  },
25
25
  "name": {
26
26
  "type": "string",
@@ -26,7 +26,12 @@ 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, update_workspace_icon_from_existing, workspace_icon_config
29
+ from ..solution.compiler.icon_utils import (
30
+ encode_workspace_icon_with_defaults,
31
+ update_workspace_icon_from_existing,
32
+ validate_workspace_icon_choice,
33
+ workspace_icon_config,
34
+ )
30
35
  from ..solution.compiler.view_compiler import VIEW_TYPE_MAP
31
36
  from ..solution.executor import _build_viewgraph_questions, _compact_dict, _flatten_viewgraph_questions, extract_field_map
32
37
  from ..solution.spec_models import FieldType, FormLayoutRowSpec, FormLayoutSectionSpec, ViewSpec
@@ -3063,6 +3068,7 @@ class AiBuilderFacade:
3063
3068
  *,
3064
3069
  profile: str,
3065
3070
  visibility: VisibilityPatch | None,
3071
+ degradation_warnings: list[dict[str, Any]] | None = None,
3066
3072
  ) -> dict[str, Any]:
3067
3073
  if visibility is None:
3068
3074
  return default_member_auth()
@@ -3096,35 +3102,116 @@ class AiBuilderFacade:
3096
3102
  member_ids=visibility.external_selectors.member_ids,
3097
3103
  member_emails=visibility.external_selectors.member_emails,
3098
3104
  )
3099
- # 冗余引用降级:提取器对同一成员同时产出 uid/email/name 三通道(uid 必有且直通解析,
3100
- # 保证成员不丢);email/name 走后端通讯录搜索,受数据/权限边界影响解析可能失败。
3101
- # 这类失败降级为非阻断——只有 uid/dept_id 直通也覆盖不了的引用才真正阻断。
3105
+ # 冗余引用降级(review §2-1 逐主体锚定校验):提取器对同一成员同时产出
3106
+ # uid/email/name 三通道(uid 必有且直通解析,保证成员不丢);email/name 走
3107
+ # 后端通讯录搜索,受数据/权限边界影响解析可能失败。降级只在「该主体已被
3108
+ # 直通锚覆盖」时成立,否则升级为阻断——关闭手写声明混用 uid 锚定 + 仅
3109
+ # email/name 引用时,主体被静默丢弃的窗口。覆盖判定按失败类别的可验证面:
3110
+ # - AMBIGUOUS_*:目录命中多候选,候选(matches)与已解析直通锚 uid/deptId/
3111
+ # roleId 求交——交非空说明歧义主体之一已被锚定,主体集合不丢;
3112
+ # - *_NOT_FOUND:目录答「不存在」,失败值与已解析条目的真实属性(email/
3113
+ # name)比对(直通 stub 条目属性是 id 字符串,不会误匹配);
3114
+ # - *_SEARCH_FAILED:目录服务本身不可达,主体身份无从验证——保留全局
3115
+ # 「直通锚非空」guard(eval 隔离环境外部登录资源不可达 → 搜索 49500
3116
+ # 超时的确定性约束;纯手写声明仍阻断),降级明细经 degradation_warnings
3117
+ # 回响应信封,不再静默。
3102
3118
  _SOFT_RESOLUTION_KINDS = {"member_email", "member_name", "department_name", "role", "external_member_email"}
3103
3119
  _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
- blocking_issues = [
3110
- issue
3111
- for issue in [
3112
- *member_resolution["issues"],
3113
- *role_resolution["issues"],
3114
- *department_resolution["issues"],
3115
- *external_member_resolution["issues"],
3116
- ]
3117
- # 冗余 email/name 引用「找不到」降级为非阻断(uid/id 直通已保主体不丢);
3118
- # 权限拒绝/查询失败类(LOOKUP_FAILED/PERMISSION_DENIED):主体直通已非空时降级,否则仍阻断
3119
- if not (
3120
- 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
- )
3125
- )
3126
- ]
3120
+ member_entries = member_resolution.get("member_entries") or []
3121
+ department_entries = department_resolution.get("department_entries") or []
3122
+ role_entries = role_resolution.get("role_entries") or []
3123
+ external_entries = external_member_resolution.get("member_entries") or []
3124
+ uid_direct_count = len(member_entries) + len(department_entries)
3125
+ resolved_anchor_ids = {
3126
+ value
3127
+ for value in (
3128
+ _coerce_positive_int(entry.get("uid"))
3129
+ for entry in [*member_entries, *external_entries]
3130
+ )
3131
+ if value is not None
3132
+ } | {
3133
+ value
3134
+ for value in (
3135
+ _coerce_positive_int(entry.get("deptId"))
3136
+ for entry in department_entries
3137
+ )
3138
+ if value is not None
3139
+ } | {
3140
+ value
3141
+ for value in (_coerce_positive_int(entry.get("roleId")) for entry in role_entries)
3142
+ if value is not None
3143
+ }
3144
+
3145
+ def _match_anchor_ids(issue: dict[str, Any]) -> set[int]:
3146
+ matched: set[int] = set()
3147
+ for candidate in issue.get("matches") or []:
3148
+ if not isinstance(candidate, dict):
3149
+ continue
3150
+ for key in ("uid", "id", "memberId", "deptId", "roleId"):
3151
+ value = _coerce_positive_int(candidate.get(key))
3152
+ if value is not None:
3153
+ matched.add(value)
3154
+ return matched
3155
+
3156
+ def _matches_resolved_attribute(issue: dict[str, Any]) -> bool:
3157
+ kind = str(issue.get("kind") or "")
3158
+ raw = str(issue.get("value") or "").strip()
3159
+ if not raw:
3160
+ return False
3161
+ if kind in {"member_email", "external_member_email"}:
3162
+ entries = external_entries if kind.startswith("external") else member_entries
3163
+ return any(raw.lower() == str(entry.get("email") or "").strip().lower() for entry in entries)
3164
+ if kind == "member_name":
3165
+ return any(raw == str(entry.get("name") or "").strip() for entry in member_entries)
3166
+ if kind == "department_name":
3167
+ return any(raw == str(entry.get("deptName") or "").strip() for entry in department_entries)
3168
+ if kind == "role":
3169
+ return any(raw == str(entry.get("roleName") or "").strip() for entry in role_entries)
3170
+ return False
3171
+
3172
+ blocking_issues: list[dict[str, Any]] = []
3173
+ degraded_issues: list[dict[str, Any]] = []
3174
+ for issue in [
3175
+ *member_resolution["issues"],
3176
+ *role_resolution["issues"],
3177
+ *department_resolution["issues"],
3178
+ *external_member_resolution["issues"],
3179
+ ]:
3180
+ if str(issue.get("kind") or "") not in _SOFT_RESOLUTION_KINDS:
3181
+ blocking_issues.append(issue)
3182
+ continue
3183
+ error_code = str(issue.get("error_code") or "")
3184
+ if error_code.endswith("_SEARCH_FAILED"):
3185
+ covered = uid_direct_count > 0
3186
+ elif error_code.startswith("AMBIGUOUS_"):
3187
+ covered = bool(_match_anchor_ids(issue) & resolved_anchor_ids)
3188
+ elif error_code in _NOT_FOUND_RESOLUTION_CODES:
3189
+ covered = _matches_resolved_attribute(issue)
3190
+ else:
3191
+ # PERMISSION_DENIED/未知失败类:不可归因为「主体已被锚定的冗余通道」,
3192
+ # 维持阻断(master 语义)。
3193
+ blocking_issues.append(issue)
3194
+ continue
3195
+ if covered:
3196
+ degraded_issues.append(issue)
3197
+ else:
3198
+ blocking_issues.append(issue)
3127
3199
  self._raise_visibility_resolution_issues(blocking_issues)
3200
+ if degraded_issues and degradation_warnings is not None:
3201
+ for issue in degraded_issues:
3202
+ degradation_warnings.append(
3203
+ _warning(
3204
+ "VISIBILITY_SOFT_REFERENCE_DEGRADED",
3205
+ (
3206
+ f"visibility {issue.get('kind')} '{issue.get('value')}' could not be resolved "
3207
+ f"({issue.get('error_code')}); the subject is dropped from visibility selectors "
3208
+ "unless it was also anchored by a direct id reference"
3209
+ ),
3210
+ kind=issue.get("kind"),
3211
+ value=issue.get("value"),
3212
+ error_code=issue.get("error_code"),
3213
+ )
3214
+ )
3128
3215
 
3129
3216
  external_depart_entries: list[dict[str, Any]] = []
3130
3217
  for dept_id in visibility.external_selectors.dept_ids:
@@ -9024,6 +9111,26 @@ class AiBuilderFacade:
9024
9111
  )
9025
9112
  except PortalsContractError as error:
9026
9113
  issues.append({"path": error.path or "/", "code": error.error_code, "message": str(error.message)})
9114
+ else:
9115
+ # 创建语义(无 dash_key)的 icon/color 目录校验与 apply 同口径:离线
9116
+ # jsonschema 不含目录枚举(更新语义须放行存量 hex 透传),不补这层
9117
+ # 会出现「validate 通过、apply 拒绝」的校验信任断裂(实测反馈失败二
9118
+ # 同型);更新语义不做目录校验(与 apply 更新分支一致)。
9119
+ if not str(parsed.dash_key or "").strip():
9120
+ icon_ok, icon_error_code, icon_message, _icon_details = validate_workspace_icon_choice(
9121
+ icon=parsed.icon,
9122
+ color=parsed.color,
9123
+ require_explicit=True,
9124
+ disallow_generic=True,
9125
+ )
9126
+ if not icon_ok:
9127
+ issues.append(
9128
+ {
9129
+ "path": "/color" if str(icon_error_code or "").endswith("COLOR_NOT_FOUND") or icon_error_code == "WORKSPACE_ICON_COLOR_REQUIRED" else "/icon",
9130
+ "code": icon_error_code or "WORKSPACE_ICON_INVALID",
9131
+ "message": icon_message or "invalid workspace icon configuration",
9132
+ }
9133
+ )
9027
9134
  ok = not issues
9028
9135
  return {
9029
9136
  "status": "success" if ok else "failed",
@@ -15316,9 +15423,14 @@ class AiBuilderFacade:
15316
15423
  requested_visibility = request.visibility
15317
15424
  if requested_visibility is None and isinstance(request.auth, dict) and request.auth:
15318
15425
  requested_visibility = VisibilityPatch.model_validate(_public_visibility_from_member_auth(request.auth))
15426
+ visibility_degradation_warnings: list[dict[str, Any]] = []
15319
15427
  try:
15320
15428
  desired_auth = (
15321
- self._compile_visibility_to_member_auth(profile=profile, visibility=requested_visibility)
15429
+ self._compile_visibility_to_member_auth(
15430
+ profile=profile,
15431
+ visibility=requested_visibility,
15432
+ degradation_warnings=visibility_degradation_warnings,
15433
+ )
15322
15434
  if requested_visibility is not None
15323
15435
  else None
15324
15436
  )
@@ -15363,6 +15475,12 @@ class AiBuilderFacade:
15363
15475
  def finalize(response: JSONObject) -> JSONObject:
15364
15476
  if schema_context is not None:
15365
15477
  response["schemaVersion"] = schema_context.schema_version
15478
+ # 可见性冗余引用降级明细回信封(review §2-1:降级不再静默)
15479
+ if visibility_degradation_warnings:
15480
+ existing_warnings = response.get("warnings")
15481
+ warnings = list(existing_warnings) if isinstance(existing_warnings, list) else []
15482
+ warnings.extend(item for item in visibility_degradation_warnings if item not in warnings)
15483
+ response["warnings"] = warnings
15366
15484
  _merge_duration_breakdown(
15367
15485
  response,
15368
15486
  portal_initial_read_ms=portal_initial_read_ms,
@@ -20693,6 +20811,18 @@ def _compile_portal_task_config(section_title: str, raw: dict[str, Any]) -> dict
20693
20811
  task_items.append(item)
20694
20812
  if task_items:
20695
20813
  task_config["dashTaskConfigList"] = task_items
20814
+ # dashTaskConfigList 必须始终携带:后端 DashTaskSvcImpl#addTaskInfos 对
20815
+ # taskConfig.getDashTaskConfigList() 直接迭代(ComponentTaskConfig 字段无默认
20816
+ # 初始化),键缺失 → null.size() NPE → 500(实测反馈「task 组件无论如何 500」
20817
+ # 根因)。声明未表达 tasks 时按前端新建缺省补 4 类标准任务(title 用后端 i18n 键,
20818
+ # 与存量/前端缺省一致);提取侧读面恒有条目,round-trip 不受影响。
20819
+ if not task_config.get("dashTaskConfigList"):
20820
+ task_config["dashTaskConfigList"] = [
20821
+ {"title": "all.taskTodo", "type": 1, "ordinal": 0, "beingCheck": True},
20822
+ {"title": "all.taskTimeout", "type": 2, "ordinal": 1, "beingCheck": True},
20823
+ {"title": "all.taskUpcomingTimeout", "type": 3, "ordinal": 2, "beingCheck": True},
20824
+ {"title": "all.taskRemind", "type": 4, "ordinal": 3, "beingCheck": True},
20825
+ ]
20696
20826
  return task_config
20697
20827
 
20698
20828
 
@@ -1487,8 +1487,26 @@ def _handle_portal_apply(args: argparse.Namespace, context: CliContext) -> dict:
1487
1487
  schema_version=effective_schema_version,
1488
1488
  publish=effective_publish,
1489
1489
  )
1490
- legacy_args_used = True
1491
- if legacy_args_used:
1490
+ # deprecation 提示仅在实际使用 patch 参数时打印(review §5 噪声收敛):
1491
+ # 纯 flag 调用(--dash-key/--publish/--schema-version/--package-id 定位或发布
1492
+ # 开关)不是 patch 形态,不该被提示;判定集与 --file 互斥检查的 patch 参数集一致。
1493
+ patch_args_used = any(
1494
+ bool(item)
1495
+ for item in (
1496
+ getattr(args, "sections_file", None),
1497
+ getattr(args, "patch_sections_file", None),
1498
+ getattr(args, "visibility_file", None),
1499
+ getattr(args, "auth_file", None),
1500
+ getattr(args, "dash_global_config_file", None),
1501
+ getattr(args, "config_file", None),
1502
+ getattr(args, "payload_file", None),
1503
+ getattr(args, "icon", None),
1504
+ getattr(args, "color", None),
1505
+ (getattr(args, "dash_name", "") or ""),
1506
+ (getattr(args, "layout_preset", "") or ""),
1507
+ )
1508
+ )
1509
+ if patch_args_used:
1492
1510
  print(
1493
1511
  "[deprecation] builder portal apply patch parameters are in a compatibility window; prefer `--file <declaration.json>`",
1494
1512
  file=sys.stderr,
@@ -630,7 +630,7 @@ def build_builder_server() -> PolicyAwareFastMCP:
630
630
  apps=apps,
631
631
  )
632
632
 
633
- @server.tool()
633
+ @server.tool(description=ai_builder._high_risk_tool_description(operation="update", target="portal configuration"))
634
634
  def portal_apply(
635
635
  profile: str = DEFAULT_PROFILE,
636
636
  dash_key: str = "",
@@ -696,7 +696,7 @@ def build_builder_server() -> PolicyAwareFastMCP:
696
696
  patch_sections=patch_sections,
697
697
  )
698
698
 
699
- @server.tool()
699
+ @server.tool(description=ai_builder._high_risk_tool_description(operation="delete", target="portal configuration"))
700
700
  def portal_delete(
701
701
  profile: str = DEFAULT_PROFILE,
702
702
  dash_key: str = "",
@@ -4422,6 +4422,12 @@ class AiBuilderTools(ToolBase):
4422
4422
  normalized_args=normalized_args,
4423
4423
  suggested_next_call={"tool_name": "portal_apply", "arguments": {"profile": profile, **normalized_args}},
4424
4424
  ))
4425
+ # 更新语义(dash_key 定位)写成功后挂人工复核 notice:被收敛删除的
4426
+ # portal_update/portal_update_base_info 原带该防护(master 同款),
4427
+ # portal_apply 成为唯一写入口后在此恢复;创建语义不挂(新建资源
4428
+ # 是 agent 主路径,逐次人工复核不可行)。
4429
+ if not portal_creating and result.get("write_executed") and result.get("status") in {"success", "partial_success"}:
4430
+ result = self._attach_human_review_notice(result, operation="update", target="portal configuration")
4425
4431
  return self._attach_apply_envelope(profile, "portal_apply", result)
4426
4432
 
4427
4433
  def portal_delete(self, *, profile: str, dash_key: str) -> JSONObject:
@@ -4433,6 +4439,8 @@ class AiBuilderTools(ToolBase):
4433
4439
  normalized_args=normalized_args,
4434
4440
  suggested_next_call={"tool_name": "portal_delete", "arguments": {"profile": profile, **normalized_args}},
4435
4441
  ))
4442
+ if result.get("write_executed") and result.get("status") in {"success", "partial_success"}:
4443
+ result = self._attach_human_review_notice(result, operation="delete", target="portal configuration")
4436
4444
  return self._attach_apply_envelope(profile, "portal_delete", result)
4437
4445
 
4438
4446
  def app_publish_verify(