@qingflow-tech/qingflow-cli 1.1.50-beta.3 → 1.1.50-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 (50) 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/SKILL.md +37 -0
  5. package/skills/qingflow-cli/reference/builder/50-views.md +19 -4
  6. package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +2 -0
  7. package/src/qingflow_mcp/app_form_validator.py +53 -14
  8. package/src/qingflow_mcp/builder_facade/app_form.py +152 -47
  9. package/src/qingflow_mcp/builder_facade/models.py +19 -0
  10. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +3 -3
  11. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +1 -1
  12. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +1 -1
  13. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +1 -1
  14. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +3 -3
  15. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +3 -3
  16. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +1 -1
  17. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +1 -1
  18. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +3 -3
  19. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +1 -1
  20. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +1 -1
  21. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +1 -1
  22. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +3 -3
  23. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +1 -1
  24. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +1 -1
  25. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +1 -1
  26. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +3 -3
  27. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +3 -3
  28. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/rich_text.json +1 -1
  29. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +1 -1
  30. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +1 -1
  31. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +3 -3
  32. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +1 -1
  33. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/formula.json +35 -0
  34. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +2 -2
  35. package/src/qingflow_mcp/builder_facade/schemas/views/manifest.json +2 -3
  36. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/complete.json +68 -16
  37. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/button-config.json +6 -6
  38. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/data-permission.json +1 -1
  39. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/display.json +13 -11
  40. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/operation-permission.json +3 -3
  41. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/query-condition.json +1 -1
  42. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/summary.json +51 -6
  43. package/src/qingflow_mcp/builder_facade/service.py +198 -83
  44. package/src/qingflow_mcp/builder_facade/views_extract.py +21 -1
  45. package/src/qingflow_mcp/cli/commands/builder.py +4 -2
  46. package/src/qingflow_mcp/server_app_builder.py +2 -2
  47. package/src/qingflow_mcp/solution/compiler/view_compiler.py +2 -1
  48. package/src/qingflow_mcp/solution/executor.py +3 -1
  49. package/src/qingflow_mcp/solution/spec_models.py +1 -0
  50. package/src/qingflow_mcp/tools/ai_builder_tools.py +7 -5
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.50-beta.3
10
+ npm install @qingflow-tech/qingflow-cli@1.1.50-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.50-beta.3 qingflow
24
+ npx -y -p @qingflow-tech/qingflow-cli@1.1.50-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.50-beta.3 qingflow-skills install --agent codex --scope user --copy
65
+ npx -y -p @qingflow-tech/qingflow-cli@1.1.50-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.50-beta.3",
3
+ "version": "1.1.50-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.50b3"
7
+ version = "1.1.50b5"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -14,6 +14,43 @@ description: |
14
14
 
15
15
  本 Skill 负责 CLI 入口、文件约定和业务分流。已知 `app_key` 时直接执行 `data query/get`。只有应用名称时,先用 `qingflow --json app list --query "应用名称"` 查找当前工作区的可见应用,并核对返回项的 `app_name/title`;目标明确且唯一时取其 `app_key` 再查询,否则请用户确认。不要把应用名称直接传给 `--app-key`,也不要为定位名称转向 builder 或切换工作区。字段能力、权限范围与下一步动作以当前工具 schema、data 响应和 `hints` 为准。
16
16
 
