@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.
Files changed (85) hide show
  1. package/README.md +3 -3
  2. package/package.json +1 -1
  3. package/pyproject.toml +1 -2
  4. package/skills/qingflow-cli/SKILL.md +2 -104
  5. package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +2 -2
  6. package/skills/qingflow-cli/reference/builder/30-schema-fields.md +5 -22
  7. package/skills/qingflow-cli/reference/builder/70-portal.md +16 -37
  8. package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +3 -3
  9. package/skills/qingflow-cli/reference/examples/portal/portal_sections_all_types.example.json +7 -6
  10. package/src/qingflow_mcp/app_form_compat.py +0 -3
  11. package/src/qingflow_mcp/app_form_validator.py +20 -89
  12. package/src/qingflow_mcp/builder_facade/app_form.py +112 -629
  13. package/src/qingflow_mcp/builder_facade/models.py +13 -164
  14. package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +1 -15
  15. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +38 -112
  16. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +8 -31
  17. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +10 -6
  18. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +10 -33
  19. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +10 -6
  20. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +18 -11
  21. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +8 -4
  22. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +8 -4
  23. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +10 -6
  24. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +8 -4
  25. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +8 -4
  26. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +8 -4
  27. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +10 -6
  28. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +8 -4
  29. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +10 -6
  30. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +8 -4
  31. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +12 -8
  32. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +15 -73
  33. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +8 -4
  34. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +8 -4
  35. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +29 -90
  36. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +8 -4
  37. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/formula.json +0 -43
  38. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +19 -27
  39. package/src/qingflow_mcp/builder_facade/service.py +72 -1143
  40. package/src/qingflow_mcp/cli/commands/builder.py +5 -114
  41. package/src/qingflow_mcp/cli/main.py +1 -34
  42. package/src/qingflow_mcp/cloud_context.py +1 -16
  43. package/src/qingflow_mcp/public_surface.py +8 -69
  44. package/src/qingflow_mcp/response_trim.py +1 -2
  45. package/src/qingflow_mcp/server.py +6 -12
  46. package/src/qingflow_mcp/server_app_builder.py +6 -41
  47. package/src/qingflow_mcp/server_app_user.py +5 -9
  48. package/src/qingflow_mcp/server_cloud.py +7 -43
  49. package/src/qingflow_mcp/solution/compiler/icon_utils.py +0 -73
  50. package/src/qingflow_mcp/solution/executor.py +2 -5
  51. package/src/qingflow_mcp/tools/ai_builder_tools.py +12 -110
  52. package/src/qingflow_mcp/tools/portal_tools.py +54 -4
  53. package/src/qingflow_mcp/tools/solution_tools.py +0 -2
  54. package/skills/qingflow-cli/reference/examples/portal/portal_apply_v1alpha1.example.json +0 -100
  55. package/src/qingflow_mcp/builder_facade/portals_schema.py +0 -397
  56. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_content_classification.json +0 -146
  57. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_content_extraction.json +0 -146
  58. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_content_generation.json +0 -146
  59. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_document_parsing.json +0 -146
  60. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_image_generation.json +0 -146
  61. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_image_recognition.json +0 -146
  62. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_machine_translation.json +0 -146
  63. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ai_text_summary.json +0 -146
  64. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date_duration.json +0 -146
  65. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/embed_field.json +0 -146
  66. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/image_select.json +0 -146
  67. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/location.json +0 -146
  68. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/ocr.json +0 -146
  69. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/rich_text.json +0 -146
  70. package/src/qingflow_mcp/builder_facade/schemas/portals/manifest.json +0 -59
  71. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/complete.json +0 -1397
  72. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/carousel.json +0 -193
  73. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/chart.json +0 -643
  74. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/favourite.json +0 -203
  75. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/filter.json +0 -268
  76. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/grid.json +0 -265
  77. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/link.json +0 -131
  78. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/task.json +0 -199
  79. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/text.json +0 -123
  80. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/components/view.json +0 -196
  81. package/src/qingflow_mcp/builder_facade/schemas/portals/v1alpha1/summary.json +0 -800
  82. package/src/qingflow_mcp/tool_catalog.py +0 -387
  83. package/src/qingflow_mcp/tool_catalog_i18n.py +0 -185
  84. package/src/qingflow_mcp/tool_policy.py +0 -197
  85. 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
- # key 允许含空白(r10 实测存量 key 如「应用包 ID」带空格,\s 排除导致占位符
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
- # 行宽契约(与后端语义对齐):任意 1-100 整数 + 全有或全无。
237
- # 后端 html_width 无取值/总和校验,存量存在 33/34 三等分与字段删除后
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
- # name 两侧 strip:存量字段名带前导/尾随空格,queId 已同时给出不构成歧义
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 "").strip()
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 "").strip() == name]
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 "ambiguous", None
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 == "ambiguous":
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
- # 方案 A(duplicate-field-name):同父级允许同名(后端以 queId 为身份,40013
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
- # 两侧 strip:模板引用名提取时已 strip(字段名存量带尾随空格,如
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 = str(field.get("name") or "") or str(field.get("queId") or field_type)
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