@qingflow-tech/qingflow-cli 1.1.61-beta.4 → 1.1.61-beta.5
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 -1
- package/skills/qingflow-cli/reference/builder/30-schema-fields.md +13 -0
- package/skills/qingflow-cli/reference/builder/70-portal.md +37 -16
- package/skills/qingflow-cli/reference/examples/portal/portal_apply_v1alpha1.example.json +100 -0
- package/skills/qingflow-cli/reference/examples/portal/portal_sections_all_types.example.json +6 -7
- package/src/qingflow_mcp/app_form_validator.py +20 -3
- package/src/qingflow_mcp/builder_facade/app_form.py +137 -17
- package/src/qingflow_mcp/builder_facade/models.py +164 -13
- package/src/qingflow_mcp/builder_facade/portals_schema.py +397 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +14 -1
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +49 -9
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +27 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_content_classification.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_content_extraction.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_content_generation.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_document_parsing.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_image_generation.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_image_recognition.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_machine_translation.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_text_summary.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +2 -2
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +27 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date_duration.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/embed_field.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/image_select.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/location.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +2 -2
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ocr.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +2 -2
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +27 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +29 -2
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +20 -7
- package/src/qingflow_mcp/builder_facade/schemas/portals/manifest.json +59 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/complete.json +1397 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/carousel.json +193 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/chart.json +643 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/favourite.json +203 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/filter.json +268 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/grid.json +265 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/link.json +131 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/task.json +199 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/text.json +123 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/view.json +196 -0
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/summary.json +800 -0
- package/src/qingflow_mcp/builder_facade/service.py +1104 -71
- package/src/qingflow_mcp/cli/commands/builder.py +114 -5
- package/src/qingflow_mcp/public_surface.py +2 -0
- package/src/qingflow_mcp/server_app_builder.py +32 -1
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +73 -0
- package/src/qingflow_mcp/solution/executor.py +5 -2
- package/src/qingflow_mcp/tools/ai_builder_tools.py +110 -12
- package/src/qingflow_mcp/tools/portal_tools.py +4 -54
- package/src/qingflow_mcp/tools/portal_writer.py +59 -0
- package/src/qingflow_mcp/tools/solution_tools.py +2 -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.
|
|
10
|
+
npm install @qingflow-tech/qingflow-cli@1.1.61-beta.5
|
|
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.
|
|
24
|
+
npx -y -p @qingflow-tech/qingflow-cli@1.1.61-beta.5 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.
|
|
65
|
+
npx -y -p @qingflow-tech/qingflow-cli@1.1.61-beta.5 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.
|
|
3
|
+
"version": "1.1.61-beta.5",
|
|
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
|
@@ -218,6 +218,19 @@ current CLI payload.
|
|
|
218
218
|
| `relation` | 关联记录(**`config.relationMode`**:`single` \| `multiple`) |
|
|
219
219
|
| `data_relation` | 数据关联/引用填充 |
|
|
220
220
|
| `subtable` | 子表(**`subfields[]`**) |
|
|
221
|
+
| `date_duration` | 日期区间(透传型,无特殊配置) |
|
|
222
|
+
| `image_select` | 图片单选(透传型,选项经模板保真) |
|
|
223
|
+
| `location` | 地理位置(透传型) |
|
|
224
|
+
| `ocr` | OCR 识别(透传型) |
|
|
225
|
+
| `embed_field` | 内嵌字段(透传型) |
|
|
226
|
+
| `ai_content_generation` | AI 内容生成(透传型) |
|
|
227
|
+
| `ai_content_extraction` | AI 内容提取(透传型) |
|
|
228
|
+
| `ai_content_classification` | AI 内容分类(透传型) |
|
|
229
|
+
| `ai_machine_translation` | AI 机器翻译(透传型) |
|
|
230
|
+
| `ai_image_recognition` | AI 图像识别(透传型) |
|
|
231
|
+
| `ai_image_generation` | AI 图像生成(透传型) |
|
|
232
|
+
| `ai_document_parsing` | AI 文档解析(透传型) |
|
|
233
|
+
| `ai_text_summary` | AI 文本摘要(透传型) |
|
|
221
234
|
|
|
222
235
|
历史表单中的后端 `queType=1` 是“描述文字”布局控件,不属于上述新建字段类型。`app-form get` 为了保持完整声明可校验,会将它兼容输出为 `long_text`;`app-form apply` 会根据已有字段模板恢复原始 `queType=1` 和 `queDefaultValue`,所以修改其他字段时不会丢失描述文字。新建可填写多行文本仍使用 `long_text`,不要用它创建描述文字控件。
|
|
223
236
|
|
|
@@ -11,13 +11,26 @@ Not responsible for: creating apps or views referenced by the portal. Create or
|
|
|
11
11
|
## Main chain
|
|
12
12
|
|
|
13
13
|
```text
|
|
14
|
-
portal list/get if updating -> verify apps/views and read chart lists only when reusing existing charts
|
|
14
|
+
portal list/get if updating -> verify apps/views and read chart lists only when reusing existing charts
|
|
15
|
+
-> (offline) builder portal validate --file decl.json
|
|
16
|
+
-> builder portal apply --file decl.json [--schema-version v1alpha1]
|
|
17
|
+
-> builder portal get --dash-key K --schema-kind summary|complete (readback) -> publish state
|
|
15
18
|
```
|
|
16
19
|
|
|
20
|
+
## Versioned schema and declaration files
|
|
21
|
+
|
|
22
|
+
- Declarations are versioned: `schema_version` (default `v1alpha1`), `apiVersion: builder.qingflow.com/v1alpha1`. The envelope echoes `schemaVersion` when you pass `--schema-version`.
|
|
23
|
+
- Two document kinds: **summary** = name + layout + component list (five stable component types: `chart/view/grid/text/link`); **complete** = summary superset with global config (`icon/color/hide_copyright/global_config/style_template_id/overall_style`), nine component types (adds `filter/carousel/task/favourite`), component `style_config`, and `visibility` authorization.
|
|
24
|
+
- Read the contract itself: `qingflow --json builder portal schema --schema-kind summary|complete|<component>`; every component type (`chart|view|grid|filter|text|link|carousel|task|favourite`) has a self-contained schema — fetch it before guessing config keys.
|
|
25
|
+
- `builder portal get` defaults to `--schema-kind summary` and returns a canonical `declaration` (the round-trip anchor for replay tooling). Use `--schema-kind raw` for the legacy normalized response and `--schema-kind complete` for the full declaration.
|
|
26
|
+
- Offline validation: `qingflow --json builder portal validate --file decl.json` (accepts `-` for stdin; zero backend calls). Fix issues before applying.
|
|
27
|
+
- Apply main form: `qingflow builder portal apply --file decl.json --schema-version v1alpha1 [--no-publish]`. Legacy patch parameters (`--sections-file`, `--visibility-file`, ...) still work in a compatibility window but emit a deprecation notice; do not use them in new automations. `--payload-file` (raw DTO passthrough) is legacy-only.
|
|
28
|
+
|
|
17
29
|
## Demo files
|
|
18
30
|
|
|
19
31
|
| Scenario | Example |
|
|
20
32
|
|----------|---------|
|
|
33
|
+
| Recommended v1alpha1 declaration (`--file`) | [portal_apply_v1alpha1.example.json](../examples/portal/portal_apply_v1alpha1.example.json) |
|
|
21
34
|
| Recommended business workbench | [portal_sections_standard_workbench.example.json](../examples/portal/portal_sections_standard_workbench.example.json) |
|
|
22
35
|
| Stable component capability probe | [portal_sections_five_types.example.json](../examples/portal/portal_sections_five_types.example.json) |
|
|
23
36
|
| Raw/experimental all-types probe | [portal_sections_all_types.example.json](../examples/portal/portal_sections_all_types.example.json) |
|
|
@@ -66,8 +79,9 @@ Recommended standard workbench skeleton:
|
|
|
66
79
|
|
|
67
80
|
## Update existing portal
|
|
68
81
|
|
|
69
|
-
-
|
|
70
|
-
-
|
|
82
|
+
- Prefer the declaration-file form: `builder portal apply --file decl.json --schema-version v1alpha1 --dash-key K --no-publish` (the file may omit `dash_key` and pass it via `--dash-key`; do not send `package_id` for updates).
|
|
83
|
+
- Read the current declaration first with `builder portal get --dash-key K --schema-kind complete`, edit it, `builder portal validate --file`, then apply. `sections` is full-replace semantics.
|
|
84
|
+
- `patch_sections[]` (via `--patch-sections-file`, compatibility window) remains for targeted section changes when a full declaration round-trip is not practical.
|
|
71
85
|
- `patch_sections[]` selectors may use `order`, `chart_ref.chart_id`, `chart_ref.chart_key`, or `view_ref.view_key`; chart sections read back from portal often only include `chart_id`, so patching by `chart_id` is valid and does not require `app_key`.
|
|
72
86
|
- If using a business-entry grid, include real `items`; do not submit an empty entry container.
|
|
73
87
|
- Validate referenced `chart_key`, `chart_id`, `view_key`, or `app_key` before claiming the portal is complete.
|
|
@@ -110,7 +124,9 @@ qingflow --json builder portal get --dash-key "<DASH_KEY>" --no-being-draft
|
|
|
110
124
|
| 参数 | 说明 |
|
|
111
125
|
|------|------|
|
|
112
126
|
| **`--publish` / `--no-publish`** | 默认 **`--publish`**。仅 **`--no-publish`** 时不会调用发布接口;契约说明 **`publish=false` 不宣称线上已变**。 |
|
|
113
|
-
| **`--
|
|
127
|
+
| **`--file`**(主形态) | 完整门户声明 JSON 对象(支持 **`-`** stdin);与 patch 参数互斥;声明内可带 **`schema_version`**。 |
|
|
128
|
+
| **`--schema-version`** | 固定声明契约版本(缺省读文件内 `schema_version`,再缺省 latest)。传入时声明先过 complete schema 离线校验,**`INVALID_SPEC` 不写**。 |
|
|
129
|
+
| **`--payload-file`**(旧) | 完整门户 JSON 对象透传(兼容窗口);新代码改用 **`--file`**。标准字段写 `dash_name`。 |
|
|
114
130
|
| **`--sections-file`** | JSON **数组**;**全量替换**。省略时仅改基础信息,且 **`hide_copyright` / `dash_global_config` / `config`** 不能单独提交(**`PORTAL_SECTIONS_REQUIRED`**)。 |
|
|
115
131
|
| **`--layout-preset`** | 可选:**`auto` / `dashboard_2col` / `dashboard_3col`**。当区块未显式写 **`position`** 时由工具生成大屏布局。 |
|
|
116
132
|
| **`--visibility-file`** | 与 **`--auth-file`** 不能同时用。 |
|
|
@@ -198,8 +214,8 @@ mobile 固定按 **6 栅格** 从上到下堆叠:通常所有组件 `x=0, cols
|
|
|
198
214
|
|
|
199
215
|
**当前工具边界**:
|
|
200
216
|
|
|
201
|
-
-
|
|
202
|
-
-
|
|
217
|
+
- 声明式(`--file` / complete 档)支持**九类组件**:**`grid` / `chart` / `view` / `text` / `link`**(summary 档稳定面)+ **`carousel` / `task` / `favourite`**(complete 档,配置键见专项 Schema)+ **`filter`**(complete 档受控承载,仍按 **§3.4** raw 边界处理,不作为主路径)。
|
|
218
|
+
- **`task`(一个门户最多 1 个)与 `favourite`(一个门户最多 1 个)** 已进入 v1alpha1 complete 声明面(`task`/`favourite` 类型化 config 键),不要再按后端 raw 形状(`taskConfig`/`favouriteConfig`)手拼;声明形态见 `qingflow --json builder portal schema --schema-kind task|favourite`。旧文档中的「task/favorite 不在公开合约里」已过时。
|
|
203
219
|
- 推荐门户的可直接 apply 示例见 **[portal_sections_standard_workbench.example.json](../examples/portal/portal_sections_standard_workbench.example.json)**。这是唯一推荐复制的业务工作台布局模板:每一行 `cols_sum=24`,同行 `rows` 一致,下一行 `y` 连续。顶部先用一个业务入口 `grid` 占满 24 栅格;如果工具已支持待办组件,再把顶部拆成 `grid cols=12 + task cols=12`。
|
|
204
220
|
- **[portal_sections_five_types.example.json](../examples/portal/portal_sections_five_types.example.json)** 和 **[portal_sections_all_types.example.json](../examples/portal/portal_sections_all_types.example.json)** 仅用于组件能力探针,不作为业务门户布局模板;搭建正式门户时不要从这些探针推导布局。
|
|
205
221
|
- `grid` 必须写 **`config.items[]`**;只写 `gridTitle` / `beingShowTitle` 会生成空入口容器,工具会返回 **`PORTAL_GRID_ITEMS_EMPTY`**。
|
|
@@ -230,16 +246,21 @@ mobile 固定按 **6 栅格** 从上到下堆叠:通常所有组件 `x=0, cols
|
|
|
230
246
|
|
|
231
247
|
### 3.2 `source_type` ↔ 回读 `components[].type`(搭建侧)
|
|
232
248
|
|
|
233
|
-
| `source_type` | 回读 `type`(数字) | 搭建侧含义 |
|
|
234
|
-
|
|
235
|
-
| **`chart`** | **9** | QingBI 图表块 |
|
|
236
|
-
| **`view`** | **10** | 应用视图块 |
|
|
237
|
-
| **`grid`** | **2** | 九宫格 |
|
|
238
|
-
| **`
|
|
239
|
-
| **`
|
|
240
|
-
| **`
|
|
241
|
-
|
|
242
|
-
|
|
249
|
+
| `source_type` | 回读 `type`(数字) | 搭建侧含义 | 声明档 |
|
|
250
|
+
|---------------|---------------------|------------|----------|
|
|
251
|
+
| **`chart`** | **9** | QingBI 图表块 | summary |
|
|
252
|
+
| **`view`** | **10** | 应用视图块 | summary |
|
|
253
|
+
| **`grid`** | **2** | 九宫格 | summary |
|
|
254
|
+
| **`text`** | **5** | 富文本说明 | summary |
|
|
255
|
+
| **`link`** | **4** | 外链 | summary |
|
|
256
|
+
| **`filter`** | **6** | 筛选条 | complete(raw/unstable) |
|
|
257
|
+
| **`carousel`** | **3** | 轮播(数据源应用 + 标题/图片/链接字段) | complete |
|
|
258
|
+
| **`task`** | **8** | 待办(一个门户最多 1 个) | complete |
|
|
259
|
+
| **`favourite`** | **7** | 收藏(一个门户最多 1 个) | complete |
|
|
260
|
+
|
|
261
|
+
每类组件的详细配置键经专项 Schema 获取:`qingflow --json builder portal schema --schema-kind <类型>`(如 `--schema-kind carousel`、`--schema-kind task`)。`carousel` 的 `title_field/image_field/link_field` 填字段显示名(CLI 编译为 queId);`task.tasks[].task_type` 取值 1=我的待办 2=已超时 3=即将超时 4=催办 5=我发起的 6=抄送我的 7=草稿箱 8=我的已办 9=待完善;`favourite.items[]` 用 `item_type`(1=应用 2=门户)+`link_key` 锚定目标。
|
|
262
|
+
|
|
263
|
+
CLI 校验(`PortalSectionPatch`):**chart** 须 **`chart`** 或 **`chart_ref`**;**view** 须 **`view_ref`**;**text** 须 **`text`**;**link** 须 **`url`**;**carousel/task/favourite** 须各自类型化 config 键。**grid** / **filter** 无额外 ref,**`config` 形状由专项 Schema 与后端接受为准**。
|
|
243
264
|
|
|
244
265
|
### 3.3 **`chart`**:一个字段同时支持引用已有图表和内联创建图表
|
|
245
266
|
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "v1alpha1",
|
|
3
|
+
"name": "业务工作台",
|
|
4
|
+
"package_tag_id": 2030703,
|
|
5
|
+
"icon": "view-grid",
|
|
6
|
+
"color": "#2468F2",
|
|
7
|
+
"hide_copyright": false,
|
|
8
|
+
"global_config": {
|
|
9
|
+
"auto_refresh": false,
|
|
10
|
+
"interval": 60,
|
|
11
|
+
"export": true
|
|
12
|
+
},
|
|
13
|
+
"visibility": {
|
|
14
|
+
"mode": "workspace",
|
|
15
|
+
"external_mode": "not"
|
|
16
|
+
},
|
|
17
|
+
"layout_preset": "auto",
|
|
18
|
+
"sections": [
|
|
19
|
+
{
|
|
20
|
+
"title": "业务入口",
|
|
21
|
+
"source_type": "grid",
|
|
22
|
+
"position": {
|
|
23
|
+
"pc": { "x": 0, "y": 0, "cols": 24, "rows": 4 }
|
|
24
|
+
},
|
|
25
|
+
"config": {
|
|
26
|
+
"gridTitle": "业务入口",
|
|
27
|
+
"beingShowTitle": true,
|
|
28
|
+
"items": [
|
|
29
|
+
{ "type": 1, "jumpMode": 1, "linkAppKey": "APP_KEY_DEVICE", "linkFormType": 1, "title": "设备台账" },
|
|
30
|
+
{ "type": 1, "jumpMode": 1, "linkAppKey": "APP_KEY_WORKORDER", "linkFormType": 1, "title": "工单管理" }
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"title": "设备总数",
|
|
36
|
+
"source_type": "chart",
|
|
37
|
+
"role": "metric",
|
|
38
|
+
"position": {
|
|
39
|
+
"pc": { "x": 0, "y": 4, "cols": 6, "rows": 5 }
|
|
40
|
+
},
|
|
41
|
+
"chart": {
|
|
42
|
+
"app_key": "APP_KEY_DEVICE",
|
|
43
|
+
"name": "设备总数",
|
|
44
|
+
"chart_type": "target",
|
|
45
|
+
"metric": "count(*)"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"title": "异常设备数",
|
|
50
|
+
"source_type": "chart",
|
|
51
|
+
"role": "metric",
|
|
52
|
+
"position": {
|
|
53
|
+
"pc": { "x": 6, "y": 4, "cols": 6, "rows": 5 }
|
|
54
|
+
},
|
|
55
|
+
"chart": {
|
|
56
|
+
"app_key": "APP_KEY_DEVICE",
|
|
57
|
+
"name": "异常设备数",
|
|
58
|
+
"chart_type": "target",
|
|
59
|
+
"metric": "count(*)",
|
|
60
|
+
"filters": [
|
|
61
|
+
{ "field_name": "状态", "operator": "eq", "values": ["异常"] }
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"title": "工单趋势",
|
|
67
|
+
"source_type": "chart",
|
|
68
|
+
"position": {
|
|
69
|
+
"pc": { "x": 0, "y": 9, "cols": 12, "rows": 7 }
|
|
70
|
+
},
|
|
71
|
+
"chart": {
|
|
72
|
+
"app_key": "APP_KEY_WORKORDER",
|
|
73
|
+
"name": "工单趋势",
|
|
74
|
+
"chart_type": "line",
|
|
75
|
+
"metric": "count(*)",
|
|
76
|
+
"group_by": ["创建日期"]
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"title": "工单类型分布",
|
|
81
|
+
"source_type": "chart",
|
|
82
|
+
"position": {
|
|
83
|
+
"pc": { "x": 12, "y": 9, "cols": 12, "rows": 7 }
|
|
84
|
+
},
|
|
85
|
+
"chart_ref": { "app_key": "APP_KEY_WORKORDER", "chart_name": "工单类型分布" }
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"title": "异常设备明细",
|
|
89
|
+
"source_type": "view",
|
|
90
|
+
"position": {
|
|
91
|
+
"pc": { "x": 0, "y": 16, "cols": 24, "rows": 11 }
|
|
92
|
+
},
|
|
93
|
+
"view_ref": {
|
|
94
|
+
"app_key": "APP_KEY_DEVICE",
|
|
95
|
+
"view_key": "VIEW_KEY_ABNORMAL_DEVICES"
|
|
96
|
+
},
|
|
97
|
+
"view_display": { "show_title": true }
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
}
|
package/skills/qingflow-cli/reference/examples/portal/portal_sections_all_types.example.json
CHANGED
|
@@ -17,14 +17,13 @@
|
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
19
|
"config": {
|
|
20
|
-
"
|
|
21
|
-
"graphList": [
|
|
20
|
+
"graphs": [
|
|
22
21
|
{
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"graphName": "可选_图表显示名"
|
|
22
|
+
"chart_key": "REPLACE_WITH_QINGBI_CHART_ID",
|
|
23
|
+
"chart_name": "可选_图表显示名"
|
|
26
24
|
}
|
|
27
|
-
]
|
|
25
|
+
],
|
|
26
|
+
"rules": []
|
|
28
27
|
}
|
|
29
28
|
},
|
|
30
29
|
{
|
|
@@ -142,4 +141,4 @@
|
|
|
142
141
|
"chart_id": "YOUR_CHART_ID"
|
|
143
142
|
}
|
|
144
143
|
}
|
|
145
|
-
]
|
|
144
|
+
]
|
|
@@ -28,10 +28,16 @@ _INTEGRATION_OUTPUT_TARGET_FIELD_TYPES = {
|
|
|
28
28
|
"single_select",
|
|
29
29
|
"multi_select",
|
|
30
30
|
"attachment",
|
|
31
|
+
# r10 存量实证放行:link(92nl6blfk402)/member(ab9fjvj33001)——后端写链
|
|
32
|
+
# 无类型白名单,运行时同为字符串写入路径
|
|
33
|
+
"link",
|
|
34
|
+
"member",
|
|
31
35
|
"data_relation",
|
|
32
36
|
"subtable",
|
|
33
37
|
}
|
|
34
|
-
|
|
38
|
+
# key 允许含空白(r10 实测存量 key 如「应用包 ID」带空格,\s 排除导致占位符
|
|
39
|
+
# 扫描漏检 → validate 误报「key must appear as {{key}}」)
|
|
40
|
+
_Q_LINKER_TEMPLATE_RE = re.compile(r"\{\{(?P<key>[^{}]+)\}\}")
|
|
35
41
|
_FORMULA_IDENTIFIER_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
|
36
42
|
#: 公式字段引用正则(#queId_<id>$,支持表格嵌套 <id>.<id>)
|
|
37
43
|
_FORMULA_FIELD_REF_RE = re.compile(r"#queId_([^$]+)\$")
|
|
@@ -353,7 +359,15 @@ def _static_errors(payload: dict[str, Any]) -> list[tuple[str, str]]:
|
|
|
353
359
|
"is_empty and is_not_empty require a literal operand with values: []",
|
|
354
360
|
)
|
|
355
361
|
)
|
|
356
|
-
elif
|
|
362
|
+
elif (
|
|
363
|
+
operand.get("kind") == "literal"
|
|
364
|
+
and not values
|
|
365
|
+
and operator not in {"subordinate_to", "not_subordinate_to"}
|
|
366
|
+
):
|
|
367
|
+
# subordinate 两算子放行空 literal:存量「设置后清空」形态
|
|
368
|
+
# (r13 实证 41ce0ad8 六处)——后端写链 checkRelationMatchRules
|
|
369
|
+
# 仅校验 dynamic_range(FormSvcImpl:10448),formula 渲染对空
|
|
370
|
+
# judgeValueDetails 输出 [](Formula:397),读回解析对称,往返保真
|
|
357
371
|
errors.append(
|
|
358
372
|
(
|
|
359
373
|
f"{operand_path}/values",
|
|
@@ -386,7 +400,10 @@ def _static_errors(payload: dict[str, Any]) -> list[tuple[str, str]]:
|
|
|
386
400
|
selected_name = selected[1].get("name") if selected is not None else selector.get("name")
|
|
387
401
|
if not selected_name and selector.get("queId") in fields_by_id:
|
|
388
402
|
selected_name = fields_by_id[selector["queId"]].get("name")
|
|
389
|
-
|
|
403
|
+
# 两侧 strip:模板引用名提取时已 strip(字段名存量带尾随空格,如
|
|
404
|
+
# am4oh08v2401「答:Answer 」),严格相等会把「引用名≠当前字段名」的
|
|
405
|
+
# 存量形态误报为 key 不一致
|
|
406
|
+
if selected_name and str(item.get("key") or "").strip() != str(selected_name).strip():
|
|
390
407
|
errors.append((f"{input_path}/key", "json_path key must equal the selected field name"))
|
|
391
408
|
|
|
392
409
|
if field_type == "q_linker":
|
|
@@ -54,6 +54,24 @@ QUE_TYPE_TO_CANONICAL = {
|
|
|
54
54
|
22: "department",
|
|
55
55
|
25: "relation",
|
|
56
56
|
26: "code_block",
|
|
57
|
+
# 透传型字段(r10 读面 65 app 缺口):声明层 EmptyConfig,round-trip 经后端
|
|
58
|
+
# 模板保真(选项/技能配置列随模板透传);语义 = QuestionTypeEnum 14/15/17/23
|
|
59
|
+
# 与 Constants QUE_TYPE_EMBED_FIELD(27)、AI 系 30-37(AI_CONTENT_GENERATION
|
|
60
|
+
# /EXTRACTION/CLASSIFICATION、AI_MACHINE_TRANSLATION、AI_IMAGE_RECOGNITION
|
|
61
|
+
# /GENERATION、AI_DOCUMENT_PARSING、AI_TEXT_SUMMARY)
|
|
62
|
+
14: "date_duration",
|
|
63
|
+
15: "image_select",
|
|
64
|
+
17: "location",
|
|
65
|
+
23: "ocr",
|
|
66
|
+
27: "embed_field",
|
|
67
|
+
30: "ai_content_generation",
|
|
68
|
+
31: "ai_content_extraction",
|
|
69
|
+
32: "ai_content_classification",
|
|
70
|
+
33: "ai_machine_translation",
|
|
71
|
+
34: "ai_image_recognition",
|
|
72
|
+
35: "ai_image_generation",
|
|
73
|
+
36: "ai_document_parsing",
|
|
74
|
+
37: "ai_text_summary",
|
|
57
75
|
}
|
|
58
76
|
CANONICAL_TO_QUE_TYPE = {
|
|
59
77
|
"text": 2,
|
|
@@ -78,6 +96,20 @@ CANONICAL_TO_QUE_TYPE = {
|
|
|
78
96
|
"data_relation": 19,
|
|
79
97
|
"relation": 25,
|
|
80
98
|
"subtable": 18,
|
|
99
|
+
|
|
100
|
+
"date_duration": 14,
|
|
101
|
+
"image_select": 15,
|
|
102
|
+
"location": 17,
|
|
103
|
+
"ocr": 23,
|
|
104
|
+
"embed_field": 27,
|
|
105
|
+
"ai_content_generation": 30,
|
|
106
|
+
"ai_content_extraction": 31,
|
|
107
|
+
"ai_content_classification": 32,
|
|
108
|
+
"ai_machine_translation": 33,
|
|
109
|
+
"ai_image_recognition": 34,
|
|
110
|
+
"ai_image_generation": 35,
|
|
111
|
+
"ai_document_parsing": 36,
|
|
112
|
+
"ai_text_summary": 37,
|
|
81
113
|
}
|
|
82
114
|
SUBFIELD_TYPES = {
|
|
83
115
|
"text",
|
|
@@ -1227,11 +1259,20 @@ class AppFormNormalizer:
|
|
|
1227
1259
|
name = match.group("name").strip()
|
|
1228
1260
|
if name:
|
|
1229
1261
|
token = match.group("token").strip()
|
|
1262
|
+
# 名字两侧 strip 匹配:存量字段名带尾随空格(如 am4oh08v2401
|
|
1263
|
+
# 「答:Answer 」),模板引用名无空格——严格相等会丢 queId 锚定
|
|
1230
1264
|
matching_ids = [
|
|
1231
1265
|
que_id
|
|
1232
1266
|
for que_id, field_name in field_names.items()
|
|
1233
|
-
if field_name == name and _encrypt_question_id(que_id) == token
|
|
1267
|
+
if str(field_name).strip() == name and _encrypt_question_id(que_id) == token
|
|
1234
1268
|
]
|
|
1269
|
+
if not matching_ids:
|
|
1270
|
+
# 悬空引用(字段已删/改名,模板仍留旧引用):原文透传——占位符
|
|
1271
|
+
# 化会丢 token 无法复原(加密不可逆),后端写链对模板原样落库;
|
|
1272
|
+
# 不注册 input(声明无此输入,写回与 B0 模板逐字一致,
|
|
1273
|
+
# r10 实测 9gtommb3ng01 等 6 app「inputs[N].field could not
|
|
1274
|
+
# be resolved」即此形态)
|
|
1275
|
+
return match.group(0)
|
|
1235
1276
|
json_path_inputs.setdefault(
|
|
1236
1277
|
name,
|
|
1237
1278
|
{
|
|
@@ -1244,11 +1285,21 @@ class AppFormNormalizer:
|
|
|
1244
1285
|
|
|
1245
1286
|
request_values[request_key] = Q_LINKER_VALUE_REFERENCE_RE.sub(replace_reference, raw_value)
|
|
1246
1287
|
inputs.extend(json_path_inputs.values())
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1288
|
+
# URL 空容忍边界(#208③):后端写链仅 configMode=1(自定义)校验 url/method/
|
|
1289
|
+
# resultType 非空(FormQuestionSvcImpl.createQuestionRemoteLookUp:444 抛
|
|
1290
|
+
# REMOTE_LOOK_UP_SETTING_WRONG);configMode=2 插件模板 / 3 连接中心 /
|
|
1291
|
+
# 4 轻翼智能体的 URL 由模板侧提供,本表 url 空 是合法存量形态——非自定义
|
|
1292
|
+
# 模式放行空 URL 透传(写回走模板透传,round-trip 保真;r12 归因 27 app;
|
|
1293
|
+
# 自定义模式 url 空的 2 app 脓数据仍拒绝,入排除账本)。
|
|
1294
|
+
config_mode = _positive_int(config.get("configMode")) or 1
|
|
1295
|
+
if config_mode == 1:
|
|
1296
|
+
request_url = _required_text(
|
|
1297
|
+
request_values["url"],
|
|
1298
|
+
message="Q-Linker request URL is empty",
|
|
1299
|
+
details={"queId": source_id, "path": "/config/qLinkerBinding/request/url"},
|
|
1300
|
+
)
|
|
1301
|
+
else:
|
|
1302
|
+
request_url = str(request_values["url"] or "")
|
|
1252
1303
|
request = _without_none(
|
|
1253
1304
|
{
|
|
1254
1305
|
"url": request_url,
|
|
@@ -1771,7 +1822,9 @@ class AppFormCompiler:
|
|
|
1771
1822
|
if not isinstance(item, dict) or item.get("source") != "json_path":
|
|
1772
1823
|
continue
|
|
1773
1824
|
selected = _resolve_internal_selector(item.get("field") or {}, internal_fields, f"qLinkerBinding.inputs[{index}].field")
|
|
1774
|
-
|
|
1825
|
+
# 两侧 strip:模板引用名提取时已 strip(存量字段名带尾随空格,
|
|
1826
|
+
# 如 r10 am4oh08v2401「答:Answer 」),严格相等误报 key 不一致
|
|
1827
|
+
if str(item.get("key") or "").strip() != str(selected.get("name") or "").strip():
|
|
1775
1828
|
raise AppFormFailure(
|
|
1776
1829
|
"UNSUPPORTED_FIELD_CONFIG",
|
|
1777
1830
|
"json_path input key must equal the selected field name for lossless readback",
|
|
@@ -2449,7 +2502,18 @@ class AppFormCompiler:
|
|
|
2449
2502
|
def _question(self, profile: str, app_key: str, field: JSONObject, internal: JSONObject, temp_id: int, relations: list[JSONObject]) -> JSONObject:
|
|
2450
2503
|
field_type = _field_type(field)
|
|
2451
2504
|
que_type = CANONICAL_TO_QUE_TYPE[field_type]
|
|
2452
|
-
build_type =
|
|
2505
|
+
build_type = (
|
|
2506
|
+
"boolean" if field_type == "single_choice"
|
|
2507
|
+
else "text" if field_type in {
|
|
2508
|
+
"link", "data_relation", "rich_text",
|
|
2509
|
+
# 透传型新类型(r10 读面覆盖):build 走 text 兜底,类型/配置经模板保真
|
|
2510
|
+
"date_duration", "image_select", "location", "ocr", "embed_field",
|
|
2511
|
+
"ai_content_generation", "ai_content_extraction", "ai_content_classification",
|
|
2512
|
+
"ai_machine_translation", "ai_image_recognition", "ai_image_generation",
|
|
2513
|
+
"ai_document_parsing", "ai_text_summary",
|
|
2514
|
+
}
|
|
2515
|
+
else field_type
|
|
2516
|
+
)
|
|
2453
2517
|
built_internal = deepcopy(internal)
|
|
2454
2518
|
built_internal["type"] = build_type
|
|
2455
2519
|
if build_type == "subtable":
|
|
@@ -2821,7 +2885,21 @@ class AppFormCompiler:
|
|
|
2821
2885
|
if source.get("kind") == "app":
|
|
2822
2886
|
source_fields, _target_form = self._target_form_candidates(profile, str(source.get("appKey") or ""))
|
|
2823
2887
|
source_fields_are_backend = True
|
|
2824
|
-
|
|
2888
|
+
# 展示字段同样按后端语义透传:写链对 displayedQueId 无存在性校验
|
|
2889
|
+
# 原样落库(fillMatchRulesList 读链按全局 question∪draft_question
|
|
2890
|
+
# 解析)。解析不到时 queType=None → 写侧 origin_type 回退 current
|
|
2891
|
+
# 模板值(见 data_relation relatedQueId/queOriginType 组装)。
|
|
2892
|
+
try:
|
|
2893
|
+
target = _resolve_backend_selector(source.get("field") or {}, source_fields, "source.field")
|
|
2894
|
+
except AppFormFailure as error:
|
|
2895
|
+
if error.error_code != "INVALID_FIELD_SELECTOR" or _positive_int((source.get("field") or {}).get("queId")) is None:
|
|
2896
|
+
raise
|
|
2897
|
+
target = {
|
|
2898
|
+
"queId": _positive_int(source["field"].get("queId")),
|
|
2899
|
+
"queTitle": str(source["field"].get("name") or "").strip() or None,
|
|
2900
|
+
"queType": None,
|
|
2901
|
+
"_app_form_unresolved": True,
|
|
2902
|
+
}
|
|
2825
2903
|
relation.update(
|
|
2826
2904
|
{
|
|
2827
2905
|
"displayedQueId": target["queId"],
|
|
@@ -3070,7 +3148,23 @@ class AppFormCompiler:
|
|
|
3070
3148
|
location: str,
|
|
3071
3149
|
) -> JSONObject:
|
|
3072
3150
|
if source_fields_are_backend:
|
|
3073
|
-
|
|
3151
|
+
try:
|
|
3152
|
+
return _resolve_backend_selector(selector, source_fields, location)
|
|
3153
|
+
except AppFormFailure as error:
|
|
3154
|
+
# 目标表单候选集(草稿∪发布 formQues)解析不到、但 selector 带正
|
|
3155
|
+
# queId:按后端语义透传——读链 fillMatchRulesList 按 queId 查
|
|
3156
|
+
# question∪draft_question 全局表,查不到容忍置空(new Question());
|
|
3157
|
+
# 写链 updateFormInfo 对 matchRules/sortConfig 源 queId 无存在性
|
|
3158
|
+
# 校验、原样落库。存量里该形态 = 引用指向目标表单 GET 响应之外
|
|
3159
|
+
# 的字段(跨表/已删,如 r8 实测 00e0e0d8 筛选源),严格拒绝即
|
|
3160
|
+
# round-trip 断裂。queType 置 None 与后端「查不到置空」同构。
|
|
3161
|
+
if error.error_code == "INVALID_FIELD_SELECTOR" and _positive_int(selector.get("queId")) is not None:
|
|
3162
|
+
return {
|
|
3163
|
+
"queId": _positive_int(selector.get("queId")),
|
|
3164
|
+
"queTitle": str(selector.get("name") or "").strip() or None,
|
|
3165
|
+
"_app_form_unresolved": True,
|
|
3166
|
+
}
|
|
3167
|
+
raise
|
|
3074
3168
|
return _resolve_internal_selector(selector, source_fields, location)
|
|
3075
3169
|
|
|
3076
3170
|
@staticmethod
|
|
@@ -3088,7 +3182,11 @@ class AppFormCompiler:
|
|
|
3088
3182
|
return title
|
|
3089
3183
|
|
|
3090
3184
|
@staticmethod
|
|
3091
|
-
def _data_relation_que_type(field: JSONObject) -> int:
|
|
3185
|
+
def _data_relation_que_type(field: JSONObject) -> int | None:
|
|
3186
|
+
if field.get("_app_form_unresolved"):
|
|
3187
|
+
# 透传形态(目标表单候选集解析不到):后端 fillMatchRulesList 对查
|
|
3188
|
+
# 不到的 queId 置空 queType 继续出参,写链不校验——同构置 None。
|
|
3189
|
+
return None
|
|
3092
3190
|
backend_type = _positive_int(field.get("queType"))
|
|
3093
3191
|
if backend_type is not None:
|
|
3094
3192
|
return backend_type
|
|
@@ -3142,7 +3240,12 @@ class AppFormCompiler:
|
|
|
3142
3240
|
ordered_ops = common | {"greater_than", "greater_or_equal", "less_than", "less_or_equal", "dynamic_range", "decimal_range"}
|
|
3143
3241
|
organization_ops = common | {"belongs_to", "not_belongs_to", "subordinate_to", "not_subordinate_to", "subordinate_included", "subordinate_not_included", "contains_any", "contains_all"}
|
|
3144
3242
|
field_type = _canonical_backend_field_type(source)
|
|
3145
|
-
if
|
|
3243
|
+
if source.get("_app_form_unresolved"):
|
|
3244
|
+
# 透传形态:源字段类型不可知(后端读链查不到置空),后端写链对
|
|
3245
|
+
# operator×源类型组合无校验——跳过类型白名单,仅保留 operand 形状
|
|
3246
|
+
# 校验(下方),与「读面保留、写面透传」的存量形态处理同口径。
|
|
3247
|
+
allowed = common | text_ops | ordered_ops | organization_ops
|
|
3248
|
+
elif field_type in {"number", "amount", "date", "datetime"}:
|
|
3146
3249
|
# date/datetime 补 belongs_to/not_belongs_to:后端 judgeType=13(JUDGE_BELONG_TO)
|
|
3147
3250
|
# 对时间字段的存量语义是「属于时间范围」(judgeValues 形如
|
|
3148
3251
|
# "2025-01-30 14:20:59~2026-01-30 14:20:59"),与组织字段的属于语义
|
|
@@ -3150,6 +3253,12 @@ class AppFormCompiler:
|
|
|
3150
3253
|
allowed = ordered_ops | ({"belongs_to", "not_belongs_to"} if field_type in {"date", "datetime"} else set())
|
|
3151
3254
|
elif field_type in {"member", "department"}:
|
|
3152
3255
|
allowed = organization_ops
|
|
3256
|
+
elif field_type in {"text", "multi_select"}:
|
|
3257
|
+
# text/multi_select 补 belongs_to/not_belongs_to:judgeType=13 同为多态
|
|
3258
|
+
# 语义(选项「属于」集合),后端写链对 operator×源类型无校验——存量
|
|
3259
|
+
# 实证 r8:bjqj2bge2g02(multi_select 资产类型 belongs_to 当前字段)、
|
|
3260
|
+
# e4f644fe(text 客户等级 belongs_to literal ["BC"])。
|
|
3261
|
+
allowed = text_ops | {"belongs_to", "not_belongs_to"}
|
|
3153
3262
|
else:
|
|
3154
3263
|
allowed = text_ops
|
|
3155
3264
|
if operator not in allowed:
|
|
@@ -3163,7 +3272,10 @@ class AppFormCompiler:
|
|
|
3163
3272
|
if operator in EMPTY_OPERATORS:
|
|
3164
3273
|
if operand.get("kind") != "literal" or values not in ([], None):
|
|
3165
3274
|
raise AppFormFailure("UNSUPPORTED_FIELD_CONFIG", "empty-value operators require an empty literal operand", "CONTRACT_PRECHECK")
|
|
3166
|
-
elif operand.get("kind") == "literal" and not values:
|
|
3275
|
+
elif operand.get("kind") == "literal" and not values and operator not in {"subordinate_to", "not_subordinate_to"}:
|
|
3276
|
+
# subordinate 两算子放行空 literal:存量「设置后清空」形态(r13 实证
|
|
3277
|
+
# 41ce0ad8)——后端写链仅校验 dynamic_range(checkRelationMatchRules),
|
|
3278
|
+
# formula 对空 judgeValueDetails 渲染 [],读回解析对称,往返保真
|
|
3167
3279
|
raise AppFormFailure("UNSUPPORTED_FIELD_CONFIG", "literal data_relation operands must contain at least one value", "CONTRACT_PRECHECK")
|
|
3168
3280
|
|
|
3169
3281
|
@staticmethod
|
|
@@ -3279,11 +3391,19 @@ class AppFormCompiler:
|
|
|
3279
3391
|
merged: list[JSONObject] = []
|
|
3280
3392
|
seen: set[int] = set()
|
|
3281
3393
|
for item in [*_backend_question_candidates(draft_form), *_backend_question_candidates(published_form)]:
|
|
3282
|
-
|
|
3283
|
-
if
|
|
3284
|
-
|
|
3394
|
+
raw_id = item.get("queId")
|
|
3395
|
+
if isinstance(raw_id, int) and not isinstance(raw_id, bool):
|
|
3396
|
+
# queId 是字段身份,身份语义覆盖 0 与负数:0 是系统「编号」虚拟行
|
|
3397
|
+
# (que_type=8,GET 草稿/发布两态各带一行)、负数是系统行(-1 数据来源
|
|
3398
|
+
# 等,补导后物理在表)。只按正整数去重会让 queId=0 的「编号」在
|
|
3399
|
+
# 并集里出现两次,by_name 引用恒定命中 2 个候选 → AMBIGUOUS_
|
|
3400
|
+
# FIELD_SELECTOR(r13 103 app data_relation/relation 深层配置写回
|
|
3401
|
+
# 全量失败根因)。同 queId 即同字段,草稿优先(与正整数身份同
|
|
3402
|
+
# 语义);同名不同 queId 的真实用户字段仍双候选保留,by_name 歧义
|
|
3403
|
+
# 照常显式拒绝(存量脏形态不消歧,禁止静默取第一个)。
|
|
3404
|
+
if raw_id in seen:
|
|
3285
3405
|
continue
|
|
3286
|
-
seen.add(
|
|
3406
|
+
seen.add(raw_id)
|
|
3287
3407
|
merged.append(item)
|
|
3288
3408
|
return merged, (draft_form or published_form)
|
|
3289
3409
|
|