@qingflow-tech/qingflow-cli 1.1.61-beta.5 → 1.1.61
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 -2
- package/skills/qingflow-cli/SKILL.md +2 -104
- package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +2 -2
- package/skills/qingflow-cli/reference/builder/30-schema-fields.md +5 -22
- package/skills/qingflow-cli/reference/builder/70-portal.md +16 -37
- package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +3 -3
- package/skills/qingflow-cli/reference/examples/portal/portal_sections_all_types.example.json +7 -6
- package/src/qingflow_mcp/app_form_compat.py +0 -3
- package/src/qingflow_mcp/app_form_validator.py +20 -89
- package/src/qingflow_mcp/builder_facade/app_form.py +112 -629
- package/src/qingflow_mcp/builder_facade/models.py +13 -164
- package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +1 -15
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +38 -112
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +8 -31
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +10 -6
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +10 -33
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +10 -6
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +18 -11
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +10 -6
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +10 -6
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +10 -6
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +12 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +15 -73
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +29 -90
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/formula.json +0 -43
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +19 -27
- package/src/qingflow_mcp/builder_facade/service.py +72 -1143
- package/src/qingflow_mcp/cli/commands/builder.py +5 -114
- package/src/qingflow_mcp/cli/main.py +1 -34
- package/src/qingflow_mcp/cloud_context.py +1 -16
- package/src/qingflow_mcp/public_surface.py +8 -69
- package/src/qingflow_mcp/response_trim.py +1 -2
- package/src/qingflow_mcp/server.py +6 -12
- package/src/qingflow_mcp/server_app_builder.py +6 -41
- package/src/qingflow_mcp/server_app_user.py +5 -9
- package/src/qingflow_mcp/server_cloud.py +7 -43
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +0 -73
- package/src/qingflow_mcp/solution/executor.py +2 -5
- package/src/qingflow_mcp/tools/ai_builder_tools.py +12 -110
- package/src/qingflow_mcp/tools/portal_tools.py +54 -4
- package/src/qingflow_mcp/tools/solution_tools.py +0 -2
- package/skills/qingflow-cli/reference/examples/portal/portal_apply_v1alpha1.example.json +0 -100
- package/src/qingflow_mcp/builder_facade/portals_schema.py +0 -397
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_content_classification.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_content_extraction.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_content_generation.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_document_parsing.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_image_generation.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_image_recognition.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_machine_translation.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_text_summary.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date_duration.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/embed_field.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/image_select.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/location.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ocr.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/rich_text.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/portals/manifest.json +0 -59
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/complete.json +0 -1397
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/carousel.json +0 -193
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/chart.json +0 -643
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/favourite.json +0 -203
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/filter.json +0 -268
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/grid.json +0 -265
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/link.json +0 -131
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/task.json +0 -199
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/text.json +0 -123
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/view.json +0 -196
- package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/summary.json +0 -800
- package/src/qingflow_mcp/tool_catalog.py +0 -387
- package/src/qingflow_mcp/tool_catalog_i18n.py +0 -185
- package/src/qingflow_mcp/tool_policy.py +0 -197
- package/src/qingflow_mcp/tools/portal_writer.py +0 -59
|
@@ -11,15 +11,9 @@ 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 必须写回,两闸门同口径)。
|
|
19
14
|
_INTEGRATION_OUTPUT_TARGET_FIELD_TYPES = {
|
|
20
15
|
"text",
|
|
21
16
|
"long_text",
|
|
22
|
-
"rich_text",
|
|
23
17
|
"number",
|
|
24
18
|
"amount",
|
|
25
19
|
"date",
|
|
@@ -27,17 +21,8 @@ _INTEGRATION_OUTPUT_TARGET_FIELD_TYPES = {
|
|
|
27
21
|
"single_choice",
|
|
28
22
|
"single_select",
|
|
29
23
|
"multi_select",
|
|
30
|
-
"attachment",
|
|
31
|
-
# r10 存量实证放行:link(92nl6blfk402)/member(ab9fjvj33001)——后端写链
|
|
32
|
-
# 无类型白名单,运行时同为字符串写入路径
|
|
33
|
-
"link",
|
|
34
|
-
"member",
|
|
35
|
-
"data_relation",
|
|
36
|
-
"subtable",
|
|
37
24
|
}
|
|
38
|
-
|
|
39
|
-
# 扫描漏检 → validate 误报「key must appear as {{key}}」)
|
|
40
|
-
_Q_LINKER_TEMPLATE_RE = re.compile(r"\{\{(?P<key>[^{}]+)\}\}")
|
|
25
|
+
_Q_LINKER_TEMPLATE_RE = re.compile(r"\{\{(?P<key>[^{}\s]+)\}\}")
|
|
41
26
|
_FORMULA_IDENTIFIER_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
|
42
27
|
#: 公式字段引用正则(#queId_<id>$,支持表格嵌套 <id>.<id>)
|
|
43
28
|
_FORMULA_FIELD_REF_RE = re.compile(r"#queId_([^$]+)\$")
|
|
@@ -233,32 +218,19 @@ def _static_errors(payload: dict[str, Any]) -> list[tuple[str, str]]:
|
|
|
233
218
|
widths = ["width" in field for field in fields if isinstance(field, dict)]
|
|
234
219
|
if widths and any(widths) and not all(widths):
|
|
235
220
|
errors.append((row_path, "width must be present on every field in the row or omitted from all"))
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
# 留白(如 3×25=75)的行;get 读出的原布局必须能原样过校验
|
|
239
|
-
# (apply(get(S)) ≡ S 命题的必要条件)。
|
|
240
|
-
if widths and all(widths):
|
|
241
|
-
bad = [f.get("width") for f in fields if isinstance(f, dict) and not (isinstance(f.get("width"), int) and 1 <= f["width"] <= 100)]
|
|
242
|
-
if bad:
|
|
243
|
-
errors.append((row_path, "field widths must be an integer between 1 and 100"))
|
|
221
|
+
if widths and all(widths) and sum(field.get("width", 0) for field in fields if isinstance(field, dict)) != 100:
|
|
222
|
+
errors.append((row_path, "field widths must total 100"))
|
|
244
223
|
for field_index, field in enumerate(fields):
|
|
245
224
|
if isinstance(field, dict):
|
|
246
225
|
top_fields.append((f"{row_path}/{field_index}", field))
|
|
247
226
|
def resolve_local_selector(selector: Any) -> tuple[str, tuple[str, dict[str, Any]] | None]:
|
|
248
|
-
"""Resolve against the declared body, preserving backend-dependent ID-only cases.
|
|
249
|
-
|
|
250
|
-
返回态:resolved(唯一定位)/ invalid(定位失败)/ ambiguous(方案 A:name-only
|
|
251
|
-
命中多个同名字段,调用方须报 AMBIGUOUS_FIELD_SELECTOR)/ unknown(存在未命名
|
|
252
|
-
字段,后端侧再判)/ schema(结构不完整,交给 Schema 校验)。
|
|
253
|
-
"""
|
|
227
|
+
"""Resolve against the declared body, preserving backend-dependent ID-only cases."""
|
|
254
228
|
if not isinstance(selector, dict):
|
|
255
229
|
return "schema", None
|
|
256
230
|
raw_que_id = selector.get("queId")
|
|
257
231
|
que_id = raw_que_id if isinstance(raw_que_id, int) and not isinstance(raw_que_id, bool) else None
|
|
258
232
|
raw_name = selector.get("name")
|
|
259
|
-
|
|
260
|
-
# (与 compiler 侧三个 resolve 函数同口径)。
|
|
261
|
-
name = raw_name.strip() if isinstance(raw_name, str) and raw_name.strip() else ""
|
|
233
|
+
name = raw_name if isinstance(raw_name, str) and raw_name else ""
|
|
262
234
|
if raw_que_id is not None and que_id is None:
|
|
263
235
|
return "schema", None
|
|
264
236
|
if raw_name is not None and not name:
|
|
@@ -267,52 +239,26 @@ def _static_errors(payload: dict[str, Any]) -> list[tuple[str, str]]:
|
|
|
267
239
|
matches = [(path, field) for path, field in top_fields if field.get("queId") == que_id]
|
|
268
240
|
if len(matches) != 1:
|
|
269
241
|
return "invalid", None
|
|
270
|
-
declared_name = str(matches[0][1].get("name") or "")
|
|
242
|
+
declared_name = str(matches[0][1].get("name") or "")
|
|
271
243
|
if name and declared_name and name != declared_name:
|
|
272
244
|
return "invalid", None
|
|
273
245
|
return "resolved", matches[0]
|
|
274
246
|
if name:
|
|
275
|
-
matches = [(path, field) for path, field in top_fields if str(field.get("name") or "")
|
|
247
|
+
matches = [(path, field) for path, field in top_fields if str(field.get("name") or "") == name]
|
|
276
248
|
if len(matches) == 1:
|
|
277
249
|
return "resolved", matches[0]
|
|
278
250
|
if len(matches) > 1:
|
|
279
|
-
return "
|
|
251
|
+
return "invalid", None
|
|
280
252
|
if any(not str(field.get("name") or "") for _, field in top_fields):
|
|
281
253
|
return "unknown", None
|
|
282
254
|
return "invalid", None
|
|
283
255
|
return "schema", None
|
|
284
256
|
|
|
285
|
-
def ambiguity_message(selector: Any, subject: str) -> str:
|
|
286
|
-
name = str((selector or {}).get("name") or "").strip()
|
|
287
|
-
candidate_ids = sorted(
|
|
288
|
-
{
|
|
289
|
-
field.get("queId")
|
|
290
|
-
for _, field in top_fields
|
|
291
|
-
if str(field.get("name") or "").strip() == name
|
|
292
|
-
and isinstance(field.get("queId"), int)
|
|
293
|
-
and not isinstance(field.get("queId"), bool)
|
|
294
|
-
}
|
|
295
|
-
)
|
|
296
|
-
return (
|
|
297
|
-
f"AMBIGUOUS_FIELD_SELECTOR: {subject} name {name!r} matches multiple fields "
|
|
298
|
-
f"(candidate queIds: {candidate_ids}); duplicate names are allowed but "
|
|
299
|
-
"selectors must use queId to disambiguate"
|
|
300
|
-
)
|
|
301
|
-
|
|
302
257
|
title_selector = spec.get("dataTitleField")
|
|
303
258
|
if top_fields:
|
|
304
259
|
if "dataTitleField" not in spec and "appKey" in payload:
|
|
305
260
|
pass
|
|
306
|
-
elif title_selector is None:
|
|
307
|
-
# dataTitleField=null 仅在带 appKey 的更新/round-trip 场景放行:后端写链
|
|
308
|
-
# FormSvcImpl.normalizeDataTitleQueId 对 null 自动 fallback 到第一个可用
|
|
309
|
-
# 字段,get 读面在后端未设置时原样输出 null。创建场景(无 appKey)仍需
|
|
310
|
-
# 显式 selector。
|
|
311
|
-
if "appKey" not in payload:
|
|
312
|
-
errors.append(("/spec/dataTitleField", "dataTitleField must resolve to exactly one top-level field"))
|
|
313
|
-
elif resolve_local_selector(title_selector)[0] == "ambiguous":
|
|
314
|
-
errors.append(("/spec/dataTitleField", ambiguity_message(title_selector, "dataTitleField")))
|
|
315
|
-
elif resolve_local_selector(title_selector)[0] == "invalid":
|
|
261
|
+
elif title_selector is None or resolve_local_selector(title_selector)[0] == "invalid":
|
|
316
262
|
errors.append(("/spec/dataTitleField", "dataTitleField must resolve to exactly one top-level field"))
|
|
317
263
|
elif title_selector is not None:
|
|
318
264
|
errors.append(("/spec/dataTitleField", "dataTitleField must be null when body is empty"))
|
|
@@ -320,15 +266,11 @@ def _static_errors(payload: dict[str, Any]) -> list[tuple[str, str]]:
|
|
|
320
266
|
cover_selector = spec.get("dataCoverField")
|
|
321
267
|
if cover_selector is not None:
|
|
322
268
|
cover_resolution, cover = resolve_local_selector(cover_selector)
|
|
323
|
-
if cover_resolution == "
|
|
324
|
-
errors.append(("/spec/dataCoverField", ambiguity_message(cover_selector, "dataCoverField")))
|
|
325
|
-
elif cover_resolution == "invalid":
|
|
269
|
+
if cover_resolution == "invalid":
|
|
326
270
|
errors.append(("/spec/dataCoverField", "dataCoverField must resolve to exactly one top-level field"))
|
|
327
271
|
elif cover_resolution == "resolved" and cover is not None and _field_type(cover[1]) != "attachment":
|
|
328
272
|
errors.append(("/spec/dataCoverField", "dataCoverField must select a top-level attachment field"))
|
|
329
|
-
|
|
330
|
-
# 实测 1624 组),不再报 duplicate name;歧义在 selector 解析处以
|
|
331
|
-
# AMBIGUOUS_FIELD_SELECTOR 显式拒绝。
|
|
273
|
+
names: dict[str, str] = {}
|
|
332
274
|
fields_by_id = {
|
|
333
275
|
field.get("queId"): field
|
|
334
276
|
for _, field in top_fields
|
|
@@ -336,6 +278,12 @@ def _static_errors(payload: dict[str, Any]) -> list[tuple[str, str]]:
|
|
|
336
278
|
}
|
|
337
279
|
output_targets: dict[str, tuple[str, str]] = {}
|
|
338
280
|
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"
|
|
339
287
|
field_type = _field_type(field)
|
|
340
288
|
if field_type == "data_relation":
|
|
341
289
|
config = field.get("config") if isinstance(field.get("config"), dict) else {}
|
|
@@ -359,15 +307,7 @@ def _static_errors(payload: dict[str, Any]) -> list[tuple[str, str]]:
|
|
|
359
307
|
"is_empty and is_not_empty require a literal operand with values: []",
|
|
360
308
|
)
|
|
361
309
|
)
|
|
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),读回解析对称,往返保真
|
|
310
|
+
elif operand.get("kind") == "literal" and not values:
|
|
371
311
|
errors.append(
|
|
372
312
|
(
|
|
373
313
|
f"{operand_path}/values",
|
|
@@ -389,9 +329,6 @@ def _static_errors(payload: dict[str, Any]) -> list[tuple[str, str]]:
|
|
|
389
329
|
selector = item.get("field") if isinstance(item.get("field"), dict) else {}
|
|
390
330
|
input_path = f"{path}/config/{binding_name}/inputs/{index}"
|
|
391
331
|
resolution, selected = resolve_local_selector(selector)
|
|
392
|
-
if resolution == "ambiguous":
|
|
393
|
-
errors.append((f"{input_path}/field", ambiguity_message(selector, f"{binding_name}.inputs[{index}].field")))
|
|
394
|
-
continue
|
|
395
332
|
if resolution == "invalid":
|
|
396
333
|
errors.append((f"{input_path}/field", "input field must resolve to exactly one top-level field"))
|
|
397
334
|
continue
|
|
@@ -400,10 +337,7 @@ def _static_errors(payload: dict[str, Any]) -> list[tuple[str, str]]:
|
|
|
400
337
|
selected_name = selected[1].get("name") if selected is not None else selector.get("name")
|
|
401
338
|
if not selected_name and selector.get("queId") in fields_by_id:
|
|
402
339
|
selected_name = fields_by_id[selector["queId"]].get("name")
|
|
403
|
-
|
|
404
|
-
# am4oh08v2401「答:Answer 」),严格相等会把「引用名≠当前字段名」的
|
|
405
|
-
# 存量形态误报为 key 不一致
|
|
406
|
-
if selected_name and str(item.get("key") or "").strip() != str(selected_name).strip():
|
|
340
|
+
if selected_name and item.get("key") != selected_name:
|
|
407
341
|
errors.append((f"{input_path}/key", "json_path key must equal the selected field name"))
|
|
408
342
|
|
|
409
343
|
if field_type == "q_linker":
|
|
@@ -446,16 +380,13 @@ def _static_errors(payload: dict[str, Any]) -> list[tuple[str, str]]:
|
|
|
446
380
|
)
|
|
447
381
|
)
|
|
448
382
|
|
|
449
|
-
producer =
|
|
383
|
+
producer = name or str(field.get("queId") or field_type)
|
|
450
384
|
for index, item in enumerate(binding.get("outputs") or []):
|
|
451
385
|
if not isinstance(item, dict):
|
|
452
386
|
continue
|
|
453
387
|
selector = item.get("targetField")
|
|
454
388
|
target_path = f"{path}/config/{binding_name}/outputs/{index}/targetField"
|
|
455
389
|
resolution, selected = resolve_local_selector(selector)
|
|
456
|
-
if resolution == "ambiguous":
|
|
457
|
-
errors.append((target_path, ambiguity_message(selector, f"{binding_name}.outputs[{index}].targetField")))
|
|
458
|
-
continue
|
|
459
390
|
if resolution == "invalid":
|
|
460
391
|
errors.append((target_path, "targetField must resolve to exactly one top-level field"))
|
|
461
392
|
continue
|