@qingflow-tech/qingflow-cli 1.1.60 → 1.1.61-beta.2

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 (62) hide show
  1. package/README.md +3 -3
  2. package/package.json +1 -1
  3. package/pyproject.toml +3 -1
  4. package/skills/qingflow-cli/SKILL.md +120 -8
  5. package/skills/qingflow-cli/reference/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +1 -1
  6. package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +1 -1
  7. package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +2 -2
  8. package/skills/qingflow-cli/reference/builder/30-schema-fields.md +9 -5
  9. package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +3 -3
  10. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +4 -4
  11. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_EXPLORATION_REPORT.md +1 -1
  12. package/skills/qingflow-cli/reference/data/README.md +20 -0
  13. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_SPECIALIZED_WORKFLOW.md +7 -1
  14. package/skills/qingflow-common/SKILL.md +5 -2
  15. package/src/qingflow_mcp/app_form_compat.py +3 -0
  16. package/src/qingflow_mcp/app_form_validator.py +69 -17
  17. package/src/qingflow_mcp/backend_client.py +33 -8
  18. package/src/qingflow_mcp/builder_facade/app_form.py +497 -100
  19. package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +1 -0
  20. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +64 -30
  21. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +4 -8
  22. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +4 -8
  23. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +6 -10
  24. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +6 -10
  25. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +11 -18
  26. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +4 -8
  27. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +4 -8
  28. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +6 -10
  29. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +4 -8
  30. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +4 -8
  31. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +4 -8
  32. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +6 -10
  33. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +4 -8
  34. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +4 -8
  35. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +4 -8
  36. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +6 -10
  37. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +46 -15
  38. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/rich_text.json +146 -0
  39. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +4 -8
  40. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +4 -8
  41. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +61 -27
  42. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +4 -8
  43. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/formula.json +43 -0
  44. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +7 -12
  45. package/src/qingflow_mcp/builder_facade/service.py +39 -1
  46. package/src/qingflow_mcp/cli/commands/data.py +13 -3
  47. package/src/qingflow_mcp/cli/formatters.py +7 -0
  48. package/src/qingflow_mcp/cli/main.py +34 -1
  49. package/src/qingflow_mcp/cloud_context.py +16 -1
  50. package/src/qingflow_mcp/public_surface.py +67 -8
  51. package/src/qingflow_mcp/response_trim.py +2 -1
  52. package/src/qingflow_mcp/server.py +13 -7
  53. package/src/qingflow_mcp/server_app_builder.py +9 -5
  54. package/src/qingflow_mcp/server_app_user.py +10 -6
  55. package/src/qingflow_mcp/server_cloud.py +43 -7
  56. package/src/qingflow_mcp/tool_catalog.py +387 -0
  57. package/src/qingflow_mcp/tool_catalog_i18n.py +185 -0
  58. package/src/qingflow_mcp/tool_policy.py +197 -0
  59. package/src/qingflow_mcp/tools/data_files.py +174 -0
  60. package/src/qingflow_mcp/tools/data_tools.py +42 -13
  61. package/src/qingflow_mcp/tools/record_tools.py +14 -72
  62. package/src/qingflow_mcp/tools/storage_download.py +89 -0
@@ -111,7 +111,7 @@
111
111
  ]
112
112
  },
113
113
  "fileSize": {
114
- "minimum": 1,
114
+ "minimum": 0,
115
115
  "type": [
116
116
  "integer",
117
117
  "null"
@@ -131,7 +131,7 @@
131
131
  ]
132
132
  },
