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

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 +2 -2
  7. package/src/qingflow_mcp/builder_facade/models.py +15 -4
  8. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/complete.json +5 -5
  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 +1 -1
  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
package/README.md CHANGED
@@ -7,7 +7,7 @@ Qingflow CLI 是轻流面向开发者和 AI Agent 的命令行工具。它把轻
7
7
  ## 安装
8
8
 
9
9
  ```bash
10
- npm install @qingflow-tech/qingflow-cli@1.1.61-beta.5
10
+ npm install @qingflow-tech/qingflow-cli@1.1.61-beta.6
11
11
  ```
12
12
 
13
13
  推荐从官网复制这一条一键安装 Qingflow CLI:
@@ -21,7 +21,7 @@ npx qingflow-cli@latest install
21
21
  ## 运行
22
22
 
23
23
  ```bash
24
- npx -y -p @qingflow-tech/qingflow-cli@1.1.61-beta.5 qingflow
24
+ npx -y -p @qingflow-tech/qingflow-cli@1.1.61-beta.6 qingflow
25
25
  ```
26
26
 
27
27
  全局安装后可以直接运行:
@@ -62,7 +62,7 @@ qingflow-skills install --agent codex --scope user
62
62
  如果你通过一次性 `npx -p` 执行 skills 安装,建议加 `--copy`,避免 symlink 指向 npm 临时执行缓存:
63
63
 
64
64
  ```bash
65
- npx -y -p @qingflow-tech/qingflow-cli@1.1.61-beta.5 qingflow-skills install --agent codex --scope user --copy
65
+ npx -y -p @qingflow-tech/qingflow-cli@1.1.61-beta.6 qingflow-skills install --agent codex --scope user --copy
66
66
  ```
67
67
 
68
68
  `qingflow-skills install` 默认使用 symlink,且遇到同名 skill 不会覆盖;只有显式传入 `--force` 才会替换。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qingflow-tech/qingflow-cli",
3
- "version": "1.1.61-beta.5",
3
+ "version": "1.1.61-beta.6",
4
4
  "description": "Human-friendly Qingflow command line interface for auth, record operations, import, tasks, and stable builder flows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qingflow-mcp"
7
- version = "1.1.61b5"
7
+ version = "1.1.61b6"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -70,6 +70,7 @@ qingflow_mcp = [
70
70
  "py.typed",
71
71
  "builder_facade/schemas/app_form/**/*.json",
72
72
  "builder_facade/schemas/views/**/*.json",
73
+ "builder_facade/schemas/portals/**/*.json",
73
74
  ]
74
75
 
75
76
  [tool.pytest.ini_options]
@@ -233,5 +233,5 @@ CLI 对外推荐使用 **`group_by` + `metric(s)`**;旧的 **`dimension_field_
233
233
 
234
234
  - [SKILL.md](../SKILL.md)
235
235
  - [QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md](./QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md)
236
- - [QINGFLOW_CLI_BUILDER_PORTAL_WORKFLOW.md](./QINGFLOW_CLI_BUILDER_PORTAL_WORKFLOW.md)(**`chart_ref`**)
236
+ - [builder/70-portal.md](./builder/70-portal.md)(门户声明式主链与 **`chart` / `chart_ref`** 用法)
237
237
  - [QINGFLOW_CLI_FIELD_DATA_TYPES.md](./QINGFLOW_CLI_FIELD_DATA_TYPES.md)(成员侧 **`kind`** 与搭建 **`type`** 对照理解业务)
@@ -1,263 +1,11 @@
1
- # Builder 门户创建与变更 SOP:`qingflow builder portal`
1
+ # Builder Portal Workflow
2
2
 
3
- 搭建侧稳定入口:**`qingflow builder portal`**:`list` / `get` / **`apply`** / **`delete`**(与根命令 **`qingflow portal`** 的「成员可读列表/详情」不同,见 **[QINGFLOW_CLI_EXPLORATION_REPORT.md](./QINGFLOW_CLI_EXPLORATION_REPORT.md) §4.6**)。
4
-
5
- 实现要点来自 CLI 打包内的 `builder_facade/models.py`(`PortalApplyRequest`、`PortalSectionPatch`)、`builder_facade/service.py`(`portal_apply` / `portal_delete`)、`cli/commands/builder.py`。已对 **`package_id=2030703`**、应用 **`ead8ims5i401`** **实跑**:**新建**(`--no-publish`)、**`--publish` 更新**(网关 **503** 恢复后已重试成功)、**五类区块可稳定落库**;**`filter`(`type:6`)经 CLI 直传未落库** 见 **§3.4 / §8**。另已对测试包 **`1414907`** 实跑 **`portal delete`**:临时门户 **`etcivtmv5402`** 删除后回读 **`readback_status=deleted`**。
6
-
7
- > **权限**:**更新**需 **`edit_portal`**;**新建**按后端 `DashCtrl.createDash` 链路只预检目标包 **`add_app`**,不额外要求包 **`edit_app`**。失败时参见主技能与 **ADMIN** 速查。
8
-
9
- > **契约**:`qingflow --json builder contract --tool-name portal_apply`;删除契约:`qingflow --json builder contract --tool-name portal_delete`
10
-
11
- ---
12
-
13
- ## 1. 读:`list` → `get`
3
+ 旧文已并入 [builder/70-portal.md](./builder/70-portal.md)。门户创建/更新走声明式主链(版本化 `--file` 声明 + 离线 validate + 唯一 apply 入口):
14
4
 
15
5
  ```bash
