@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.
- package/README.md +3 -3
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-cli/SKILL.md +37 -0
- package/skills/qingflow-cli/reference/builder/50-views.md +19 -4
- package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +2 -0
- package/src/qingflow_mcp/app_form_validator.py +53 -14
- package/src/qingflow_mcp/builder_facade/app_form.py +152 -47
- package/src/qingflow_mcp/builder_facade/models.py +19 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +3 -3
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +1 -1
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +1 -1
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +1 -1
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +3 -3
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +3 -3
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +1 -1
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +1 -1
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +3 -3
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +1 -1
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +1 -1
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +1 -1
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +3 -3
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +1 -1
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +1 -1
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +1 -1
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +3 -3
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +3 -3
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/rich_text.json +1 -1
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +1 -1
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +1 -1
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +3 -3
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +1 -1
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/formula.json +35 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +2 -2
- package/src/qingflow_mcp/builder_facade/schemas/views/manifest.json +2 -3
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/complete.json +68 -16
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/button-config.json +6 -6
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/data-permission.json +1 -1
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/display.json +13 -11
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/operation-permission.json +3 -3
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/query-condition.json +1 -1
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/summary.json +51 -6
- package/src/qingflow_mcp/builder_facade/service.py +198 -83
- package/src/qingflow_mcp/builder_facade/views_extract.py +21 -1
- package/src/qingflow_mcp/cli/commands/builder.py +4 -2
- package/src/qingflow_mcp/server_app_builder.py +2 -2
- package/src/qingflow_mcp/solution/compiler/view_compiler.py +2 -1
- package/src/qingflow_mcp/solution/executor.py +3 -1
- package/src/qingflow_mcp/solution/spec_models.py +1 -0
- 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.
|
|
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.
|
|
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.
|
|
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
|
+
"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
|
@@ -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
|
-
-
|
|
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
|
|
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 "
|
|
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 == "
|
|
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
|
-
|
|
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
|