133
133
  "maxFileLimit": {
134
- "minimum": 1,
134
+ "minimum": 0,
135
135
  "type": [
136
136
  "integer",
137
137
  "null"
@@ -229,7 +229,7 @@
229
229
  },
230
230
  "DataRelationCondition": {
231
231
  "additionalProperties": false,
232
- "description": "一条数据关联过滤条件;sourceField 必须属于数据来源,operator 必须适用于该字段类型。固定值请使用 literal operand,不要直接声明后端 matchRules。",
232
+ "description": "一条数据关联过滤条件;sourceField 必须属于数据来源,operator 必须适用于该字段类型。固定值请使用 literal operand,不要直接声明后端 matchRules。sourceField 省略为存量读回形态(历史脏公式的源字段缺失,如 #queId_0$,写回时以 queId=0 透传),新建条件应提供 sourceField。",
233
233
  "properties": {
234
234
  "operand": {
235
235
  "$ref": "#/$defs/DataRelationOperand",
@@ -265,11 +265,10 @@
265
265
  },
266
266
  "sourceField": {
267
267
  "$ref": "#/$defs/FieldSelector",
268
- "description": "被过滤的数据源字段。"
268
+ "description": "被过滤的数据源字段;存量读回形态可能缺失(后端反解析出的条件源字段 queId=0 且无名称)。"
269
269
  }
270
270
  },
271
271
  "required": [
272
- "sourceField",
273
272
  "operator",
274
273
  "operand"
275
274
  ],
@@ -277,7 +276,7 @@
277
276
  },
278
277
  "DataRelationConfig": {
279
278
  "additionalProperties": false,
280
- "description": "数据关联字段配置;从应用、Q-Linker 或代码块输出中读取数据。",
279
+ "description": "数据关联字段配置;从应用、Q-Linker 或代码块输出中读取数据。source 省略表示该字段当前无关联来源配置(存量表单读回形态:后端 GET 对 displayed 目标缺失的 relation 行不回传,FormSvcImpl#getQuestionRelationsBeans continue 分支;新建数据关联字段应提供 source)。",
281
280
  "properties": {
282
281
  "aggregation": {
283
282
  "description": "可选聚合方式;null 表示不聚合。",
@@ -312,12 +311,10 @@
312
311
  },
313
312
  "source": {
314
313
  "$ref": "#/$defs/DataRelationSource",
315
- "description": "数据来源;按 kind 选择一种来源结构。"
314
+ "description": "数据来源;按 kind 选择一种来源结构。存量读回形态可能无 source(后端读面不回传关联配置),此时写回不重建该字段的关联。"
316
315
  }
317
316
  },
318
- "required": [
319
- "source"
320
- ],
317
+ "required": [],
321
318
  "type": "object"
322
319
  },
323
320
  "DataRelationCustomButton": {
@@ -989,7 +986,7 @@
989
986
  "description": "普通字段的默认值;后端 queDefaultValue 为字符串,因此数字等字面量也必须以字符串传入。defaultType 为 1 时表示静态文本,为 3 时表示公式文本,为 2 时不携带字面量默认值。表格、地址、附件、引用等结构化默认值必须放在具体字段 config.defaultValues。显式 null 表示清除默认值。"
990
987
  },
991
988
  "name": {
992
- "description": "字段展示名称;新增字段必填,已有字段可在携带 queId 时省略;同一父级内唯一。",
989
+ "description": "字段展示名称;新增字段必填,已有字段可在携带 queId 时省略;同父级允许同名(后端以 queId 为字段身份),配置引用建议携带 queId。",
993
990
  "minLength": 1,
994
991
  "type": "string"
995
992
  },
@@ -1010,13 +1007,9 @@
1010
1007
  "type": "string"
1011
1008
  },
1012
1009
  "width": {
1013
- "description": "字段在所在行的宽度百分比;只能选择 25、50、75 或 100。",
1014
- "enum": [
1015
- 25,
1016
- 50,
1017
- 75,
1018
- 100
1019
- ],
1010
+ "description": "字段在所在行的宽度百分比(1-100 整数);后端不校验总和与取值,存量数据存在 33/34 等三等分布值。",
1011
+ "maximum": 100,
1012
+ "minimum": 1,
1020
1013
  "type": "integer"
1021
1014
  }
1022
1015
  },
@@ -1036,10 +1029,10 @@
1036
1029
  ]
1037
1030
  }
1038
1031
  ],