17
+ ## 关联文件
18
+
19
+
20
+ | 文件 | 说明 |
21
+ | ------------------------------------------------------------------------------------------------ |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
22
+ | `manifest.yaml` | 技能清单 |
23
+ | [reference/00-INDEX.md](./reference/00-INDEX.md) | **总导航**:按用户目标选择 record / task / builder 文档;复杂任务先读 |
24
+ | [reference/QINGFLOW_CLI_MEMBER_CHEATSHEET.md](./reference/core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md) | **普通成员**主线:最短路径、`view_id`、与管理命令的差异预期 |
25
+ | [reference/QINGFLOW_CLI_FIELD_DATA_TYPES.md](./reference/core/QINGFLOW_CLI_FIELD_DATA_TYPES.md) | **字段数据类型**:`record schema …` 的 `fields[]`(`field_id`、`title`、`kind`、`options`…);`record list/get/access` 与写入 payload 的形态对照 |
26
+ | [reference/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md](./reference/core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md) | **应用记录获取数据**:`app get`(`accessible_views`)→ `record schema browse` → `record list(query)` 模糊定位 / `record get` 详情页首屏上下文 / `record logs` 全量日志;分析取数转 `record_access` |
27
+ | [reference/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md](./reference/task/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md) | **任务(待办已办)上下文 SOP**:`task list` → `task get` → `task log` → `task report` → `task action`;只把 `task_id` 作为主定位参数 |
28
+ | [reference/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md](./reference/record/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md) | **批量导入 SOP**:`app_get`(**`import_capability`**)→ `record_import_template_get` → `record_import_verify` →(可选)`record_import_repair_local` → `record_import_start` → `record_import_status_get`;CLI `import` / `record schema import` |
29
+ | [reference/builder/README.md](./reference/builder/README.md) | **Builder 总导航**:完整系统、单应用、字段、布局、视图、报表、门户、按钮/关联资源、流程、发布校验 |
30
+ | [reference/builder/10-build-single-app.md](./reference/builder/10-build-single-app.md) | **单应用搭建主入口**:一个应用端到端交付顺序 |
31
+ | [reference/builder/20-build-complete-system.md](./reference/builder/20-build-complete-system.md) | **完整系统/应用包主入口**:多应用、跨应用 relation、门户闭环 |
32
+ | [reference/builder/30-schema-fields.md](./reference/builder/30-schema-fields.md) | **字段 / schema**:字段类型、直觉别名、数据标题/封面、schema 创建与更新 |
33
+ | [reference/builder/40-layout.md](./reference/builder/40-layout.md) | **布局维护**:仅用于已有应用的分组、`rows` 矩阵、布局修复;新建应用布局写在 `form` 内 |
34
+ | [reference/builder/50-views.md](./reference/builder/50-views.md) | **视图**:表格/卡片/看板/甘特、固定筛选、查询面板、视图 patch |
35
+ | [reference/builder/60-charts.md](./reference/builder/60-charts.md) | **报表 / QingBI**:指标卡、图表、语义聚合、筛选、排序、更新 |
36
+ | [reference/builder/70-portal.md](./reference/builder/70-portal.md) | **门户**:标准工作台、业务入口、指标、图表、视图组件、发布 |
37
+ | [reference/builder/80-buttons-associated-resources.md](./reference/builder/80-buttons-associated-resources.md) | **关联资源 / 非主链路按钮维护**:关联视图/报表、`match_mappings`、上下文传参;按钮配置看视图文档 `action_buttons`;创建自定义按钮前先 `button get --button-text` 查重复用 |
38
+ | [reference/builder/90-workflow.md](./reference/builder/90-workflow.md) | **流程**:WorkflowSpec、`patch_nodes`、角色/成员、流程回读 |
39
+ | [reference/builder/99-publish-verify.md](./reference/builder/99-publish-verify.md) | **发布与回读**:`publish verify`、partial/readback 处理、最终汇报口径 |
40
+ | [reference/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md](./reference/record/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md) | **新建记录**:`record schema insert -> record insert --items-file`;成员/部门/关联优先自然语言输入;细节见 [record/insert](./reference/record/insert/README.md) |
41
+ | [reference/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md](./reference/record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md) | **更新记录**:主链路是 `record get` → 智能体按 `fields[]` 组 key/value → **`record update`**;`record schema update` 仅用于失败后诊断可写字段/路径 |
42
+ | [reference/QINGFLOW_CLI_RECORD_DELETE_WORKFLOW.md](./reference/record/QINGFLOW_CLI_RECORD_DELETE_WORKFLOW.md) | **删除记录**:`record list/get` 精确定位 → `record delete --app-key --record-id --view-id system:*`;批量用 `--record-ids-file` |
43
+ | [reference/record/analysis/README.md](./reference/record/analysis/README.md) | **数据分析**:最终统计结论、分析报告、趋势/排名/比例/分布统一走 `record access -> Python/pandas` |
44
+ | [reference/builder/reference/app-delivery-sop.md](./reference/builder/reference/app-delivery-sop.md) | **底层交付参考**:历史完整 SOP、权限、系统列和工具细节;不是主入口 |
45
+ | [reference/builder/reference/workspace-icons.md](./reference/builder/reference/workspace-icons.md) | **图标参考**:`builder icon catalog`;新建应用包、应用、门户必须显式选择非 `template` 的 `icon + color` |
46
+ | [reference/builder/reference/match-rules.md](./reference/builder/reference/match-rules.md) | **统一筛选 / 条件写法**:`filters`、`visible_when`、QingBI 图表筛选、工作流条件、`match_mappings`、`field_mappings` 的 `operator/value` 规则 |
47
+ | [reference/builder/workflow/README.md](./reference/builder/workflow/README.md) | **工作流搭建**:已有应用的流程建模、节点配置、`builder flow apply` 与 `patch_nodes` |
48
+ | [reference/builder/code-integrations/README.md](./reference/builder/code-integrations/README.md) | **代码块 / Q-Linker 配置**:字段别名、绑定、输入字段插入和配置排障 |
49
+ | [reference/QINGFLOW_CLI_ADMIN_CHEATSHEET.md](./reference/core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md) | **管理员**:检索、导入等与成员差异(主技能正文不展开) |
50
+ | [reference/QINGFLOW_CLI_EXPLORATION_REPORT.md](./reference/core/QINGFLOW_CLI_EXPLORATION_REPORT.md) | CLI 实测与 argparse/服务端对齐备忘 |
51
+ | [reference/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md](./reference/core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md) | **可选**:全角色「一次可复制」分项模板;默认执行技能**不必**打开 |
52
+ | [scripts/find-app-by-keyword.py](./scripts/find-app-by-keyword.py) | **旧版 CLI 兼容脚本**:当 `app list --query` 不可用时,用 `app list` 本地解析;默认优先使用 `qingflow --json app list --query <关键词>` |
53
+ | [scripts/builder-package-from-app-list.py](./scripts/builder-package-from-app-list.py) | **旧版 CLI 兼容脚本**:当 `builder package list` 不可用时,才从 `app list` 曲线反查 **`package_name` / `resolve-id`**;新版默认直接用 `qingflow --json builder package list --query <关键词>` |
17
54
  ## CLI 执行与 JSON 落盘