16
- qingflow --json builder portal list > tmp/builder_portal_list.json
17
-
18
- qingflow --json builder portal get --dash-key "<DASH_KEY>" > tmp/builder_portal_get.json
19
- qingflow --json builder portal get --dash-key "<DASH_KEY>" --no-being-draft
20
- ```
21
-
22
- - **`list`** 可能带 **`PORTAL_PERMISSION_READ_UNAVAILABLE`**,**`verified: false`** 仍可能有可用 **`items[]`**。
23
- - **`get`**:**`--dash-key`** 必填;默认 **`--being-draft`** 为草稿。
24
-
25
- ---
26
-
27
- ## 2. 写:`apply` 两种模式(互斥)
28
-
29
- **不可同时**出现 **`--dash-key`** 与 **`--package-id`**。
30
-
31
- | 模式 | 必备参数 | 含义 |
32
- |------|----------|------|
33
- | **新建** | **`--package-id`** + **`--dash-name`** | **`--sections-file` 须非空**。 |
34
- | **更新** | **`--dash-key`** | 不要带 **`package_id`**。 |
35
-
36
- | 参数 | 说明 |
37
- |------|------|
38
- | **`--publish` / `--no-publish`** | 默认 **`--publish`**。仅 **`--no-publish`** 时不会调用发布接口;契约说明 **`publish=false` 不宣称线上已变**。 |
39
- | **`--payload-file`** | 完整门户 JSON 对象;**标准字段写 `dash_name`**。新版 CLI 兼容 **`name -> dash_name`**、单页 **`pages[0].components -> sections`**,但 agent 默认不要依赖兼容别名。 |
40
- | **`--sections-file`** | JSON **数组**;**全量替换**。省略时仅改基础信息,且 **`hide_copyright` / `dash_global_config` / `config`** 不能单独提交(**`PORTAL_SECTIONS_REQUIRED`**)。 |
41
- | **`--layout-preset`** | 可选:**`auto` / `dashboard_2col` / `dashboard_3col`**。当区块未显式写 **`position`** 时由工具生成大屏布局。 |
42
- | **`--visibility-file`** | 与 **`--auth-file`** 不能同时用。 |
43
- | **`--icon`** / **`--color`** | 新建门户必须显式传合法的非 `template` 图标和颜色;候选用 `qingflow --json builder icon catalog`。编辑已有门户时省略则保留现状。 |
44
- | **`--hide-copyright`** / **`--dash-global-config-file`** / **`--config-file`** | 见契约;部分键 **依赖与 `sections` 同批提交**。 |
45
-
46
- **图标规则**:CLI 不按门户名自动猜图标;智能体需要自行选择业务贴合的 `icon + color`。读回结果里的 `icon_config` 可直接用于前端资源卡片展示。
47
-
48
- ---
49
-
50
- ## 3. `sections[]`:六类组件与回读 `type`
51
-
52
- ### 3.1 公共键
53
-
54
- - **`title`**(必填)、**`source_type`**(必填,小写)、可选 **`position`**(**`pc`/`mobile`** 各 **`x,y,cols,rows`**;省略则由**实现自动排布**)。
55
- - **`config`**、**`dash_style_config`**:按类型 Optional。默认只写 snake_case 规范键。
56
-
57
- ### 3.1.0 Payload 字段口径
58
-
59
- 推荐完整 payload 直接使用 CLI 标准键:
60
-
61
- ```json
62
- {
63
- "dash_name": "产品研发数据大屏",
64
- "package_id": 1414909,
65
- "layout_preset": "dashboard_2col",
66
- "pages": [
67
- {
68
- "title": "研发总览",
69
- "components": [
70
- {
71
- "title": "部门分布",
72
- "source_type": "chart",
73
- "chart_ref": {"app_key": "APP_KEY", "chart_id": "CHART_ID"}
74
- }
75
- ]
76
- }
77
- ]
78
- }
6
+ qingflow --json builder portal get --dash-key DASH_KEY --schema-kind complete > portal.json # 更新前先读
7
+ qingflow --json builder portal validate --file portal.json # 零后端离线校验
8
+ qingflow builder portal apply --file portal.json --schema-version v1alpha1 [--dash-key DASH_KEY] [--no-publish]
79
9
  ```
80
10
 
81
- `name` 只是兼容别名;如果遇到旧版 CLI 报 **`name Extra inputs are not permitted`**,不要反复重试,改成 **`dash_name`** 或升级 CLI。
82
-
83
- ### 3.1.1 布局硬规则:PC 24 栅格,mobile 6 栅格
84
-
85
- **门户 PC 端不是 12 栅格,而是 24 栅格。**如果把两列写成 **`x=0/6, cols=6`**,所有组件只会占左半屏,工具会返回 **`PORTAL_LAYOUT_HALF_WIDTH`**。
86
-
87
- 推荐:
88
-
89
- | 场景 | PC 写法 |
90
- |------|---------|
91
- | 不确定布局 | **省略 `position`**,或传 **`--layout-preset auto`** |
92
- | 两列可视化图表 | **`x=0/12, cols=12`**,图表 **`rows >= 7`** |
93
- | 三列可视化图表 | **`x=0/8/16, cols=8`**,图表 **`rows >= 7`** |
94
- | 四个指标卡 | **`x=0/6/12/18, cols=6, rows=5`** |
95
- | 具体数据视图 / 明细表 | 优先 **`cols=12`** 或 **`cols=24`**,视图 **`rows >= 11`** |
96
-
97
- mobile 写法固定按 **6 栅格**:通常 **`x=0, cols=6`**。如果只写了 PC position,CLI 会自动补 mobile position,并返回 **`PORTAL_MOBILE_POSITION_MISSING`** 提醒。
98
-
99
- 图表卡片过小时会返回 **`PORTAL_CHART_CARD_TOO_SMALL`**;指标卡按 **`pc.cols >= 6, pc.rows >= 5`** 校验,普通可视化图表按 **`pc.cols >= 8, pc.rows >= 7`** 校验。指标区图表建议写 `role: "metric"`;此时必须引用 `target` / `indicator` 图表,否则返回 **`PORTAL_METRIC_SECTION_CHART_TYPE_MISMATCH`**,应先用 `app_charts_apply` 创建缺失指标卡再组门户。标准工作台数量也会进入 `layout_diagnostics.standard_template_counts`:指标卡推荐 4-6 个、BI 图表 2-3 个、业务视图 1-2 个,超出或不足会返回 `PORTAL_STANDARD_*_COUNT_OUT_OF_RANGE`。
100
-
101
- ### 3.1.2 推荐门户模板:业务工作台
102
-
103
- 默认按 **业务入口 → 核心指标 → BI 可视化 → 业务视图** 搭建。门户首屏应直接呈现可用工作台,不要做营销页、说明页或大面积装饰区。
104
-
105
- | 区域 | 推荐组件 | PC 布局 | 要点 |
106
- |------|----------|---------|------|
107
- | 顶部业务入口 | **`grid`** + 可选 **待办 `task`** | `grid x=0,y=0,cols=12,rows=4`;待办 `x=12,y=0,cols=12,rows=4` | `grid` 放 2-6 个核心业务入口;待办用于当前用户任务概览。当前公开 `portal apply --sections-file` 暂未支持 `source_type=task`,不要伪造;见下方边界说明。 |
108
- | 核心指标 | **`chart`** 指标卡 + `role: "metric"` | 4 张:`x=0/6/12/18,y=4,cols=6,rows=5` | 指标卡推荐高度 **5**。24 栅格下单卡最小 `cols=6`,所以一行 4 张最稳;如果要 5 个指标,拆成两行。 |
109
- | BI 可视化 | **`chart`** 图表 | 3 张:`x=0/8/16,y=9,cols=8,rows=7`;或 2 张:`x=0/12,cols=12,rows=7` | 可视化图表推荐高度 **7**,一行 2-3 个,1-2 行。 |
110
- | 业务数据视图 | **`view`** | `x=0/12,y=16,cols=12,rows=11`;或单表 `cols=24,rows=11` | 数据视图推荐高度 **11**,一行 1-2 个,1-2 行。只挂业务视图,不要创建或引用默认的 **全部数据 / 我的数据** 当主门户视图。 |
111
-
112
- mobile 固定按 **6 栅格** 从上到下堆叠:通常所有组件 `x=0, cols=6`,`y` 按 PC 顺序递增。若顶部同时有 `grid + task`,mobile 建议先业务入口 `y=0,rows=4`,再待办 `y=4,rows=4`,核心指标从 `y=8` 开始;若当前公开 CLI 只能写 `grid`,核心指标可从 `y=4` 开始。
113
-
114
- **当前工具边界**:
115
-
116
- - 公开 `sections-file` 稳定支持:**`grid` / `chart` / `view` / `text` / `link`**;`filter` 仍按 **§3.4** 的 raw `filterConfig` 边界处理,不作为主路径。
117
- - **待办 `task` / 常用 `favorite`** 是前端/后端 raw 组件形状,不在当前公开 `source_type` 合约里。智能体不要在 `sections-file` 中写 `source_type: "task"`;只有工具后续显式支持 `task`,或维护者走 raw 门户写入链路时,才使用待办槽位。
118
- - 推荐门户的可直接 apply 示例见 **[portal_sections_standard_workbench.example.json](./portal_sections_standard_workbench.example.json)**。该示例使用当前公开 CLI 可稳定写入的组件,顶部先用一个业务入口 `grid` 占满 24 栅格;如果工具已支持待办组件,再把顶部拆成 `grid cols=12 + task cols=12`。
119
- - `grid` 必须写 **`config.items[]`**;只写 `gridTitle` / `beingShowTitle` 会生成空入口容器,工具会返回 **`PORTAL_GRID_ITEMS_EMPTY`**。
120
-
121
- **待办组件 raw 形状参考**(仅用于工具实现对齐,当前不要作为 `--sections-file` 输入):
122
-
123
- ```json
124
- {
125
- "type": 8,
126
- "position": {
127
- "pc": {"x": 12, "y": 0, "cols": 12, "rows": 4},
128
- "mobile": {"x": 0, "y": 4, "cols": 6, "rows": 4}
129
- },
130
- "taskConfig": {
131
- "beingShowTitle": true,
132
- "componentTaskTitle": "待办",
133
- "beingShowHint": true,
134
- "componentTaskHint": "及时处理待办,可以有效提升流程效率",
135
- "dashTaskConfigList": [
136
- {"type": 1, "title": "all.taskTodo", "beingCheck": true, "ordinal": 0},
137
- {"type": 2, "title": "all.taskTimeout", "beingCheck": true, "ordinal": 1},
138
- {"type": 3, "title": "all.taskUpcomingTimeout", "beingCheck": true, "ordinal": 2},
139
- {"type": 4, "title": "all.taskRemind", "beingCheck": true, "ordinal": 3}
140
- ]
141
- }
142
- }
143
- ```
144
-
145
- ### 3.2 `source_type` ↔ 回读 `components[].type`(搭建侧)
146
-
147
- | `source_type` | 回读 `type`(数字) | 搭建侧含义 |
148
- |---------------|---------------------|------------|
149
- | **`chart`** | **9** | QingBI 图表块 |
150
- | **`view`** | **10** | 应用视图块 |
151
- | **`grid`** | **2** | 九宫格 |
152
- | **`filter`** | **6** | 筛选条 |
153
- | **`text`** | **5** | 富文本说明 |
154
- | **`link`** | **4** | 外链 |
155
-
156
- CLI 校验(`PortalSectionPatch`):**chart** 须 **`chart_ref`**;**view** 须 **`view_ref`**;**text** 须 **`text`**;**link** 须 **`url`**。**grid** / **filter** 无额外 ref,**`config` 形状由后端接受为准**。
157
-
158
- ### 3.3 **`chart`**:几乎总须 **`builder charts apply` 先建报表**
159
-
160
- - `portal` 只做 **`chart_ref` 解析**(`app_key` + **`chart_id`** 优先,否则 **唯一 `chart_name`**)。
161
- - 若 **`builder app get charts`** 为空,先 **`builder charts apply --app-key … --upsert-file`** 创建(契约 **minimal_example** 如 **`target` + `indicator_field_ids: []`**),从响应 **`chart_results[].chart_id`** 取 id 再写 **`chart_ref`**.
162
-
163
- ### 3.4 **`filter`**:`config` **整包**即接口里的 **`filterConfig`**
164
-
165
- 实现:组件为 **`{"type": 6, "filterConfig": deepcopy(section.config)}`**。
166
-
167
- - **理论上**与 solution 编译器一致:外层 **`{"filterConfig": [ … ], "graphList": [ … ]}`**;**`graphList`** 常含 **`graphType`**(如 **`CHART`**)、**`graphKey`** 或 **`graphRef`(`entity_id` / `chart_id`)** 等。
168
- - **本环境 CLI 直写实测(多次 `--publish`)**:在已验证 **chart / view / grid / text / link** 均可落库的前提下,**`source_type: filter` 始终未出现在 `draft_result.components` 中**;尝试过:**空数组**、**仅 `graphList` + QingBI `chart_id`**、**`graphKey` 为门户 `dashChartId`**、**`chart` 排在 **`filter` 前**、**仅 2 块(chart+filter)** 等,回读仍 **只有 chart(`count: 1`)** 或 **五类缺 `type: 6`**。
169
- **结论**:当前宜 **在搭建界面创建筛选条**,再 **`builder portal get` 反抓** 原始 `filterConfig` 形状;或向后端确认 **POST `/dash/{dashKey}`** 对 **`type: 6`** 的必填字段。**自动化不要默认认为 `filter` 已写入。**
170
- - portal `filter` 组件仍按 raw `filterConfig` 处理,不纳入视图 / 报表 / 关联资源的统一筛选 DSL 主链路。
171
-
172
- ### 3.5 **`grid` / `text` / `link` / `view`**
173
-
174
- - **grid**:**`config`** 并入 **`gridConfig`**;业务入口必须写 **`config.items[]`**。应用入口项推荐 `{ "type": 1, "jumpMode": 1, "linkAppKey": "APP_KEY", "linkFormType": 1, "title": "入口名" }`。空 `config` 或空 `items` 只会生成空入口容器,并触发 **`PORTAL_GRID_ITEMS_EMPTY`**。
175
- - **text** / **link**:**`text`** / **`url`** 必填。
176
- - **view**:**`view_ref`**;**`view_key`** 来自 **`builder app get views`**。
177
-
178
- ---
179
-
180
- ## 4. 前置数据
181
-
182
- | 目的 | 命令 |
183
- |------|------|
184
- | **`package_id`** | **`builder app resolve --app-key …`** → **`package_ids`** |
185
- | **`view_key`** | **`builder app get --app-key … views`** |
186
- | **`chart_id`** | **`builder app get charts`** 或 **`charts apply` 创建** |
187
-
188
- ---
189
-
190
- ## 5. 新建最小例(`--no-publish`)
191
-
192
- 见上文 **§2** 与 **`visibility`** 最小对象;**`sections`** 可先用 **单块 `view`**。成功后拿 **`dash_key`** 再 **更新 / 发布**。
193
-
194
- ---
195
-
196
- ## 6. 更新与替换语义
197
-
198
- - **`--dash-key`** + **`--sections-file`**:**列表即全量**;没写进去的区块会被拿掉。
199
- - 仅改 **可见性 / 图标 / 分组**:**不传 `--sections-file`**,且不要单独带 **§2** 所列「仅在有 sections 时允许」的键。
200
-
201
- ---
202
-
203
- ## 7. 删除:`delete`
204
-
205
- ```bash
206
- qingflow --json builder portal delete --dash-key "<DASH_KEY>" > tmp/builder_portal_delete.json
207
- ```
208
-
209
- - **只按 `dash_key` 删除一个门户**;先用 `builder portal list/get` 确认目标,不要按名称猜。
210
- - 返回成功时重点看:**`delete_executed`**、**`readback_status`**、**`safe_to_retry_delete`**、`summary.removed`。
211
- - **`delete_executed=true` + `readback_status=deleted`**:删除已执行且回读确认不存在。
212
- - **`delete_executed=true` + `readback_status=unavailable|still_exists`**:DELETE 已发出,但回读不可确认或短时间仍存在;**不要盲目重复删除**,稍后用 `builder portal get/list` 确认。
213
- - DELETE 本身失败时才按 `status=failed` / `error_code` 处理。
214
-
215
- ---
216
-
217
- ## 8. **发布**(`--publish`)
218
-
219
- - **默认会发布**:未显式 **`--no-publish`** 即 **`publish: true`**;公开入口仍是 **`portal_apply`**,发布动作由工具内部执行并回读 **`being_draft=false`**。
220
- - **已发布线索**:成功路径上 **`published: true`**;**`live_result.publishStatus`** 常见 **`2`** 表示已发布(以你环境枚举为准)。
221
- - **`partial_success` + `PORTAL_READBACK_PENDING`**:草稿/线上 **组件数量或元数据** 与「本次期望」不一致时会出现;**`PORTAL_*_VERIFICATION_INCOMPLETE`** 警告。应 **`builder portal get --dash-key …`**(草稿与 **`--no-being-draft` 线上**)**肉眼核对**区块是否齐全,而不是仅信 **`verified: true`**。
222
- - **网关 503**:**与 CLI 无关**;环境恢复后重试 **`portal get` / `portal apply`**。(本轮在网关恢复后 **`portal apply` 已成功**。)
223
-
224
- ---
225
-
226
- ## 9. 本环境实测摘要
227
-
228
- | 步骤 | 命令要点 | 结果 |
229
- |------|-----------|------|
230
- | 新建门户 | `--package-id 2030703`、`--dash-name "CLI门户探针_可删"`、`--no-publish`、单 **view** | **`dash_key`: `ecdcell64s02`** |
231
- | 补报表 | **`builder charts apply`**,`name: 门户探针_目标图`,**`target`** | **`chart_id`: `mcp_fb104267c5c249ca`** |
232
- | 503 后重试 | **`--dash-key ecdcell64s02`、`--publish`** + 含 **filter** 的六类 **`sections-file`** | **`published: true`**;**`filter` 仍不落库** → **`partial_success`**,**`components expected 6, got 5`**(与 **§3.4** 一致) |
233
- | **filter 专项** | 仅 **chart + filter** 两块的多种 **`graphList`/`graphRef`/`dashChartId`/`filterGroupConfig`** 组合 | **回读恒为 1 块(仅 `type: 9`)** |
234
- | **稳定可复现** | **`--publish`** + **[portal_sections_five_types.example.json](./portal_sections_five_types.example.json)**(**grid / text / link / view / chart**) | **`status: success`**,**`types: [2,4,5,9,10]`**,**`published: true`** |
235
- | 删除门户 | `builder portal delete --dash-key etcivtmv5402` | **`status: success`**,**`delete_executed: true`**,**`readback_status: deleted`**,**`summary.removed: 1`** |
236
-
237
- **推荐工作台模板示例**:[portal_sections_standard_workbench.example.json](./portal_sections_standard_workbench.example.json)
238
-
239
- **含 `filter` 的六类示例(`filter` 是否落库视环境/后端而定)**:[portal_sections_all_types.example.json](./portal_sections_all_types.example.json)
240
-
241
- ---
242
-
243
- ## 10. 排障
244
-
245
- | 现象 | 处理 |
246
- |------|------|
247
- | `portal apply accepts exactly one selector mode` | **`dash-key`** vs **`package-id`** 二选一。 |
248
- | `PORTAL_READBACK_PENDING` / **`components expected N, got M`** | **`portal get`** 数组件;若多写了 **`filter`** 而回读少一块,见 **§3.4**(**CLI 写 filter 可能整体不生效**);也可能是 **异步回读**,可再 **`get`** 一次。 |
249
- | **`published: true` 但 `verified: false`** | 读 **§7**;用 **`get` 草稿+线上**对照。 |
250
- | **503** | 网关/运维;稍后重试。 |
251
- | 缺 **chart** | **[QINGFLOW_CLI_BUILDER_CHARTS_WORKFLOW.md](./QINGFLOW_CLI_BUILDER_CHARTS_WORKFLOW.md)**:`builder charts apply`;或换有报表的应用。 |
252
- | **VISIBILITY_CONFLICT** | 只用 **`--visibility-file`**。 |
253
- | `delete_executed=true` 但 `readback_status=unavailable|still_exists` | DELETE 已发出,回读未确认;不要重复删除,稍后 `builder portal get/list` 复核。 |
254
-
255
- ---
256
-
257
- ## 11. 交叉引用
258
-
259
- - [SKILL.md](../SKILL.md)
260
- - [QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md](./QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md)
261
- - [QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md](./QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md)
262
- - [QINGFLOW_CLI_BUILDER_CHARTS_WORKFLOW.md](./QINGFLOW_CLI_BUILDER_CHARTS_WORKFLOW.md)
263
- - [QINGFLOW_CLI_EXPLORATION_REPORT.md](./QINGFLOW_CLI_EXPLORATION_REPORT.md)
11
+ 契约与九类组件专项 Schema:`qingflow --json builder portal schema --schema-kind complete|<类型>`。
@@ -141,7 +141,7 @@ qingflow --json builder portal get --dash-key "<DASH_KEY>" --no-being-draft
141
141
 
142
142
  ### 3.1 公共键
143
143
 
144
- - **`title`**(必填)、**`source_type`**(必填,小写)、可选 **`position`**(**`pc`/`mobile`** 各 **`x,y,cols,rows`**;省略则由**实现自动排布**)。
144
+ - **`source_type`**(必填,小写)、**`title`**(可选——schema 仅强制 `source_type`;新建组件建议提供标题,存量 filter 组件合法无标题,round-trip 透传不要合成)、可选 **`position`**(嵌套 **`pc`/`mobile`** 各 **`x,y,cols,rows`**;平铺别名 **`pc_x/pc_y/pc_w/pc_h`、`mobile_*`** 也接受,`x/y` 与 `w/h/cols/rows` 混写均认;省略则由**实现自动排布**)。
145
145
  - **`config`**、**`dash_style_config`**:按类型 Optional。默认只写 snake_case 规范键。
146
146
 
147
147
  ### 3.1.0 Payload 字段口径
@@ -348,7 +348,7 @@ qingflow --json builder portal delete --dash-key "<DASH_KEY>" > tmp/builder_port
348
348
 
349
349
  ## 8. **发布**(`--publish`)
350
350
 
351
- - **默认会发布**:未显式 **`--no-publish`** 即 **`publish: true`**;公开入口仍是 **`portal_apply`**,发布动作由工具内部执行并回读 **`being_draft=false`**。
351
+ - **默认会发布**:未显式 **`--no-publish`** 即 **`publish: true`**;公开入口仍是 **`portal_apply`**,发布动作由工具内部执行并回读 **`being_draft=false`**。**注意**:对**已发布门户**用 **`--no-publish`** 更新会把发布状态**降回草稿**(草稿写了、线上态被覆盖标记)——需要保留线上态的试写/迭代不要带 **`--no-publish`** 对已发布门户提交,或写完后显式补发布。
352
352
  - **已发布线索**:成功路径上 **`published: true`**;**`live_result.publishStatus`** 常见 **`2`** 表示已发布(以你环境枚举为准)。
353
353
  - **`partial_success` + `PORTAL_READBACK_PENDING`**:草稿/线上 **组件数量或元数据** 与「本次期望」不一致时会出现;**`PORTAL_*_VERIFICATION_INCOMPLETE`** 警告。应 **`builder portal get --dash-key …`**(草稿与 **`--no-being-draft` 线上**)**肉眼核对**区块是否齐全,而不是仅信 **`verified: true`**。
354
354
  - **网关 503**:**与 CLI 无关**;环境恢复后重试 **`portal get` / `portal apply`**。(本轮在网关恢复后 **`portal apply` 已成功**。)
@@ -2513,8 +2513,11 @@ class PortalComponentPositionPatch(StrictModel):
2513
2513
  # 因此显式 null 必须原样透传(round-trip 保真);仅键缺省时才回落布局默认值。
2514
2514
  pc_x: int | None = Field(default=0, validation_alias=AliasChoices("pc_x", "pcX", "x"))
2515
2515
  pc_y: int | None = Field(default=0, validation_alias=AliasChoices("pc_y", "pcY", "y"))
2516
- pc_w: int | None = Field(default=12, validation_alias=AliasChoices("pc_w", "pcW", "w"))
2517
- pc_h: int | None = Field(default=8, validation_alias=AliasChoices("pc_h", "pcH", "h"))
2516
+ # 宽高别名同时接受 w/h 与嵌套形态词汇 cols/rows:真实使用反馈显示 agent 常把
2517
+ # 嵌套词汇(cols/rows)与平铺词汇(x/y)混写(position: {"x":0,"y":0,"cols":24,"rows":5}),
2518
+ # 词汇表分裂是位写出错的高频形态(schema 描述同步说明)。
2519
+ pc_w: int | None = Field(default=12, validation_alias=AliasChoices("pc_w", "pcW", "w", "cols"))
2520
+ pc_h: int | None = Field(default=8, validation_alias=AliasChoices("pc_h", "pcH", "h", "rows"))
2518
2521
  mobile_x: int | None = Field(default=0, validation_alias=AliasChoices("mobile_x", "mobileX"))
2519
2522
  mobile_y: int | None = Field(default=0, validation_alias=AliasChoices("mobile_y", "mobileY"))
2520
2523
  mobile_w: int | None = Field(default=6, validation_alias=AliasChoices("mobile_w", "mobileW"))
@@ -2759,8 +2762,16 @@ class PortalApplyRequest(StrictModel):
2759
2762
  payload = dict(value)
2760
2763
  payload.pop("schema_version", None)
2761
2764
  payload.pop("schemaVersion", None)
2762
- if "dash_name" not in payload and "dashName" not in payload and "name" in payload:
2763
- payload["dash_name"] = payload.pop("name")
2765
+ # schema 文档声明的别名补全(dashKey/dashName):此前条件判断里出现了 dashName
2766
+ # 却从未消费,别名声明撞上 StrictModel 被拒,形成 validate 提示与 apply 行为
2767
+ # 不一致的观感(实测反馈失败二)。canonical 键优先;别名始终被消费(与 canonical
2768
+ # 同传时静默让位,不留残键触发 extra_forbidden)。
2769
+ if "dashKey" in payload:
2770
+ payload.setdefault("dash_key", payload.pop("dashKey"))
2771
+ if "name" in payload:
2772
+ payload.setdefault("dash_name", payload.pop("name"))
2773
+ if "dashName" in payload:
2774
+ payload.setdefault("dash_name", payload.pop("dashName"))
2764
2775
  if "dash_global_config" in payload and "global_config" not in payload:
2765
2776
  payload["global_config"] = payload.pop("dash_global_config")
2766
2777
  elif "dashGlobalConfig" in payload and "global_config" not in payload:
@@ -27,7 +27,7 @@
27
27
  "type": "integer",
28
28
  "minimum": 1,
29
29
  "packageTagIdCompleteDescription": "归属应用包 tagId;创建必填,更新缺省沿用读面归属。别名 packageTagId/package_id/packageId。 创建(无 dash_key)时必填——由 portal_apply 实现层校验(PACKAGE_TAG_REQUIRED + 包新增权限守卫);更新(带 dash_key)缺省沿用读面归属,无所属包的门户提取产物合法缺省本字段。",
30
- "x-backend": "dash.tags[0].tagId(POST /dash/{dashKey}/baseInfo)"
30
+ "x-backend": "dash.tags[0].tagId"
31
31
  },
32
32
  "sections": {
33
33
  "type": "array",
@@ -50,16 +50,16 @@
50
50
  "publish": {
51
51
  "type": "boolean",
52
52
  "default": true,
53
- "publishCompleteDescription": "apply 后是否发布;幂等重放时用 false。"
53
+ "publishCompleteDescription": "apply 后是否发布(true=写后发布并回读线上态;false=仅落草稿)。这是生命周期动作参数,不属于声明内容,幂等重放时用 false。注意:对已发布门户用 false 更新会把发布状态降回草稿——需要保留线上态时不要用 false 试写。"
54
54
  },
55
55
  "icon": {
56
56
  "type": "string",
57
- "portalIconCompleteDescription": "门户图标名(workspace icon 目录,如 view-grid);与 color 一起编码为 dashIcon。省略时按名称生成稳定默认图标。",
57
+ "portalIconCompleteDescription": "门户图标名,必须取 workspace icon 目录(qingflow builder icon catalog,如 view-grid;不允许 template)。创建语义(无 dash_key)必须显式给出,省略会被拒绝;更新语义省略=保留存量图标。与 color 一起编码为 dashIcon。",
58
58
  "x-backend": "dash.dashIcon(编码)"
59
59
  },
60
60
  "color": {
61
61
  "type": "string",
62
- "portalColorCompleteDescription": "门户图标底色(如 #2468F2);与 icon 一起编码为 dashIcon。",
62
+ "portalColorCompleteDescription": "门户图标底色,创建语义必须取 workspace icon 目录色名(qing-orange/blue 等,目录见 qingflow builder icon catalog,hex 值如 #2468F2 会被拒绝);更新语义允许存量值(含旧版 hex)原样透传。与 icon 一起编码为 dashIcon。",
63
63
  "x-backend": "dash.dashIcon(编码)"
64
64
  },
65
65
  "hide_copyright": {
@@ -403,7 +403,7 @@
403
403
  "Position": {
404
404
  "type": "object",
405
405
  "additionalProperties": false,
406
- "positionShapeCompleteDescription": "网格位置。PC 是 24 列网格(cols≤24),mobile 是 6 列网格(cols≤6);mobile 可省略。平铺别名 pc_x/pc_y/pc_w/pc_h、mobile_* 也接受。",
406
+ "positionShapeCompleteDescription": "网格位置。PC 是 24 列网格(cols≤24),mobile 是 6 列网格(cols≤6);mobile 可省略。平铺别名 pc_x/pc_y/pc_w/pc_h、mobile_* 也接受(x/y 与 w/h/cols/rows 混写均认)。",
407
407
  "properties": {
408
408
  "pc": {
409
409
  "$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"
@@ -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"
@@ -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(