1039
- "description": "字段选择器;用于定位应用的数据标题、数据封面、自定义编号取值来源或其他配置引用的字段。至少提供 queId 或 name,两者同时提供时必须指向同一字段。",
1032
+ "description": "字段选择器;用于定位应用的数据标题、数据封面、自定义编号取值来源或其他配置引用的字段。至少提供 queId 或 name,两者同时提供时必须指向同一字段。后端允许同父级同名字段,queId 是稳定身份;name 仅在唯一时可解析,命中多个同名时报 AMBIGUOUS_FIELD_SELECTOR,此时必须改用 queId。",
1040
1033
  "properties": {
1041
1034
  "name": {
1042
- "description": "字段名称;仅在当前作用域内唯一时可用于解析。",
1035
+ "description": "字段名称;仅在当前作用域内唯一时可用于解析,同名命中多个字段时报 AMBIGUOUS_FIELD_SELECTOR,应改用 queId。",
1043
1036
  "minLength": 1,
1044
1037
  "type": "string"
1045
1038
  },
@@ -1401,7 +1394,7 @@
1401
1394
  },
1402
1395
  "RelationConfig": {
1403
1396
  "additionalProperties": false,
1404
- "description": "引用字段配置;引用其他应用中的数据。",
1397
+ "description": "引用字段配置;引用其他应用中的数据。展示字段与权限以 displayFields 为准(后端 referQuestions 全量有序回写,queAuth 1=可编辑/2=隐藏/3=仅可见);displayField/visibleFields 为已废弃旧形态,apply 兼容接收一个版本窗口(displayField 同时视为双向绑定目标)。",
1405
1398
  "properties": {
1406
1399
  "beingDefaultFormulaAutoFillEnabled": {
1407
1400
  "description": "是否启用引用字段的默认值公式自动填充。",
@@ -1439,7 +1432,32 @@
1439
1432
  },
1440
1433
  "displayField": {
1441
1434
  "$ref": "#/$defs/FieldSelector",
1442
- "description": "引用值的主展示字段。"
1435
+ "description": "已废弃:主展示字段;apply 兼容翻译为 twoWayBoundField + displayFields 首项,后续版本移除。"
1436
+ },
1437
+ "displayFields": {
1438
+ "description": "有序展示字段全量列表(含隐藏字段),按 ordinal 保序回写为后端 referQuestions。可为空列表:空 = 双向绑定未配展示字段的合法存量形态(后端写链路对空 referQuestions 跳过插入、读面容忍空),round-trip 空↔空稳定;与已废弃 displayField/visibleFields 不可混用,条目 queId 不可重复。",
1439
+ "items": {
1440
+ "additionalProperties": false,
1441
+ "properties": {
1442
+ "auth": {
1443
+ "default": "visible",
1444
+ "description": "展示字段权限;editable=可编辑(1), hidden=隐藏(2), visible=仅可见(3);缺失按 visible。",
1445
+ "enum": [
1446
+ "editable",
1447
+ "hidden",
1448
+ "visible"
1449
+ ]
1450
+ },
1451
+ "field": {
1452
+ "$ref": "#/$defs/FieldSelector"
1453
+ }
1454
+ },
1455
+ "required": [
1456
+ "field"
1457
+ ],
1458
+ "type": "object"
1459
+ },
1460
+ "type": "array"
1443
1461
  },
1444
1462
  "relationMode": {
1445
1463
  "default": "single",
@@ -1454,8 +1472,19 @@
1454
1472
  "minLength": 1,
1455
1473
  "type": "string"
1456
1474
  },
1475
+ "twoWayBoundField": {
1476
+ "description": "双向引用绑定的目标字段(后端 referQueId);null/省略 = 单向引用。get 读面在 referQueId 非空时必产出。",
1477
+ "oneOf": [
1478
+ {
1479
+ "type": "null"
1480
+ },
1481
+ {
1482
+ "$ref": "#/$defs/FieldSelector"
1483
+ }
1484
+ ]
1485
+ },
1457
1486
  "visibleFields": {
1458
- "description": "引用选择界面额外展示的字段。",
1487
+ "description": "已废弃:额外展示字段;apply 兼容翻译为 displayFields 后续项,后续版本移除。",
1459
1488
  "items": {
1460
1489
  "$ref": "#/$defs/FieldSelector"
1461
1490
  },
@@ -1464,8 +1493,7 @@
1464
1493
  }
1465
1494
  },
1466
1495
  "required": [
1467
- "targetAppKey",
1468
- "displayField"
1496
+ "targetAppKey"
1469
1497
  ],
1470
1498
  "type": "object"
1471
1499
  },