18
55
 
19
56
  1. 已知业务目标时直接执行目标命令,不固定前置 `auth whoami`、`workspace get` 或 `workspace select`。只有业务命令明确返回未登录、会话失效或工作区错误后,才恢复上下文并重试原命令。
@@ -1,6 +1,6 @@
1
1
  # Builder Views
2
2
 
3
- Read this when the task is about table/card/board/gantt/hierarchy views, fixed filters, query panels, view sorting, view data permissions, associated report/view display on a view, or repairing existing views.
3
+ Read this when the task is about table/card/board/gantt/hierarchy/calendar views, fixed filters, query panels, view sorting, view data permissions, associated report/view display on a view, or repairing existing views.
4
4
 
5
5
  ## Scope
6
6
 
@@ -18,7 +18,7 @@ app get -> app get fields/views -> views schema -> prepare one view -> views val
18
18
 
19
19
  视图声明是**版本化契约**(`apiVersion: builder.qingflow.com/v1alpha1`)。`builder views apply` 是视图创建的**唯一入口**(旧命令式 `view_create` 已删除;不要再寻找或使用任何“直接创建视图”的命令/工具)。
20
20
 
21
- - **两级 schema**:**summary**(最简版:类型/名称/数据展示内容 + `data_permission` 数据权限形状)与 **complete**(完整版:visibility 可见性授权、`filters`、`sort`、`query_conditions`、`action_buttons`/`buttons`、`associated_resources`、`card`/`gantt`/`hierarchy` 类型专属配置、`display`/`record`/`share`)。字段级定义逐字段带 `x-backend` 后端映射。
21
+ - **两级 schema**:**summary**(最简版:类型/名称/数据展示内容 + `data_permission` 数据权限形状)与 **complete**(完整版:visibility 可见性授权、`filters`、`sort`、`query_conditions`、`action_buttons`/`buttons`、`associated_resources`、`card`/`gantt`/`hierarchy`/`calendar` 类型专属配置、`display`/`record`/`share`)。字段级定义逐字段带 `x-backend` 后端映射。
22
22
  - **读 schema**:`qingflow --json builder views schema` 默认返回 `v1alpha1/summary.json`;需要完整可写配置时显式传 `--schema-kind complete`。MCP 对应 `app_views_schema`。
