@qingflow-tech/qingflow-cli 1.1.51 → 1.1.54
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 +4 -4
- package/npm/lib/runtime.mjs +29 -13
- package/package.json +1 -1
- package/pyproject.toml +8 -2
- package/skills/qingflow-cli/SKILL.md +93 -322
- package/skills/qingflow-cli/manifest.yaml +1 -1
- package/skills/qingflow-cli/reference/00-INDEX.md +4 -4
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md +3 -9
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +3 -137
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +6 -4
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_FIELD_DATA_TYPES.md +14 -14
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +20 -19
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +18 -14
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md +7 -12
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +2 -1
- package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +3 -2
- package/skills/qingflow-cli/reference/builder/30-schema-fields.md +9 -5
- package/skills/qingflow-cli/reference/builder/50-views.md +192 -191
- package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +2 -0
- package/skills/qingflow-cli/reference/builder/README.md +1 -1
- package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +6 -6
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +76 -95
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_EXPLORATION_REPORT.md +6 -4
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_FIELD_DATA_TYPES.md +13 -13
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +20 -19
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +18 -14
- package/skills/qingflow-cli/reference/data/README.md +32 -0
- package/skills/qingflow-cli/reference/examples/README.md +1 -1
- package/skills/qingflow-cli/reference/examples/views/views_apply_v1alpha1.example.json +14 -0
- package/skills/qingflow-cli/reference/examples/views/views_upsert_table_minimal.example.json +30 -36
- package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_SPECIALIZED_WORKFLOW.md +50 -0
- package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +2 -1
- package/skills/qingflow-cli/reference/record/analysis/README.md +21 -113
- package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +10 -10
- package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +9 -9
- package/skills/qingflow-cli/reference/record/analysis/business-context.md +1 -1
- package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +10 -10
- package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +52 -80
- package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +2 -2
- package/skills/qingflow-common/SKILL.md +7 -4
- package/src/qingflow_mcp/app_form_compat.py +3 -0
- package/src/qingflow_mcp/app_form_validator.py +69 -17
- package/src/qingflow_mcp/builder_facade/app_form.py +497 -100
- package/src/qingflow_mcp/builder_facade/models.py +227 -17
- package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +1 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +64 -30
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +6 -10
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +6 -10
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +11 -18
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +6 -10
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +6 -10
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +6 -10
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +46 -15
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/rich_text.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +61 -27
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/formula.json +43 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +7 -12
- package/src/qingflow_mcp/builder_facade/schemas/views/manifest.json +42 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/complete.json +1245 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/button-config.json +53 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/data-permission.json +60 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/display.json +73 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/operation-permission.json +48 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/query-condition.json +47 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/summary.json +319 -0
- package/src/qingflow_mcp/builder_facade/service.py +1709 -518
- package/src/qingflow_mcp/builder_facade/views_extract.py +595 -0
- package/src/qingflow_mcp/builder_facade/views_schema.py +400 -0
- package/src/qingflow_mcp/cli/commands/__init__.py +2 -1
- package/src/qingflow_mcp/cli/commands/builder.py +76 -55
- package/src/qingflow_mcp/cli/commands/data.py +134 -0
- package/src/qingflow_mcp/cli/commands/record.py +1 -1
- package/src/qingflow_mcp/cli/context.py +3 -0
- package/src/qingflow_mcp/cli/formatters.py +97 -1
- package/src/qingflow_mcp/public_surface.py +8 -0
- package/src/qingflow_mcp/response_trim.py +11 -2
- package/src/qingflow_mcp/server.py +2 -0
- package/src/qingflow_mcp/server_app_builder.py +34 -10
- package/src/qingflow_mcp/server_app_user.py +4 -0
- package/src/qingflow_mcp/solution/compiler/view_compiler.py +2 -1
- package/src/qingflow_mcp/solution/executor.py +96 -8
- package/src/qingflow_mcp/solution/spec_models.py +1 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +218 -398
- package/src/qingflow_mcp/tools/csv_export.py +168 -0
- package/src/qingflow_mcp/tools/data_tools.py +284 -0
- package/src/qingflow_mcp/tools/record_tools.py +11 -46
- package/src/qingflow_mcp/tools/solution_tools.py +2 -0
- package/src/qingflow_mcp/tools/view_tools.py +0 -6
- package/src/qingflow_mcp/tools/view_writer.py +44 -0
- package/skills/qingflow-cli/reference/examples/views/views_batch_full.example.json +0 -159
- package/skills/qingflow-cli/reference/views_upsert_table_minimal.example.json +0 -42
|
@@ -9,14 +9,14 @@ import pandas as pd
|
|
|
9
9
|
|
|
10
10
|
files = [
|
|
11
11
|
"/absolute/path/records-0001.csv",
|
|
12
|
-
#
|
|
12
|
+
# 包含 data query 返回的每个 files[].local_path
|
|
13
13
|
]
|
|
14
14
|
|
|
15
15
|
frames = [pd.read_csv(path, dtype=str, keep_default_na=False) for path in files]
|
|
16
16
|
df = pd.concat(frames, ignore_index=True) if frames else pd.DataFrame()
|
|
17
17
|
|
|
18
18
|
# Columns are readable and field-id anchored, e.g. 项目状态__field_343283094.
|
|
19
|
-
fields = [] #
|
|
19
|
+
fields = [] # 需要字段 id 元数据时使用 data query 返回的 fields
|
|
20
20
|
field_by_id = {int(item["field_id"]): item for item in fields if "field_id" in item}
|
|
21
21
|
title_by_col = {item["column_name"]: item["title"] for item in fields if item.get("column_name")}
|
|
22
22
|
```
|
|
@@ -19,7 +19,8 @@ Do not require both surfaces. If only one is installed, complete the workflow on
|
|
|
19
19
|
## Shared Concepts
|
|
20
20
|
|
|
21
21
|
- **AppForm** is the versioned declarative definition for an application form: app metadata, fields, sections, layout, settings, and field configuration. The same JSON declaration is used by CLI and Qingflow MCP.
|
|
22
|
-
- **
|
|
22
|
+
- **Data** 是既有应用中的普通业务数据。查询、单条读取和直接增删改默认使用视图无关的 data 入口,并以当次 schema、字段能力和访问范围为准。
|
|
23
|
+
- **Record** 用于详情页附件/图片、完整日志、导入、代码块,以及用户明确指定的 record 命令;不要把普通应用数据工作路由到 record。
|
|
23
24
|
- **Task** is a workflow work item. Prefer task operations for approval, transfer, rollback, and assignee actions.
|
|
24
25
|
- **Builder resource** is package, app, view, chart, portal, navigation, or workflow configuration. It is separate from record data.
|
|
25
26
|
- **Schema version** is part of the public contract. Pin it for one AppForm or workflow operation and do not combine values from different versions.
|
|
@@ -30,11 +31,13 @@ Do not require both surfaces. If only one is installed, complete the workflow on
|
|
|
30
31
|
| --- | --- | --- |
|
|
31
32
|
| Form schema/read/apply/delete | `qingflow --json builder app-form ...` / `builder app delete` | `app_form_schema`, `app_form_get`, `app_form_apply`, `app_delete` |
|
|
32
33
|
| AppForm static validation | `qingflow --json builder app-form validate --file ...` | Performed inside `app_form_apply`; no separate tool |
|
|
33
|
-
|
|
|
34
|
-
|
|
|
34
|
+
| 普通应用数据查询与直接增删改 | `qingflow --json data ...` | Qingflow MCP `data_query/get/insert/update/delete` |
|
|
35
|
+
| 日志、附件、导入、代码块或用户指定的 record 命令 | `qingflow --json record ...` / `import ...` | Qingflow MCP 对应 record/import 专项工具 |
|
|
36
|
+
| 待办与流程动作 | `qingflow --json task ...` | Qingflow MCP task tools |
|
|
37
|
+
| 免登录打开网页或指定业务页面 | `qingflow --json auth login-link [--redirect-path PATH]` | `get_login_link` |
|
|
35
38
|
| Package, view, portal, chart, workflow | `qingflow --json builder ...` | Qingflow MCP configuration tools |
|
|
36
39
|
|
|
37
|
-
The operation names and payload schemas are defined by the public surface. Use the current Schema or tool input schema as the source of truth, not
|
|
40
|
+
The operation names and payload schemas are defined by the public surface. Use the current Schema or tool input schema as the source of truth, not another command's shape or a backend payload.
|
|
38
41
|
|
|
39
42
|
## Package Boundaries
|
|
40
43
|
|
|
@@ -10,6 +10,9 @@ from .json_types import JSONObject
|
|
|
10
10
|
TYPE_ALIASES = {
|
|
11
11
|
"multiline": "long_text",
|
|
12
12
|
"textarea": "long_text",
|
|
13
|
+
"richtext": "rich_text",
|
|
14
|
+
"rich-text": "rich_text",
|
|
15
|
+
"rich_text": "rich_text",
|
|
13
16
|
"radio": "single_choice",
|
|
14
17
|
"boolean": "single_choice",
|
|
15
18
|
"select": "single_select",
|
|
@@ -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_]*")
|
|
@@ -218,19 +227,32 @@ def _static_errors(payload: dict[str, Any]) -> list[tuple[str, str]]:
|
|
|
218
227
|
widths = ["width" in field for field in fields if isinstance(field, dict)]
|
|
219
228
|
if widths and any(widths) and not all(widths):
|
|
220
229
|
errors.append((row_path, "width must be present on every field in the row or omitted from all"))
|
|
221
|
-
|
|
222
|
-
|
|
230
|
+
# 行宽契约(与后端语义对齐):任意 1-100 整数 + 全有或全无。
|
|
231
|
+
# 后端 html_width 无取值/总和校验,存量存在 33/34 三等分与字段删除后
|
|
232
|
+
# 留白(如 3×25=75)的行;get 读出的原布局必须能原样过校验
|
|
233
|
+
# (apply(get(S)) ≡ S 命题的必要条件)。
|
|
234
|
+
if widths and all(widths):
|
|
235
|
+
bad = [f.get("width") for f in fields if isinstance(f, dict) and not (isinstance(f.get("width"), int) and 1 <= f["width"] <= 100)]
|
|
236
|
+
if bad:
|
|
237
|
+
errors.append((row_path, "field widths must be an integer between 1 and 100"))
|
|
223
238
|
for field_index, field in enumerate(fields):
|
|
224
239
|
if isinstance(field, dict):
|
|
225
240
|
top_fields.append((f"{row_path}/{field_index}", field))
|
|
226
241
|
def resolve_local_selector(selector: Any) -> tuple[str, tuple[str, dict[str, Any]] | None]:
|
|
227
|
-
"""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
|
+
"""
|
|
228
248
|
if not isinstance(selector, dict):
|
|
229
249
|
return "schema", None
|
|
230
250
|
raw_que_id = selector.get("queId")
|
|
231
251
|
que_id = raw_que_id if isinstance(raw_que_id, int) and not isinstance(raw_que_id, bool) else None
|
|
232
252
|
raw_name = selector.get("name")
|
|
233
|
-
name
|
|
253
|
+
# name 两侧 strip:存量字段名带前导/尾随空格,queId 已同时给出不构成歧义
|
|
254
|
+
# (与 compiler 侧三个 resolve 函数同口径)。
|
|
255
|
+
name = raw_name.strip() if isinstance(raw_name, str) and raw_name.strip() else ""
|
|
234
256
|
if raw_que_id is not None and que_id is None:
|
|
235
257
|
return "schema", None
|
|
236
258
|
if raw_name is not None and not name:
|
|
@@ -239,26 +261,52 @@ def _static_errors(payload: dict[str, Any]) -> list[tuple[str, str]]:
|
|
|
239
261
|
matches = [(path, field) for path, field in top_fields if field.get("queId") == que_id]
|
|
240
262
|
if len(matches) != 1:
|
|
241
263
|
return "invalid", None
|
|
242
|
-
declared_name = str(matches[0][1].get("name") or "")
|
|
264
|
+
declared_name = str(matches[0][1].get("name") or "").strip()
|
|
243
265
|
if name and declared_name and name != declared_name:
|
|
244
266
|
return "invalid", None
|
|
245
267
|
return "resolved", matches[0]
|
|
246
268
|
if name:
|
|
247
|
-
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]
|
|
248
270
|
if len(matches) == 1:
|
|
249
271
|
return "resolved", matches[0]
|
|
250
272
|
if len(matches) > 1:
|
|
251
|
-
return "
|
|
273
|
+
return "ambiguous", None
|
|
252
274
|
if any(not str(field.get("name") or "") for _, field in top_fields):
|
|
253
275
|
return "unknown", None
|
|
254
276
|
return "invalid", None
|
|
255
277
|
return "schema", None
|
|
256
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
|
+
|
|
257
296
|
title_selector = spec.get("dataTitleField")
|
|
258
297
|
if top_fields:
|
|
259
298
|
if "dataTitleField" not in spec and "appKey" in payload:
|
|
260
299
|
pass
|
|
261
|
-
elif title_selector is None
|
|
300
|
+
elif title_selector is None:
|
|
301
|
+
# dataTitleField=null 仅在带 appKey 的更新/round-trip 场景放行:后端写链
|
|
302
|
+
# FormSvcImpl.normalizeDataTitleQueId 对 null 自动 fallback 到第一个可用
|
|
303
|
+
# 字段,get 读面在后端未设置时原样输出 null。创建场景(无 appKey)仍需
|
|
304
|
+
# 显式 selector。
|
|
305
|
+
if "appKey" not in payload:
|
|
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")))
|
|
309
|
+
elif resolve_local_selector(title_selector)[0] == "invalid":
|
|
262
310
|
errors.append(("/spec/dataTitleField", "dataTitleField must resolve to exactly one top-level field"))
|
|
263
311
|
elif title_selector is not None:
|
|
264
312
|
errors.append(("/spec/dataTitleField", "dataTitleField must be null when body is empty"))
|
|
@@ -266,11 +314,15 @@ def _static_errors(payload: dict[str, Any]) -> list[tuple[str, str]]:
|
|
|
266
314
|
cover_selector = spec.get("dataCoverField")
|
|
267
315
|
if cover_selector is not None:
|
|
268
316
|
cover_resolution, cover = resolve_local_selector(cover_selector)
|
|
269
|
-
if cover_resolution == "
|
|
317
|
+
if cover_resolution == "ambiguous":
|
|
318
|
+
errors.append(("/spec/dataCoverField", ambiguity_message(cover_selector, "dataCoverField")))
|
|
319
|
+
elif cover_resolution == "invalid":
|
|
270
320
|
errors.append(("/spec/dataCoverField", "dataCoverField must resolve to exactly one top-level field"))
|
|
271
321
|
elif cover_resolution == "resolved" and cover is not None and _field_type(cover[1]) != "attachment":
|
|
272
322
|
errors.append(("/spec/dataCoverField", "dataCoverField must select a top-level attachment field"))
|
|
273
|
-
|
|
323
|
+
# 方案 A(duplicate-field-name):同父级允许同名(后端以 queId 为身份,40013
|
|
324
|
+
# 实测 1624 组),不再报 duplicate name;歧义在 selector 解析处以
|
|
325
|
+
# AMBIGUOUS_FIELD_SELECTOR 显式拒绝。
|
|
274
326
|
fields_by_id = {
|
|
275
327
|
field.get("queId"): field
|
|
276
328
|
for _, field in top_fields
|
|
@@ -278,12 +330,6 @@ def _static_errors(payload: dict[str, Any]) -> list[tuple[str, str]]:
|
|
|
278
330
|
}
|
|
279
331
|
output_targets: dict[str, tuple[str, str]] = {}
|
|
280
332
|
for path, field in top_fields:
|
|
281
|
-
name = str(field.get("name") or "")
|
|
282
|
-
if name:
|
|
283
|
-
if name in names:
|
|
284
|
-
errors.append((f"{path}/name", f"duplicate top-level field name; first declared at {names[name]}"))
|
|
285
|
-
else:
|
|
286
|
-
names[name] = f"{path}/name"
|
|
287
333
|
field_type = _field_type(field)
|
|
288
334
|
if field_type == "data_relation":
|
|
289
335
|
config = field.get("config") if isinstance(field.get("config"), dict) else {}
|
|
@@ -329,6 +375,9 @@ def _static_errors(payload: dict[str, Any]) -> list[tuple[str, str]]:
|
|
|
329
375
|
selector = item.get("field") if isinstance(item.get("field"), dict) else {}
|
|
330
376
|
input_path = f"{path}/config/{binding_name}/inputs/{index}"
|
|
331
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
|
|
332
381
|
if resolution == "invalid":
|
|
333
382
|
errors.append((f"{input_path}/field", "input field must resolve to exactly one top-level field"))
|
|
334
383
|
continue
|
|
@@ -380,13 +429,16 @@ def _static_errors(payload: dict[str, Any]) -> list[tuple[str, str]]:
|
|
|
380
429
|
)
|
|
381
430
|
)
|
|
382
431
|
|
|
383
|
-
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)
|
|
384
433
|
for index, item in enumerate(binding.get("outputs") or []):
|
|
385
434
|
if not isinstance(item, dict):
|
|
386
435
|
continue
|
|
387
436
|
selector = item.get("targetField")
|
|
388
437
|
target_path = f"{path}/config/{binding_name}/outputs/{index}/targetField"
|
|
389
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
|
|
390
442
|
if resolution == "invalid":
|
|
391
443
|
errors.append((target_path, "targetField must resolve to exactly one top-level field"))
|
|
392
444
|
continue
|