@@ -1584,7 +1612,10 @@
1584
1612
  "$ref": "#/$defs/NumberConfig"
1585
1613
  },
1586
1614
  "questionType": {
1587
- "const": "number"
1615
+ "enum": [
1616
+ "number",
1617
+ "amount"
1618
+ ]
1588
1619
  }
1589
1620
  },
1590
1621
  "required": [
@@ -1597,7 +1628,10 @@
1597
1628
  "$ref": "#/$defs/EmptyConfig"
1598
1629
  },
1599
1630
  "questionType": {
1600
- "const": "date"
1631
+ "enum": [
1632
+ "date",
1633
+ "datetime"
1634
+ ]
1601
1635
  }
1602
1636
  },
1603
1637
  "required": [
@@ -78,7 +78,7 @@
78
78
  "description": "普通字段的默认值;后端 queDefaultValue 为字符串,因此数字等字面量也必须以字符串传入。defaultType 为 1 时表示静态文本,为 3 时表示公式文本,为 2 时不携带字面量默认值。表格、地址、附件、引用等结构化默认值必须放在具体字段 config.defaultValues。显式 null 表示清除默认值。"
79
79
  },
80
80
  "name": {
81
- "description": "字段展示名称;新增字段必填,已有字段可在携带 queId 时省略;同一父级内唯一。",
81
+ "description": "字段展示名称;新增字段必填,已有字段可在携带 queId 时省略;同父级允许同名(后端以 queId 为字段身份),配置引用建议携带 queId。",
82
82
  "minLength": 1,
83
83
  "type": "string"
84
84
  },
@@ -99,13 +99,9 @@
99
99
  "type": "string"
100
100
  },
101
101
  "width": {
102
- "description": "字段在所在行的宽度百分比;只能选择 25、50、75 或 100。",
103
- "enum": [
104
- 25,
105
- 50,
106
- 75,
107
- 100
108
- ],
102
+ "description": "字段在所在行的宽度百分比(1-100 整数);后端不校验总和与取值,存量数据存在 33/34 等三等分布值。",
103
+ "maximum": 100,
104
+ "minimum": 1,
109
105
  "type": "integer"
110
106
  }
111
107
  },
@@ -236,6 +236,28 @@
236
236
  "DATEDELTA(date, number, unit)"
237
237
  ]
238
238
  },
239
+ "DATEDIF": {
240
+ "category": "date",
241
+ "example": "DATEDIF(#queId_102$, #queId_101$)",
242
+ "meaning": "历史日期差函数名,仅存在于存量公式中(当前公式引擎未注册,日期差请使用 DAYS(end, start))。为使存量声明的 validate/round-trip 通过而保留声明,不建议新公式使用。",
243
+ "parameters": [
244
+ {
245
+ "description": "结束日期或列表",
246
+ "name": "end"
247
+ },
248
+ {
249
+ "description": "开始日期或列表",
250
+ "name": "start"
251
+ }
252
+ ],
253
+ "returns": {
254
+ "description": "天数差文本列表",
255
+ "type": "list"
256
+ },
257
+ "signatures": [
258
+ "DATEDIF(end, start)"
259
+ ]
260
+ },
239
261
  "DATETOTIMESTAMP": {
240
262
  "category": "date",
241
263
  "example": "DATETOTIMESTAMP(#queId_101$)",
@@ -363,6 +385,19 @@
363
385
  "GETUSEREMAIL()"
364
386
  ]
365
387
  },