23
23
  - **Schema-first 写入**:先用 `builder views schema --schema-version VERSION --schema-kind complete` 读取完整 schema;将相同版本显式传给 `validate --schema-version VERSION` 和 `apply --schema-version VERSION`。`validate` 也可从声明信封读取 `schema_version`,但推荐显式传版本;apply 的版本校验必须用 `--schema-version`。`validate` 接受一个直接视图声明对象,不接受 `upsert_view` 包装或批量 envelope。
24
24
  - **说明字段**:视图 Schema 使用语义化的 `*Description` 自定义 key(如 `dataPermissionDescription`)承载说明,加载和引用展开时原样保留;读取说明的工具应识别这些 key。标准校验器忽略这些自定义字段,仍按 `type`、`enum`、`required` 等关键字校验;`default` 是标准注解,不会自动填值。`*Description` 不是提交视图声明时的业务字段。
@@ -26,7 +26,8 @@ app get -> app get fields/views -> views schema -> prepare one view -> views val
26
26
  - **读 canonical 声明**:`qingflow --json builder views get --app-key APP --schema-version VERSION --schema-kind summary`,返回指定版本的 summary 声明数组(字段名反解);完整可写配置用 `--schema-kind complete` 并检查 `config_read_errors[].gaps`。
27
27
  - **完整声明的提取边界**:`views get --schema-kind complete` 优先读取 `buttonConfigVO` 中的完整按钮绑定。公式过滤、无法解析的过滤规则或按钮 ID 会使该视图返回 `VIEW_NOT_EXTRACTABLE`(见 `details.config_read_errors[].gaps`),不会生成删减后的可写声明。修改这类视图的其他属性应使用局部 patch。更新现有视图时,已配置的系统字段原样保留,不补默认值、不重算序号;读接口生成的未配置占位对象不会写回,不会因此新增系统字段配置。
28
28
  - **版本协商**:apply 显式传 `--schema-version v1alpha1` 时启用版本化校验:非法声明返回 `INVALID_SPEC` 且**不写**;响应信封回显 `schemaVersion`。apply 不传时行为与旧版一致(别名宽容,零破坏)。
29
- - **complete 档新能力速览**:`data_permission`(`{type: config|custom, scope: all|oneself|dept|subordinate|custom}`;`scope=custom` 的规则明细写 `filters`);`sort`(`[{que_id, ascending}]`,空数组=恢复默认排序);`hierarchy` 类型(须 `hierarchy.title_que_id/children_que_id`,可选 `root_filter`);`card`/`gantt`/`display`/`record`/`share` 配置组。summary 档仅四公开类型(table/card/board/gantt)。
29
+ - **公开视图类型**:summary 与 complete 均支持 `table`、`card`、`board`、`gantt`、`hierarchy`、`calendar`。层级视图须提供 `hierarchy: {title_que_id, children_que_id}`;日历视图须提供 `calendar: {title_que_id, date_que_id}`,可选 `color_que_id`。日历 `date_que_id` 可引用表单日期字段,也可用系统字段 `2`(申请时间)或 `3`(更新时间)。
30
+ - **complete 档配置速览**:`data_permission`(`{type: config|custom, scope: all|oneself|dept|subordinate|custom}`;`scope=custom` 的规则明细写 `filters`);`sort`(`[{que_id, ascending}]`,空数组=恢复默认排序);`hierarchy`(可选 `root_filter`);`card`/`gantt`/`calendar`/`display`/`record`/`share` 配置组。
30
31
 
