@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.
Files changed (56) hide show
  1. package/README.md +3 -3
  2. package/package.json +1 -1
  3. package/pyproject.toml +1 -1
  4. package/skills/qingflow-cli/reference/builder/30-schema-fields.md +13 -0
  5. package/skills/qingflow-cli/reference/builder/70-portal.md +37 -16
  6. package/skills/qingflow-cli/reference/examples/portal/portal_apply_v1alpha1.example.json +100 -0
  7. package/skills/qingflow-cli/reference/examples/portal/portal_sections_all_types.example.json +6 -7
  8. package/src/qingflow_mcp/app_form_validator.py +20 -3
  9. package/src/qingflow_mcp/builder_facade/app_form.py +137 -17
  10. package/src/qingflow_mcp/builder_facade/models.py +164 -13
  11. package/src/qingflow_mcp/builder_facade/portals_schema.py +397 -0
  12. package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +14 -1
  13. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +49 -9
  14. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +27 -0
  15. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_content_classification.json +146 -0
  16. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_content_extraction.json +146 -0
  17. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_content_generation.json +146 -0
  18. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_document_parsing.json +146 -0
  19. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_image_generation.json +146 -0
  20. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_image_recognition.json +146 -0
  21. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_machine_translation.json +146 -0
  22. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_text_summary.json +146 -0
  23. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +2 -2
  24. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +27 -0
  25. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date_duration.json +146 -0
  26. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/embed_field.json +146 -0
  27. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/image_select.json +146 -0
  28. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/location.json +146 -0
  29. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +2 -2
  30. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ocr.json +146 -0
  31. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +2 -2
  32. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +27 -0
  33. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +29 -2
  34. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +20 -7
  35. package/src/qingflow_mcp/builder_facade/schemas/portals/manifest.json +59 -0
  36. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/complete.json +1397 -0
  37. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/carousel.json +193 -0
  38. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/chart.json +643 -0
  39. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/favourite.json +203 -0
  40. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/filter.json +268 -0
  41. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/grid.json +265 -0
  42. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/link.json +131 -0
  43. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/task.json +199 -0
  44. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/text.json +123 -0
  45. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/view.json +196 -0
  46. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/summary.json +800 -0
  47. package/src/qingflow_mcp/builder_facade/service.py +1104 -71
  48. package/src/qingflow_mcp/cli/commands/builder.py +114 -5
  49. package/src/qingflow_mcp/public_surface.py +2 -0
  50. package/src/qingflow_mcp/server_app_builder.py +32 -1
  51. package/src/qingflow_mcp/solution/compiler/icon_utils.py +73 -0
  52. package/src/qingflow_mcp/solution/executor.py +5 -2
  53. package/src/qingflow_mcp/tools/ai_builder_tools.py +110 -12
  54. package/src/qingflow_mcp/tools/portal_tools.py +4 -54
  55. package/src/qingflow_mcp/tools/portal_writer.py +59 -0
  56. 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.4
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.4 qingflow
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.4 qingflow-skills install --agent codex --scope user --copy
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.4",
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qingflow-mcp"
7
- version = "1.1.61b4"
7
+ version = "1.1.61b5"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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 -> portal apply with sections[].chart -> portal get readback -> publish state
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
- - Use `--dash-key` for updates and do not send `package_id`.
70
- - Use `patch_sections[]` for targeted section changes when possible.
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
- | **`--payload-file`** | 完整门户 JSON 对象;**标准字段写 `dash_name`**。新版 CLI 兼容 **`name -> dash_name`**、单页 **`pages[0].components -> sections`**,但 agent 默认不要依赖兼容别名。 |
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
- - 公开 `sections-file` 稳定支持:**`grid` / `chart` / `view` / `text` / `link`**;`filter` 仍按 **§3.4** 的 raw `filterConfig` 边界处理,不作为主路径。
202
- - **待办 `task` / 常用 `favorite`** 是前端/后端 raw 组件形状,不在当前公开 `source_type` 合约里。智能体不要在 `sections-file` 中写 `source_type: "task"`;只有工具后续显式支持 `task`,或维护者走 raw 门户写入链路时,才使用待办槽位。
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
- | **`filter`** | **6** | 筛选条 |
239
- | **`text`** | **5** | 富文本说明 |
240
- | **`link`** | **4** | 外链 |
241
-
242
- CLI 校验(`PortalSectionPatch`):**chart** 须 **`chart`** 或兼容旧字段 **`chart_ref`**;**view** 须 **`view_ref`**;**text** 须 **`text`**;**link** 须 **`url`**。**grid** / **filter** 无额外 ref,**`config` 形状由后端接受为准**。
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
+ }
@@ -17,14 +17,13 @@
17
17
  }
18
18
  },
19
19
  "config": {
20
- "filterConfig": [],
21
- "graphList": [
20
+ "graphs": [
22
21
  {
23
- "graphType": "CHART",
24
- "graphKey": "REPLACE_WITH_QINGBI_CHART_ID",
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
- _Q_LINKER_TEMPLATE_RE = re.compile(r"\{\{(?P<key>[^{}\s]+)\}\}")
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 operand.get("kind") == "literal" and not values:
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
- if selected_name and item.get("key") != selected_name:
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
- request_url = _required_text(
1248
- request_values["url"],
1249
- message="Q-Linker request URL is empty",
1250
- details={"queId": source_id, "path": "/config/qLinkerBinding/request/url"},
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
- if str(item.get("key") or "") != str(selected.get("name") or ""):
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 = "boolean" if field_type == "single_choice" else "text" if field_type in {"link", "data_relation", "rich_text"} else field_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
- target = _resolve_backend_selector(source.get("field") or {}, source_fields, "source.field")
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
- return _resolve_backend_selector(selector, source_fields, location)
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 field_type in {"number", "amount", "date", "datetime"}:
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
- que_id = _positive_int(item.get("queId"))
3283
- if que_id is not None:
3284
- if que_id in seen:
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(que_id)
3406
+ seen.add(raw_id)
3287
3407
  merged.append(item)
3288
3408
  return merged, (draft_form or published_form)
3289
3409