388
+ "GETUSERENAME": {
389
+ "category": "context",
390
+ "example": "GETUSERENAME()",
391
+ "meaning": "历史拼写(GETUSERNAME 的讹写),仅存在于存量公式中(当前公式引擎未注册,请使用 GETUSERNAME())。为使存量声明的 validate/round-trip 通过而保留声明,不建议新公式使用。",
392
+ "parameters": [],
393
+ "returns": {
394
+ "description": "当前用户显示名称;无法取得时为空字符串",
395
+ "type": "string"
396
+ },
397
+ "signatures": [
398
+ "GETUSERENAME()"
399
+ ]
400
+ },
366
401
  "GETUSERNAME": {
367
402
  "category": "context",
368
403
  "example": "GETUSERNAME()",
@@ -1590,5 +1625,13 @@
1590
1625
  "需要拼接字段当前值时,将该字段的实际 queId 写成未加引号的 #queId_<字段ID>$;字段 name、字段标题和字段默认值都不能替代字段引用。",
1591
1626
  "新建应用的同一次 create 声明中尚未获得 queId 时,不得虚构 queId 或以字段 name 代替:先创建字段,app_form_get 回读实际 queId 后,再提交公式默认值更新。"
1592
1627
  ]
1628
+ },
1629
+ "x-qingflow-validation": {
1630
+ "functionCallPolicy": "validate 会识别公式文本中引号外的 函数名( 调用;函数名必须在 x-qingflow-functions 中声明。未知函数会被拒绝,字符串字面量中的 NAME() 不视为函数调用。",
1631
+ "scope": [
1632
+ "defaultType=3 的 defaultValue",
1633
+ "config.defaultValueFormula",
1634
+ "data_relation config.filters.mode=advanced 的 config.filters.formula"
1635
+ ]
1593
1636
  }
1594
1637
  }
@@ -136,10 +136,10 @@
136
136
  ]
137
137
  }
138
138
  ],
139
- "description": "字段选择器;用于定位应用的数据标题、数据封面、自定义编号取值来源或其他配置引用的字段。至少提供 queId 或 name,两者同时提供时必须指向同一字段。",
139
+ "description": "字段选择器;用于定位应用的数据标题、数据封面、自定义编号取值来源或其他配置引用的字段。至少提供 queId 或 name,两者同时提供时必须指向同一字段。后端允许同父级同名字段,queId 是稳定身份;name 仅在唯一时可解析,命中多个同名时报 AMBIGUOUS_FIELD_SELECTOR,此时必须改用 queId。",
140
140
  "properties": {
141
141
  "name": {
142
- "description": "字段名称;仅在当前作用域内唯一时可用于解析。",
142
+ "description": "字段名称;仅在当前作用域内唯一时可用于解析,同名命中多个字段时报 AMBIGUOUS_FIELD_SELECTOR,应改用 queId。",
143
143
  "minLength": 1,
144
144
  "type": "string"
145
145
  },
@@ -258,13 +258,9 @@
258
258
  "type": "array"
259
259
  },
260
260
  "width": {
261
- "description": "字段在所在行的宽度百分比;只能选择 25、50、75 或 100。",
262
- "enum": [
263
- 25,
264
- 50,
265
- 75,
266
- 100
267
- ],
261
+ "description": "字段在所在行的宽度百分比(1-100 整数);后端不校验总和与取值,存量数据存在 33/34 等三等分布值。",
262
+ "maximum": 100,
263
+ "minimum": 1,
268
264
  "type": "integer"
269
265
  }
270
266
  },
@@ -334,11 +330,10 @@
334
330
  "description": "段落判别值。"
335
331
  },
336
332
  "rows": {
337
- "description": "段落内的非空有序字段行。",
333
+ "description": "段落内的有序字段行;可为空数组(空 = 存量空段落形态,后端容忍,round-trip 空↔空稳定)。",
338
334
  "items": {
339
335
  "$ref": "#/$defs/FieldRow"
340
336
  },
341
- "minItems": 1,
342
337
  "type": "array"
343
338
  },
344
339
  "sectionId": {
@@ -383,7 +378,6 @@
383
378
  ],
384
379
  "type": "object"
385
380
  },
386
- "minItems": 1,
387
381
  "type": "array"
388
382
  },