31
32
  ## Update existing views
32
33
 
@@ -132,7 +133,7 @@ CLI 将参数与文件内容拼装为服务层载荷,**顶层键**如下:
132
133
 
133
134
  ## 5. `type` 与各类型必填
134
135
 
135
- 契约 **`view.type`**:**`table`** | **`card`** | **`board`** | **`gantt`**。
136
+ 契约 **`view.type`**:**`table`** | **`card`** | **`board`** | **`gantt`** | **`hierarchy`** | **`calendar`**。
136
137
 
137
138
  | `type` | 列/字段要点 | 类型专属键 |
138
139
  |--------|----------------|------------|
@@ -140,6 +141,8 @@ CLI 将参数与文件内容拼装为服务层载荷,**顶层键**如下:
140
141
  | **`card`** | 同 table | — |
141
142
  | **`board`**(看板) | 列;且须 **`group_by`**(分组字段名) | **`group_by`** |
142
143
  | **`gantt`** | 列;须 **`start_field`**、**`end_field`**(通常为日期类字段名) | 可选 **`title_field`** |
144
+ | **`hierarchy`** | 层级标题字段和父子关系字段 | `hierarchy.title_que_id`、`hierarchy.children_que_id`;可选 `hierarchy.root_filter` |
145
+ | **`calendar`** | 标题字段和日期字段 | `calendar.title_que_id`、`calendar.date_que_id`;可选 `calendar.color_que_id` |
143
146
 
144
147
  实现侧校验:对 **table/card**,`columns` 至少包含一列;已知系统字段在读取已有视图时原样透传,不要求出现在表单业务字段索引中。未知系统字段仍会报告覆盖缺口。
145
148
 
@@ -374,6 +377,16 @@ CLI 将参数与文件内容拼装为服务层载荷,**顶层键**如下:
374
377
  - `card`:必须配置 `detail`,并在卡片行操作明确时配置 `list`;核心操作视图不要只给 `header`。
375
378
  - `gantt`:不适合作为唯一核心操作视图;需要快捷按钮时同时提供配套 table/board 视图,在该操作视图中配置 `list` 和 `detail`。
376
379
 
380
+ ### 6.4.1 绑定已有自定义按钮(`buttons`,查重优先)
381
+
382
+ 视图声明里的 `buttons[]` 只做绑定(引用应用级自定义按钮的 `button_id`),不会创建按钮本体。绑定前必须查重,不要重复创建同名按钮:
383
+
384
+ 1. **查询**:`qingflow --json builder button get --app-key APP --button-text 按钮文案`(MCP `app_get_buttons` 传 `button_text`),按文案精确匹配。
385
+ 2. **有则复用**:唯一命中 → 取返回的 `button_id` 直接写进 `buttons[]`;同名多个 → 用 `button_id` 精确指定(避免歧义)。
386
+ 3. **没有再创建**:`builder button apply` 创建(其自身也按文案去重),拿到 `button_id` 后回到视图声明绑定。
387
+
388
+ 按钮本体维护(样式/跨视图复用/删除本体)见 [80-buttons-associated-resources.md](./80-buttons-associated-resources.md)。
389
+
377
390
  ### 6.5 关联视图/报表
378
391
 
379
392
  新版默认不要通过 `app_views_apply.associated_resources` 配置关联资源。应用级关联资源池和视图展示开关统一走 **`app_associated_resources_apply`** / CLI **`builder associated-resource apply`**。
@@ -507,6 +520,8 @@ qingflow builder views apply \
507
520
  | 现象 | 处理 |
508
521
  |------|------|
509
522
  | 列校验失败 / 无有效业务列 | **`app get fields`** 核对 **`name`**;避免 `columns` 仅含 **§7.1 系统列** |