389
383
  "sectionId": {
@@ -695,6 +689,7 @@
695
689
  "x-qingflow-supported-field-types": [
696
690
  "text",
697
691
  "long_text",
692
+ "rich_text",
698
693
  "number",
699
694
  "amount",
700
695
  "link",
@@ -163,9 +163,14 @@ FIELD_TYPE_TO_QUESTION_TYPE: dict[str, int] = {
163
163
  FieldType.relation.value: 25,
164
164
  }
165
165
 
166
+ # code_block / q_linker 输出目标字段类型。后端写链无类型白名单(FormSvcImpl 原样
167
+ # 落库),运行时 DataSvcImpl.getAnswerValueByString:attachment 按拆分 URL 写入、
168
+ # subtable 整表组装(均有专门分支),rich_text/data_relation 走 Question.canImport
169
+ # 兜底按字符串写入(不报错)——四者为存量合法配置,与 validator 侧白名单同口径。
166
170
  INTEGRATION_OUTPUT_TARGET_FIELD_TYPES: tuple[str, ...] = (
167
171
  FieldType.text.value,
168
172
  FieldType.long_text.value,
173
+ "rich_text",
169
174
  FieldType.number.value,
170
175
  FieldType.amount.value,
171
176
  FieldType.date.value,
@@ -173,6 +178,9 @@ INTEGRATION_OUTPUT_TARGET_FIELD_TYPES: tuple[str, ...] = (
173
178
  FieldType.single_select.value,
174
179
  FieldType.multi_select.value,
175
180
  FieldType.boolean.value,
181
+ FieldType.attachment.value,
182
+ "data_relation",
183
+ FieldType.subtable.value,
176
184
  )
177
185
 
178
186
  MATCH_TYPE_ACCURACY = 1
@@ -23237,6 +23245,23 @@ def _apply_field_mutation(field: dict[str, Any], mutation: Any) -> None:
23237
23245
  field.pop("_question_rebuild_required", None)
23238
23246
 
23239
23247
 
23248
+ class AmbiguousFieldSelector(ValueError):
23249
+ """name-only selector 命中多个同名字段。
23250
+
23251
+ 方案 A(duplicate-field-name):后端以 queId 为字段身份、允许同父级同名;
23252
+ name 命中多个字段时禁止静默取第一个,必须显式失败并给出候选 queId。
23253
+ compiler 包装层(app_form)把它转写为 AMBIGUOUS_FIELD_SELECTOR 错误码。
23254
+ """
23255
+
23256
+ def __init__(self, location: str, candidates: list[int]) -> None:
23257
+ self.location = location
23258
+ self.candidates = candidates
23259
+ super().__init__(
23260
+ f"{location} matches multiple fields with the same name; "
23261
+ f"use queId to disambiguate (candidate queIds: {sorted(candidates)})"
23262
+ )
23263
+
23264
+
23240
23265
  def _resolve_field_selector_with_uniqueness(
23241
23266
  *,
23242
23267
  fields: list[dict[str, Any]],
@@ -23252,7 +23277,12 @@ def _resolve_field_selector_with_uniqueness(
23252
23277
  name = str(selector.name or "").strip()
23253
23278
  matched = [field for field in fields if str(field.get("name") or "").strip() == name]
23254
23279
  if len(matched) > 1:
23255
- raise ValueError(f"{location} matched multiple fields with the same name; use field_id or que_id")
23280
+ candidate_ids = [
23281
+ ref
23282
+ for candidate in matched
23283
+ if (ref := _coerce_any_int(candidate.get("que_id"))) is not None
23284
+ ]
23285
+ raise AmbiguousFieldSelector(location, candidate_ids)
23256
23286
  else:
23257
23287
  matched = []
23258
23288
  if len(matched) != 1:
@@ -29932,6 +29962,14 @@ def _build_view_update_payload(
29932
29962
  payload["viewgraphType"] = "calendarView"
29933
29963
  payload["beingShowTitleQue"] = True
29934
29964
  payload.pop("groupQueId", None)
29965
+ elif normalized_type == "hierarchy":
29966
+ payload["viewgraphType"] = "hierarchyView"
29967
+ payload["beingShowTitleQue"] = True
29968
+ payload.pop("groupQueId", None)
29969
+ elif normalized_type == "calendar":
29970
+ payload["viewgraphType"] = "calendarView"
29971
+ payload["beingShowTitleQue"] = True
29972
+ payload.pop("groupQueId", None)
29935
29973
  return _hydrate_view_backend_payload(
29936
29974
  payload=payload,
29937
29975
  view_type=normalized_type,
@@ -20,6 +20,8 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
20
20
  items.json [{"fields": {"101": "A"}}]
21
21
  fields.json {"101": "B"}
22
22
  query/get/insert/update 默认返回 field_capabilities;可用 --no-schema 关闭。
23
+ 下载附件:data get --app-key APP_KEY --record-id RECORD_ID --download-attachments,
24
+ 从 file_assets.items[].local_path 获取本地文件。
23
25
  """).strip(),
24
26
  )
25
27
  commands = parser.add_subparsers(dest="data_command", required=True, metavar="{query,get,insert,update,delete}")
@@ -27,7 +29,9 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
27
29
  query = commands.add_parser(
28
30
  "query", help="分页查询应用数据", formatter_class=argparse.RawDescriptionHelpFormatter,
29
31
  epilog='最小示例:qingflow data query --app-key APP_1 --where-file where.json\n'
30
- 'where.json: [{"field_id": 101, "op": "eq", "value": "A"}]',
32
+ 'where.json: [{"field_id": 101, "op": "eq", "value": "A"}]\n'
33
+ '查询不下载附件;对目标记录执行 data get --app-key APP_KEY --record-id RECORD_ID\n'
34
+ '--download-attachments,使用 file_assets.items[].local_path。',
31
35
  )
32
36
  query.add_argument("--app-key", required=True)
33
37
  query.add_argument("--columns-file", help='JSON 数组,例如 [101, 102]')
@@ -44,10 +48,15 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
44
48
  query.add_argument("--output", choices=("json", "csv"), default="json")
45
49
  query.set_defaults(handler=_handle_query, format_hint="data_query")
46
50
 
47
- get = commands.add_parser("get", help="读取单条应用数据")
51
+ get = commands.add_parser(
52
+ "get", help="读取单条应用数据", formatter_class=argparse.RawDescriptionHelpFormatter,
53
+ epilog='默认只读取字段;下载附件或图片时加 --download-attachments,并保留 schema。\n'
54
+ '使用 file_assets.items[].local_path;部分下载失败时读取 file_assets.warnings。',
55
+ )
48
56
  get.add_argument("--app-key", required=True)
49
57
  get.add_argument("--record-id", required=True)
50
58
  get.add_argument("--no-schema", action="store_false", dest="include_schema", default=True)
59
+ get.add_argument("--download-attachments", action="store_true", help="下载当前记录可见附件到本地(需要 schema)")
51
60
  get.set_defaults(handler=_handle_get, format_hint="data_get")
52
61
 
53
62
  insert = commands.add_parser(
@@ -102,7 +111,8 @@ def _handle_query(args: argparse.Namespace, context: CliContext) -> dict:
102
111
 
103
112
  def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
104
113
  return context.data.data_get(
105
- profile=args.profile, app_key=args.app_key, record_id=args.record_id, include_schema=args.include_schema
114
+ profile=args.profile, app_key=args.app_key, record_id=args.record_id, include_schema=args.include_schema,
115
+ download_attachments=args.download_attachments,
106
116
  )
107
117
 
108
118
 
@@ -137,6 +137,13 @@ def _format_data_result(result: dict[str, Any]) -> str:
137
137
  record = result.get("record") if isinstance(result.get("record"), dict) else {}
138
138
  if record:
139
139
  lines.append(f"Record ID: {record.get('record_id') or '-'}")
140
+ file_assets = result.get("file_assets")
141
+ if isinstance(file_assets, dict):
142
+ lines.append(f"Attachments: {file_assets.get('status') or '-'}")
143
+ for asset in file_assets.get("items", []):
144
+ if isinstance(asset, dict):
145
+ lines.append(f"- {asset.get('file_name')}: {asset.get('access_status')} / {asset.get('local_path') or '-'}")
146
+ _append_warnings(lines, file_assets.get("warnings"))
140
147
  items = result.get("items") if isinstance(result.get("items"), list) else []
141
148
  if items:
142
149
  lines.append(f"Items: {len(items)}")
@@ -7,11 +7,22 @@ import sys
7
7
  from typing import Any, Callable, TextIO
8
8
 
9
9
  from ..errors import QingflowApiError, backend_code_value_int, message_looks_like_invalid_token
10
- from ..public_surface import cli_public_tool_spec_from_namespace
10
+ from ..public_surface import (
11
+ NON_TOOL_CLI_HANDLER_ROUTES,
12
+ cli_public_tool_spec_from_namespace,
13
+ cli_route_from_namespace,
14
+ cli_trim_key_from_namespace,
15
+ )
11
16
  from ..resource_uri import cli_resource_base_url
12
17
  from ..response_trim import resolve_cli_tool_name, trim_error_response, trim_public_response
13
18
  from ..tools.ai_builder_tools import _attach_builder_apply_envelope
14
19
  from ..version import get_cli_version, get_cli_version_info
20
+ from ..tool_policy import (
21
+ TOOL_POLICY_ENV,
22
+ TOOL_POLICY_INVALID,
23
+ ToolPolicyViolation,
24
+ parse_tool_policy,
25
+ )
15
26
  from .context import CliContext, build_cli_context
16
27
  from .formatters import emit_json_result, emit_text_result
17
28
  from .commands import register_all_commands
@@ -90,6 +101,14 @@ def run(
90
101
  if handler is None:
91
102
  parser.print_help(out)
92
103
  return 2
104
+ violation = _cli_tool_policy_violation(args)
105
+ if violation is not None:
106
+ return _emit_error(
107
+ violation.payload(),
108
+ json_mode=bool(args.json),
109
+ stdout=out,
110
+ stderr=err,
111
+ )
93
112
  try:
94
113
  if _should_force_json_output(args):
95
114
  setattr(args, "json", True)
@@ -122,6 +141,20 @@ def run(
122
141
  return exit_code
123
142
 
124
143
 
144
+ def _cli_tool_policy_violation(args: argparse.Namespace) -> ToolPolicyViolation | None:
145
+ stable_id = cli_trim_key_from_namespace(args)
146
+ if stable_id is None:
147
+ # 仅显式登记的非业务分发器可跳过闸门;无子命令的新增 handler 也必须默认拒绝。
148
+ if cli_route_from_namespace(args) in NON_TOOL_CLI_HANDLER_ROUTES:
149
+ return None
150
+ return ToolPolicyViolation(
151
+ TOOL_POLICY_INVALID,
152
+ "user:unregistered",
153
+ "CLI 业务命令未登记稳定工具 ID",
154
+ )
155
+ return parse_tool_policy(os.getenv(TOOL_POLICY_ENV)).violation_for(stable_id)
156
+
157
+
125
158
  def _handle_version(_args: argparse.Namespace, _context: CliContext) -> dict[str, Any]:
126
159
  info = get_cli_version_info()
127
160
  return {
@@ -2,11 +2,14 @@ from __future__ import annotations
2
2
 
3
3
  from contextvars import ContextVar
4
4
  from dataclasses import dataclass, field
5
- from typing import Callable
5
+ from typing import TYPE_CHECKING, Callable
6
6
 
7
7
  from .backend_client import BackendRequestContext
8
8
  from .session_store import BackendSession, SessionProfile
9
9
 
10
+ if TYPE_CHECKING:
11
+ from .tool_policy import ToolPolicySnapshot
12
+
10
13
 
11
14
  CLOUD_PROFILE_NAME = "__cloud_mcp__"
12
15
 
@@ -73,3 +76,15 @@ current_call_platform: ContextVar[str] = ContextVar(
73
76
  "qingflow_mcp_current_call_platform",
74
77
  default="",
75
78
  )
79
+
80
+ # 远端 MCP 每个 HTTP 请求独立绑定策略原文;缺头用 None 表示,不回退进程环境变量。
81
+ current_tool_policy_header: ContextVar[str | None] = ContextVar(
82
+ "qingflow_mcp_current_tool_policy_header",
83
+ default=None,
84
+ )
85
+
86
+ # 在 HTTP 入口合成一次快照,列表与调用共用,避免将 URL 选择保存到会话或共享环境。
87
+ current_http_tool_policy: ContextVar[ToolPolicySnapshot | None] = ContextVar(
88
+ "qingflow_mcp_current_http_tool_policy",
89
+ default=None,
90
+ )