523
+ | 响应含 **`STALE_FIELD_REFERENCE`** / **`STALE_OPTION_REFERENCE`** 警告 | 声明引用了表单中已删除的字段/选项(后端保存视图本就原样透传);apply 已照写并在 `warnings` 留痕,重放/迁移场景属预期,人工整理时再清理死引用 |
524
+ | 系统字段作标题/时间轴 | `title_que_id`/甘特 `start_que_id` 等可直接用系统字段 id(如 2=提交时间),与 `columns` 系统列同规则 |
510
525
  | 重名视图 / 删错 | **`app get views`**;更新时带 **`view_key`** |
511
526
  | 过滤器未生效疑虑 | 看响应 **`verification.view_filters_verified`**;必要时再 **`app get views`** |
512
527
  | 查询条件未生效疑虑 | 看响应 **`verification.view_query_conditions_verified`**;不要把 `query_conditions` 当 `filters` |
@@ -23,6 +23,7 @@ app get -> app get views/charts/buttons/associated resources -> apply button or
23
23
 
24
24
  ## Custom buttons
25
25
 
26
+ - **查重优先(硬规则)**:创建任何自定义按钮前,先 `builder button get --app-key APP --button-text 文案`(MCP `app_get_buttons` 传 `button_text`)按文案精确查重。唯一命中 → 直接复用其 `button_id`,不要创建;未命中 → 再走 `builder button apply` 创建(apply 本身也按 `button_text` 去重:无 `button_id` 时唯一同名匹配会转为更新,双保险);同名多个 → 必须传 `button_id` 精确指定。禁止每次都新建同名按钮。
26
27
  - For normal view-level business actions, prefer `builder views apply` with `action_buttons`.
27
28
  - Use this standalone button tool for advanced cases: deleting button bodies, reusing the same button across many views, bulk reordering bindings, changing style independently, or configuring qRobot/wings when the user provides exact config.
28
29
  - Use `upsert_buttons` for create/update and `patch_buttons` for small changes.
@@ -44,6 +45,7 @@ Start from user intent:
44
45
  | Need | Preferred path |
45
46
  | --- | --- |
46
47
  | Create or patch a normal business button on one view | `builder views apply` with `action_buttons` |
48
+ | 查询/查重已有自定义按钮 | `builder button get --app-key APP --button-text 文案`(唯一命中复用其 `button_id`) |
47
49
  | Set style, icon, cross-view reuse, remove button body, reorder bindings | `builder button apply` |
48
50
  | Add qRobot/wings behavior | `builder button apply`, only with user-provided exact config |
49
51
  | Clear a view's custom button bindings | `builder button apply` with `view_configs[].mode="replace"` and `buttons: []` |
@@ -11,9 +11,15 @@ from .app_form_compat import normalize_app_form_declaration
11
11
  from .builder_facade.app_form_schema import AppFormContractError, compact_schema_errors, schema_error_message
12
12
 
13
13
 
14
+ #: code_block / q_linker 输出目标字段类型。后端写链无类型白名单(FormSvcImpl
15
+ #: 原样落库),运行时 DataSvcImpl.getAnswerValueByString:选项/数字/日期有专门
16
+ #: 分支;attachment 按拆分 URL 写入(专门分支);subtable 整表组装(专门分支);
17
+ #: rich_text/data_relation 走 Question.canImport 兜底按字符串写入(不报错)。
18
+ #: 四者为存量合法配置(提取读得出、apply 必须写回,两闸门同口径)。
14
19
  _INTEGRATION_OUTPUT_TARGET_FIELD_TYPES = {
15
20
  "text",
16
21
  "long_text",
22
+ "rich_text",
17
23
  "number",
18
24
  "amount",
19
25
  "date",
@@ -21,6 +27,9 @@ _INTEGRATION_OUTPUT_TARGET_FIELD_TYPES = {
21
27
  "single_choice",
22
28
  "single_select",
23
29
  "multi_select",
30
+ "attachment",
31
+ "data_relation",
32
+ "subtable",
24
33
  }
25
34
  _Q_LINKER_TEMPLATE_RE = re.compile(r"\{\{(?P<key>[^{}\s]+)\}\}")
26
35
  _FORMULA_IDENTIFIER_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
@@ -230,13 +239,20 @@ def _static_errors(payload: dict[str, Any]) -> list[tuple[str, str]]:
230
239
  if isinstance(field, dict):
231
240
  top_fields.append((f"{row_path}/{field_index}", field))
232
241
  def resolve_local_selector(selector: Any) -> tuple[str, tuple[str, dict[str, Any]] | None]:
233
- """Resolve against the declared body, preserving backend-dependent ID-only cases."""
242
+ """Resolve against the declared body, preserving backend-dependent ID-only cases.
243
+
244
+ 返回态:resolved(唯一定位)/ invalid(定位失败)/ ambiguous(方案 A:name-only
245
+ 命中多个同名字段,调用方须报 AMBIGUOUS_FIELD_SELECTOR)/ unknown(存在未命名
246
+ 字段,后端侧再判)/ schema(结构不完整,交给 Schema 校验)。
247
+ """
234
248
  if not isinstance(selector, dict):
235
249
  return "schema", None
236
250
  raw_que_id = selector.get("queId")
237
251
  que_id = raw_que_id if isinstance(raw_que_id, int) and not isinstance(raw_que_id, bool) else None
238
252
  raw_name = selector.get("name")
239
- name = raw_name if isinstance(raw_name, str) and raw_name else ""
253
+ # name 两侧 strip:存量字段名带前导/尾随空格,queId 已同时给出不构成歧义
254
+ # (与 compiler 侧三个 resolve 函数同口径)。
255
+ name = raw_name.strip() if isinstance(raw_name, str) and raw_name.strip() else ""
240
256
  if raw_que_id is not None and que_id is None:
241
257
  return "schema", None
242
258
  if raw_name is not None and not name:
@@ -245,21 +261,38 @@ def _static_errors(payload: dict[str, Any]) -> list[tuple[str, str]]:
245
261
  matches = [(path, field) for path, field in top_fields if field.get("queId") == que_id]
246
262
  if len(matches) != 1:
247
263
  return "invalid", None
248
- declared_name = str(matches[0][1].get("name") or "")
264
+ declared_name = str(matches[0][1].get("name") or "").strip()
249
265
  if name and declared_name and name != declared_name:
250
266
  return "invalid", None
251
267
  return "resolved", matches[0]
252
268
  if name:
253
- matches = [(path, field) for path, field in top_fields if str(field.get("name") or "") == name]
269
+ matches = [(path, field) for path, field in top_fields if str(field.get("name") or "").strip() == name]
254
270
  if len(matches) == 1:
255
271
  return "resolved", matches[0]
256
272
  if len(matches) > 1:
257
- return "invalid", None
273
+ return "ambiguous", None
258
274
  if any(not str(field.get("name") or "") for _, field in top_fields):
259
275
  return "unknown", None
260
276
  return "invalid", None
261
277
  return "schema", None
262
278
 
279
+ def ambiguity_message(selector: Any, subject: str) -> str:
280
+ name = str((selector or {}).get("name") or "").strip()
281
+ candidate_ids = sorted(
282
+ {
283
+ field.get("queId")
284
+ for _, field in top_fields
285
+ if str(field.get("name") or "").strip() == name
286
+ and isinstance(field.get("queId"), int)
287
+ and not isinstance(field.get("queId"), bool)
288
+ }
289
+ )
290
+ return (
291
+ f"AMBIGUOUS_FIELD_SELECTOR: {subject} name {name!r} matches multiple fields "
292
+ f"(candidate queIds: {candidate_ids}); duplicate names are allowed but "
293
+ "selectors must use queId to disambiguate"
294
+ )
295
+
263
296
  title_selector = spec.get("dataTitleField")
264
297
  if top_fields:
265
298
  if "dataTitleField" not in spec and "appKey" in payload:
@@ -271,6 +304,8 @@ def _static_errors(payload: dict[str, Any]) -> list[tuple[str, str]]:
271
304
  # 显式 selector。
272
305
  if "appKey" not in payload:
273
306
  errors.append(("/spec/dataTitleField", "dataTitleField must resolve to exactly one top-level field"))
307
+ elif resolve_local_selector(title_selector)[0] == "ambiguous":
308
+ errors.append(("/spec/dataTitleField", ambiguity_message(title_selector, "dataTitleField")))
274
309
  elif resolve_local_selector(title_selector)[0] == "invalid":
275
310
  errors.append(("/spec/dataTitleField", "dataTitleField must resolve to exactly one top-level field"))
276
311
  elif title_selector is not None:
@@ -279,11 +314,15 @@ def _static_errors(payload: dict[str, Any]) -> list[tuple[str, str]]:
279
314
  cover_selector = spec.get("dataCoverField")
280
315
  if cover_selector is not None:
281
316
  cover_resolution, cover = resolve_local_selector(cover_selector)
282
- if cover_resolution == "invalid":
317
+ if cover_resolution == "ambiguous":
318
+ errors.append(("/spec/dataCoverField", ambiguity_message(cover_selector, "dataCoverField")))
319
+ elif cover_resolution == "invalid":
283
320
  errors.append(("/spec/dataCoverField", "dataCoverField must resolve to exactly one top-level field"))
284
321
  elif cover_resolution == "resolved" and cover is not None and _field_type(cover[1]) != "attachment":
285
322
  errors.append(("/spec/dataCoverField", "dataCoverField must select a top-level attachment field"))
286
- names: dict[str, str] = {}
323
+ # 方案 A(duplicate-field-name):同父级允许同名(后端以 queId 为身份,40013
324
+ # 实测 1624 组),不再报 duplicate name;歧义在 selector 解析处以
325
+ # AMBIGUOUS_FIELD_SELECTOR 显式拒绝。
287
326
  fields_by_id = {
288
327
  field.get("queId"): field
289
328
  for _, field in top_fields
@@ -291,12 +330,6 @@ def _static_errors(payload: dict[str, Any]) -> list[tuple[str, str]]:
291
330
  }
292
331
  output_targets: dict[str, tuple[str, str]] = {}
293
332
  for path, field in top_fields:
294
- name = str(field.get("name") or "")
295
- if name:
296
- if name in names:
297
- errors.append((f"{path}/name", f"duplicate top-level field name; first declared at {names[name]}"))
298
- else:
299
- names[name] = f"{path}/name"
300
333
  field_type = _field_type(field)
301
334
  if field_type == "data_relation":
302
335
  config = field.get("config") if isinstance(field.get("config"), dict) else {}
@@ -342,6 +375,9 @@ def _static_errors(payload: dict[str, Any]) -> list[tuple[str, str]]:
342
375
  selector = item.get("field") if isinstance(item.get("field"), dict) else {}
343
376
  input_path = f"{path}/config/{binding_name}/inputs/{index}"
344
377
  resolution, selected = resolve_local_selector(selector)
378
+ if resolution == "ambiguous":
379
+ errors.append((f"{input_path}/field", ambiguity_message(selector, f"{binding_name}.inputs[{index}].field")))
380
+ continue
345
381
  if resolution == "invalid":
346
382
  errors.append((f"{input_path}/field", "input field must resolve to exactly one top-level field"))
347
383
  continue
@@ -393,13 +429,16 @@ def _static_errors(payload: dict[str, Any]) -> list[tuple[str, str]]:
393
429
  )
394
430
  )
395
431
 
396
- producer = name or str(field.get("queId") or field_type)
432
+ producer = str(field.get("name") or "") or str(field.get("queId") or field_type)
397
433
  for index, item in enumerate(binding.get("outputs") or []):
398
434
  if not isinstance(item, dict):
399
435
  continue
400
436
  selector = item.get("targetField")
401
437
  target_path = f"{path}/config/{binding_name}/outputs/{index}/targetField"
402
438
  resolution, selected = resolve_local_selector(selector)
439
+ if resolution == "ambiguous":
440
+ errors.append((target_path, ambiguity_message(selector, f"{binding_name}.outputs[{index}].targetField")))
441
+ continue
403
442
  if resolution == "invalid":
404
443
  errors.append((target_path, "targetField must resolve to exactly one top-level field"))
405
444
  continue