@qingflow-tech/qingflow-cli 1.1.57 → 1.1.58

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 (82) hide show
  1. package/README.md +4 -4
  2. package/npm/lib/runtime.mjs +13 -29
  3. package/package.json +1 -1
  4. package/pyproject.toml +1 -2
  5. package/skills/qingflow-cli/SKILL.md +319 -90
  6. package/skills/qingflow-cli/manifest.yaml +1 -1
  7. package/skills/qingflow-cli/reference/00-INDEX.md +4 -4
  8. package/skills/qingflow-cli/reference/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
  9. package/skills/qingflow-cli/reference/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +137 -3
  10. package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +4 -6
  11. package/skills/qingflow-cli/reference/QINGFLOW_CLI_FIELD_DATA_TYPES.md +14 -14
  12. package/skills/qingflow-cli/reference/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +19 -20
  13. package/skills/qingflow-cli/reference/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +14 -18
  14. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md +12 -7
  15. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +1 -2
  16. package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +2 -2
  17. package/skills/qingflow-cli/reference/builder/30-schema-fields.md +5 -9
  18. package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +3 -3
  19. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
  20. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +95 -76
  21. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_EXPLORATION_REPORT.md +4 -6
  22. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_FIELD_DATA_TYPES.md +13 -13
  23. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +19 -20
  24. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +14 -18
  25. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +1 -2
  26. package/skills/qingflow-cli/reference/record/analysis/README.md +113 -21
  27. package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +10 -10
  28. package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +9 -9
  29. package/skills/qingflow-cli/reference/record/analysis/business-context.md +1 -1
  30. package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +10 -10
  31. package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +80 -52
  32. package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +2 -2
  33. package/skills/qingflow-common/SKILL.md +4 -7
  34. package/src/qingflow_mcp/app_form_compat.py +0 -3
  35. package/src/qingflow_mcp/app_form_validator.py +17 -69
  36. package/src/qingflow_mcp/builder_facade/app_form.py +100 -497
  37. package/src/qingflow_mcp/builder_facade/models.py +2 -2
  38. package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +0 -1
  39. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +30 -64
  40. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +8 -4
  41. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +8 -4
  42. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +10 -6
  43. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +10 -6
  44. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +18 -11
  45. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +8 -4
  46. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +8 -4
  47. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +10 -6
  48. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +8 -4
  49. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +8 -4
  50. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +8 -4
  51. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +10 -6
  52. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +8 -4
  53. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +8 -4
  54. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +8 -4
  55. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +10 -6
  56. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +15 -46
  57. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +8 -4
  58. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +8 -4
  59. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +27 -61
  60. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +8 -4
  61. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/formula.json +0 -43
  62. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +12 -7
  63. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/complete.json +3 -4
  64. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/data-permission.json +3 -3
  65. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/summary.json +2 -3
  66. package/src/qingflow_mcp/builder_facade/service.py +6 -43
  67. package/src/qingflow_mcp/builder_facade/views_extract.py +12 -2
  68. package/src/qingflow_mcp/cli/commands/__init__.py +1 -2
  69. package/src/qingflow_mcp/cli/commands/record.py +1 -1
  70. package/src/qingflow_mcp/cli/context.py +0 -3
  71. package/src/qingflow_mcp/cli/formatters.py +1 -97
  72. package/src/qingflow_mcp/public_surface.py +0 -5
  73. package/src/qingflow_mcp/response_trim.py +1 -2
  74. package/src/qingflow_mcp/server.py +0 -2
  75. package/src/qingflow_mcp/server_app_user.py +0 -4
  76. package/src/qingflow_mcp/tools/record_tools.py +46 -11
  77. package/skills/qingflow-cli/reference/data/README.md +0 -32
  78. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_SPECIALIZED_WORKFLOW.md +0 -50
  79. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/rich_text.json +0 -146
  80. package/src/qingflow_mcp/cli/commands/data.py +0 -134
  81. package/src/qingflow_mcp/tools/csv_export.py +0 -168
  82. package/src/qingflow_mcp/tools/data_tools.py +0 -284
@@ -136,10 +136,10 @@
136
136
  ]
137
137
  }
138
138
  ],
139
- "description": "字段选择器;用于定位应用的数据标题、数据封面、自定义编号取值来源或其他配置引用的字段。至少提供 queId 或 name,两者同时提供时必须指向同一字段。后端允许同父级同名字段,queId 是稳定身份;name 仅在唯一时可解析,命中多个同名时报 AMBIGUOUS_FIELD_SELECTOR,此时必须改用 queId。",
139
+ "description": "字段选择器;用于定位应用的数据标题、数据封面、自定义编号取值来源或其他配置引用的字段。至少提供 queId 或 name,两者同时提供时必须指向同一字段。",
140
140
  "properties": {
141
141
  "name": {
142
- "description": "字段名称;仅在当前作用域内唯一时可用于解析,同名命中多个字段时报 AMBIGUOUS_FIELD_SELECTOR,应改用 queId。",
142
+ "description": "字段名称;仅在当前作用域内唯一时可用于解析。",
143
143
  "minLength": 1,
144
144
  "type": "string"
145
145
  },
@@ -258,9 +258,13 @@
258
258
  "type": "array"
259
259
  },
260
260
  "width": {
261
- "description": "字段在所在行的宽度百分比(1-100 整数);后端不校验总和与取值,存量数据存在 33/34 等三等分布值。",
262
- "maximum": 100,
263
- "minimum": 1,
261
+ "description": "字段在所在行的宽度百分比;只能选择 25、50、75 或 100。",
262
+ "enum": [
263
+ 25,
264
+ 50,
265
+ 75,
266
+ 100
267
+ ],
264
268
  "type": "integer"
265
269
  }
266
270
  },
@@ -330,10 +334,11 @@
330
334
  "description": "段落判别值。"
331
335
  },
332
336
  "rows": {
333
- "description": "段落内的有序字段行;可为空数组(空 = 存量空段落形态,后端容忍,round-trip 空↔空稳定)。",
337
+ "description": "段落内的非空有序字段行。",
334
338
  "items": {
335
339
  "$ref": "#/$defs/FieldRow"
336
340
  },
341
+ "minItems": 1,
337
342
  "type": "array"
338
343
  },
339
344
  "sectionId": {
@@ -378,6 +383,7 @@
378
383
  ],
379
384
  "type": "object"
380
385
  },
386
+ "minItems": 1,
381
387
  "type": "array"
382
388
  },
383
389
  "sectionId": {
@@ -689,7 +695,6 @@
689
695
  "x-qingflow-supported-field-types": [
690
696
  "text",
691
697
  "long_text",
692
- "rich_text",
693
698
  "number",
694
699
  "amount",
695
700
  "link",
@@ -75,7 +75,7 @@
75
75
  }
76
76
  ]
77
77
  },
78
- "filtersDescription": "数据范围规则,仅 type=custom 且 scope=custom 时生效,编译为 viewgraphLimit。不会叠在 config 或 all/oneself/dept/subordinate 上。扁平 [rule,...]=单组 AND;嵌套 [[rule,...],...]=组间 OR、组内 AND。",
78
+ "filtersDescription": "数据范围规则,仅 type=custom 且 scope=custom 时生效,编译为 viewgraphLimit。不会叠在 config 或 all/oneself/dept 上。扁平 [rule,...]=单组 AND;嵌套 [[rule,...],...]=组间 OR、组内 AND。",
79
79
  "x-backend": "viewgraphLimitType=1 + viewgraphLimit(List<List<MatchRule>> 单组);公式形态(scope=custom 的 OR 组)列 v1alpha2"
80
80
  },
81
81
  "sort": {
@@ -330,12 +330,11 @@
330
330
  "all",
331
331
  "oneself",
332
332
  "dept",
333
- "subordinate",
334
333
  "custom"
335
334
  ],
336
335
  "default": "all",
337
- "dataScopeDescription": "写出 data_permission 时必填,且只在 type=custom 时生效。all=不追加数据范围;oneself=仅自己发起;dept=所在部门发起;subordinate=下属发起;custom=用 filters 作为数据范围。type=config 时不要用 scope 选择应用权限,应用授权组的范围写在成员授权上。",
338
- "x-backend": "dataScope(ALL|ONESELF|DEPT|SUBORDINATE|CUSTOM)"
336
+ "dataScopeDescription": "写出 data_permission 时必填,且只在 type=custom 时生效。all=不追加数据范围;oneself=仅自己发起;dept=所在部门发起;custom=用 filters 作为数据范围。type=config 时不要用 scope 选择应用权限,应用授权组的范围写在成员授权上。",
337
+ "x-backend": "dataScope(ALL|ONESELF|DEPT|CUSTOM)"
339
338
  }
340
339
  }
341
340
  },
@@ -39,13 +39,13 @@
39
39
  "x-qingflow-apply-semantics": "写出 data_permission 时 type 与 scope 都必填,优先 type=custom、scope=all。整段省略时创建路径才按 custom + all。只有要跟随应用成员授权时才用 type=config,此时不要用 scope 表达范围。filters 仅在 type=custom 且 scope=custom 时作为数据范围;省略 filters 表示保留现有规则,空数组表示清空。",
40
40
  "x-qingflow-mode-requirements": {
41
41
  "default": "未声明 data_permission 时按 custom + all 创建。需要限制行时再改 scope,不要默认改成 config。",
42
- "config": "跟随应用成员授权的数据权限。视图上的 scope、filters 不参与查询。应用授权组自己可以是全部、自己、部门、下属或自定义公式,那些规则写在成员授权上,不写在视图声明里。",
43
- "custom": "使用视图自己的 dataScope。scope=all 不追加范围;oneself/dept/subordinate 用预设的申请人范围,不读 filters;scope=custom 时 filters 才是数据范围规则。",
42
+ "config": "跟随应用成员授权的数据权限。视图上的 scope、filters 不参与查询。应用授权组的范围写在成员授权上,不写在视图声明里。",
43
+ "custom": "使用视图自己的 dataScope。scope=all 不追加范围;oneself/dept 用预设的申请人范围,不读 filters;scope=custom 时 filters 才是数据范围规则。",
44
44
  "filters": "只在 type=custom 且 scope=custom 时作为数据权限生效。扁平数组表示一组 AND;嵌套数组表示组间 OR、组内 AND。不是叠在 config 或预设范围上的第二层筛选。"
45
45
  },
46
46
  "x-qingflow-backend-semantics": {
47
47
  "data_permission.type": "dataPermissionType(CONFIG/CUSTOM)",
48
- "data_permission.scope": "dataScope(ALL/ONESELF/DEPT/SUBORDINATE/CUSTOM)",
48
+ "data_permission.scope": "dataScope(ALL/ONESELF/DEPT/CUSTOM)",
49
49
  "filters": "viewgraphLimitType=1 + viewgraphLimit;空数组清除简易条件。"
50
50
  },
51
51
  "x-qingflow-unsupported": {
@@ -218,12 +218,11 @@
218
218
  "all",
219
219
  "oneself",
220
220
  "dept",
221
- "subordinate",
222
221
  "custom"
223
222
  ],
224
223
  "default": "all",
225
- "dataScopeDescription": "写出 data_permission 时必填,且只在 type=custom 时生效。all=不追加数据范围;oneself=仅自己发起;dept=所在部门发起;subordinate=下属发起;custom=用 filters 作为数据范围(规则在 complete 或 schema_kind=data-permission)。type=config 时不要用 scope 选择应用权限,应用授权组的范围写在成员授权上。dataset-v0 实测分布:all×560 / custom×74 / oneself×71。",
226
- "x-backend": "viewgraph.dataScope(ALL|ONESELF|DEPT|SUBORDINATE|CUSTOM)"
224
+ "dataScopeDescription": "写出 data_permission 时必填,且只在 type=custom 时生效。all=不追加数据范围;oneself=仅自己发起;dept=所在部门发起;custom=用 filters 作为数据范围(规则在 complete 或 schema_kind=data-permission)。type=config 时不要用 scope 选择应用权限,应用授权组的范围写在成员授权上。dataset-v0 实测分布:all×560 / custom×74 / oneself×71。",
225
+ "x-backend": "viewgraph.dataScope(ALL|ONESELF|DEPT|CUSTOM)"
227
226
  }
228
227
  }
229
228
  }
@@ -163,14 +163,9 @@ 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 侧白名单同口径。
170
166
  INTEGRATION_OUTPUT_TARGET_FIELD_TYPES: tuple[str, ...] = (
171
167
  FieldType.text.value,
172
168
  FieldType.long_text.value,
173
- "rich_text",
174
169
  FieldType.number.value,
175
170
  FieldType.amount.value,
176
171
  FieldType.date.value,
@@ -178,9 +173,6 @@ INTEGRATION_OUTPUT_TARGET_FIELD_TYPES: tuple[str, ...] = (
178
173
  FieldType.single_select.value,
179
174
  FieldType.multi_select.value,
180
175
  FieldType.boolean.value,
181
- FieldType.attachment.value,
182
- "data_relation",
183
- FieldType.subtable.value,
184
176
  )
185
177
 
186
178
  MATCH_TYPE_ACCURACY = 1
@@ -13776,7 +13768,7 @@ class AiBuilderFacade:
13776
13768
  schema_version=schema_version,
13777
13769
  )
13778
13770
  projected: list[dict[str, Any]] = []
13779
- scope_map = {"ALL": "all", "ONESELF": "oneself", "DEPT": "dept", "SUBORDINATE": "subordinate", "CUSTOM": "custom"}
13771
+ scope_map = {"ALL": "all", "ONESELF": "oneself", "DEPT": "dept", "CUSTOM": "custom"}
13780
13772
  for view in views if isinstance(views, list) else []:
13781
13773
  if not isinstance(view, dict):
13782
13774
  continue
@@ -13805,10 +13797,12 @@ class AiBuilderFacade:
13805
13797
  if title_que_id is not None and title_que_id in name_by_que_id:
13806
13798
  entry.setdefault("title_field", name_by_que_id[title_que_id])
13807
13799
  data_permission_type = view.get("dataPermissionType")
13808
- if data_permission_type:
13800
+ raw_scope = str(view.get("dataScope") or "").upper()
13801
+ public_scope = scope_map.get(raw_scope)
13802
+ if data_permission_type and (not raw_scope or public_scope):
13809
13803
  entry["data_permission"] = {
13810
13804
  "type": "config" if str(data_permission_type).upper() == "CONFIG" else "custom",
13811
- "scope": scope_map.get(str(view.get("dataScope") or "").upper(), "all"),
13805
+ "scope": public_scope or "all",
13812
13806
  }
13813
13807
  projected.append(entry)
13814
13808
  return {
@@ -23243,23 +23237,6 @@ def _apply_field_mutation(field: dict[str, Any], mutation: Any) -> None:
23243
23237
  field.pop("_question_rebuild_required", None)
23244
23238
 
23245
23239
 
23246
- class AmbiguousFieldSelector(ValueError):
23247
- """name-only selector 命中多个同名字段。
23248
-
23249
- 方案 A(duplicate-field-name):后端以 queId 为字段身份、允许同父级同名;
23250
- name 命中多个字段时禁止静默取第一个,必须显式失败并给出候选 queId。
23251
- compiler 包装层(app_form)把它转写为 AMBIGUOUS_FIELD_SELECTOR 错误码。
23252
- """
23253
-
23254
- def __init__(self, location: str, candidates: list[int]) -> None:
23255
- self.location = location
23256
- self.candidates = candidates
23257
- super().__init__(
23258
- f"{location} matches multiple fields with the same name; "
23259
- f"use queId to disambiguate (candidate queIds: {sorted(candidates)})"
23260
- )
23261
-
23262
-
23263
23240
  def _resolve_field_selector_with_uniqueness(
23264
23241
  *,
23265
23242
  fields: list[dict[str, Any]],
@@ -23275,12 +23252,7 @@ def _resolve_field_selector_with_uniqueness(
23275
23252
  name = str(selector.name or "").strip()
23276
23253
  matched = [field for field in fields if str(field.get("name") or "").strip() == name]
23277
23254
  if len(matched) > 1:
23278
- candidate_ids = [
23279
- ref
23280
- for candidate in matched
23281
- if (ref := _coerce_any_int(candidate.get("que_id"))) is not None
23282
- ]
23283
- raise AmbiguousFieldSelector(location, candidate_ids)
23255
+ raise ValueError(f"{location} matched multiple fields with the same name; use field_id or que_id")
23284
23256
  else:
23285
23257
  matched = []
23286
23258
  if len(matched) != 1:
@@ -29960,14 +29932,6 @@ def _build_view_update_payload(
29960
29932
  payload["viewgraphType"] = "calendarView"
29961
29933
  payload["beingShowTitleQue"] = True
29962
29934
  payload.pop("groupQueId", None)
29963
- elif normalized_type == "hierarchy":
29964
- payload["viewgraphType"] = "hierarchyView"
29965
- payload["beingShowTitleQue"] = True
29966
- payload.pop("groupQueId", None)
29967
- elif normalized_type == "calendar":
29968
- payload["viewgraphType"] = "calendarView"
29969
- payload["beingShowTitleQue"] = True
29970
- payload.pop("groupQueId", None)
29971
29935
  return _hydrate_view_backend_payload(
29972
29936
  payload=payload,
29973
29937
  view_type=normalized_type,
@@ -30221,7 +30185,6 @@ _DATA_PERMISSION_SCOPE_MAP = {
30221
30185
  "all": "ALL",
30222
30186
  "oneself": "ONESELF",
30223
30187
  "dept": "DEPT",
30224
- "subordinate": "SUBORDINATE",
30225
30188
  "custom": "CUSTOM",
30226
30189
  }
30227
30190
 
@@ -34,7 +34,6 @@ _DATA_SCOPE_TO_PUBLIC = {
34
34
  "ALL": "all",
35
35
  "ONESELF": "oneself",
36
36
  "DEPT": "dept",
37
- "SUBORDINATE": "subordinate",
38
37
  "CUSTOM": "custom",
39
38
  }
40
39
 
@@ -470,7 +469,18 @@ def extract_complete_view_declaration(
470
469
  declaration["filters"] = filters
471
470
 
472
471
  data_permission_type = str(view_info.get("dataPermissionType") or "").strip().upper()
473
- data_scope = _DATA_SCOPE_TO_PUBLIC.get(str(view_info.get("dataScope") or "").strip().upper())
472
+ raw_scope = str(view_info.get("dataScope") or "").strip().upper()
473
+ if raw_scope and raw_scope not in _DATA_SCOPE_TO_PUBLIC:
474
+ gaps.append(
475
+ _gap(
476
+ "data_permission.scope",
477
+ "unsupported_data_scope",
478
+ "dataScope is outside the public view contract",
479
+ value_preview=raw_scope,
480
+ )
481
+ )
482
+ return {}, gaps
483
+ data_scope = _DATA_SCOPE_TO_PUBLIC.get(raw_scope)
474
484
  if data_permission_type or data_scope:
475
485
  declaration["data_permission"] = {
476
486
  "type": "config" if data_permission_type == "CONFIG" else "custom",
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import argparse
4
4
 
5
- from . import app, auth, builder, chart, data, exports, imports, portal, record, task, view, workspace
5
+ from . import app, auth, builder, chart, exports, imports, portal, record, task, view, workspace
6
6
 
7
7
 
8
8
  def register_all_commands(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
@@ -13,7 +13,6 @@ def register_all_commands(subparsers: argparse._SubParsersAction[argparse.Argume
13
13
  view.register(subparsers)
14
14
  chart.register(subparsers)
15
15
  record.register(subparsers)
16
- data.register(subparsers)
17
16
  imports.register(subparsers)
18
17
  exports.register(subparsers)
19
18
  task.register(subparsers)
@@ -9,7 +9,7 @@ from .common import load_list_arg, load_object_arg, raise_config_error, require_
9
9
 
10
10
 
11
11
  def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
12
- parser = subparsers.add_parser("record", help="日志、附件、导入、代码块及 record 专项能力")
12
+ parser = subparsers.add_parser("record", help="记录与表结构")
13
13
  record_subparsers = parser.add_subparsers(
14
14
  dest="record_command",
15
15
  required=True,
@@ -11,7 +11,6 @@ from ..tools.ai_builder_tools import AiBuilderTools
11
11
  from ..tools.app_tools import AppTools
12
12
  from ..tools.auth_tools import AuthTools
13
13
  from ..tools.code_block_tools import CodeBlockTools
14
- from ..tools.data_tools import DataTools
15
14
  from ..tools.export_tools import ExportTools
16
15
  from ..tools.feedback_tools import FeedbackTools
17
16
  from ..tools.file_tools import FileTools
@@ -34,7 +33,6 @@ class CliContext:
34
33
  app: AppTools
35
34
  resource: ResourceReadTools
36
35
  record: RecordTools
37
- data: DataTools
38
36
  code_block: CodeBlockTools
39
37
  imports: ImportTools
40
38
  exports: ExportTools
@@ -66,7 +64,6 @@ def build_cli_context() -> CliContext:
66
64
  app=AppTools(sessions, backend),
67
65
  resource=ResourceReadTools(sessions, backend),
68
66
  record=RecordTools(sessions, backend),
69
- data=DataTools(sessions, backend),
70
67
  code_block=CodeBlockTools(sessions, backend),
71
68
  imports=ImportTools(sessions, backend),
72
69
  exports=ExportTools(sessions, backend),
@@ -8,10 +8,7 @@ from typing import Any, TextIO
8
8
  def emit_text_result(result: dict[str, Any], *, hint: str, stream: TextIO) -> None:
9
9
  text = _format_cancelled_result(result)
10
10
  if text is None:
11
- if hint in _DATA_FORMAT_HINTS:
12
- # 数据面失败也必须保留 field_errors 与列表型 hints,不能先走通用失败裁剪。
13
- text = _format_data_result(result)
14
- elif _is_failed_result(result) and hint != "task_action_execute":
11
+ if _is_failed_result(result) and hint != "task_action_execute":
15
12
  text = _format_failed_result(result)
16
13
  else:
17
14
  formatter = _FORMATTERS.get(hint, _format_generic)
@@ -93,92 +90,6 @@ def _format_generic(result: dict[str, Any]) -> str:
93
90
  return "\n".join(lines) + "\n"
94
91
 
95
92
 
96
- def _format_data_result(result: dict[str, Any]) -> str:
97
- lines = [f"Status: {result.get('status') or ('success' if result.get('ok') is not False else 'failed')}"]
98
- if result.get("format") == "csv":
99
- files = result.get("files") if isinstance(result.get("files"), list) else []
100
- lines.extend(
101
- [
102
- f"Local dir: {result.get('local_dir') or '-'}",
103
- f"Files: {len(files)}",
104
- ]
105
- )
106
- for item in files:
107
- if isinstance(item, dict):
108
- lines.append(
109
- f"- part {item.get('part')}: {item.get('local_path')} ({item.get('row_count')} rows)"
110
- )
111
- lines.extend(
112
- [
113
- f"Rows: {result.get('row_count')}",
114
- f"Complete: {result.get('complete')}",
115
- f"Truncated: {result.get('truncated')}",
116
- f"Safe for final conclusion: {result.get('safe_for_final_conclusion')}",
117
- f"Stopped reason: {result.get('stopped_reason') or '-'}",
118
- ]
119
- )
120
- access = result.get("access") if isinstance(result.get("access"), dict) else {}
121
- if access:
122
- sources = access.get("scope_sources") if isinstance(access.get("scope_sources"), list) else []
123
- lines.append(
124
- f"Access: level={access.get('level') or '-'} / can_write={access.get('can_write')}"
125
- f" / scope_sources={','.join(str(item) for item in sources) or '-'}"
126
- )
127
-
128
- records = result.get("records") if isinstance(result.get("records"), dict) else {}
129
- if records:
130
- items = records.get("items") if isinstance(records.get("items"), list) else []
131
- lines.append(
132
- "Pagination: "
133
- f"page={records.get('page')} / requested_page_size={records.get('requested_page_size')} / "
134
- f"page_size={records.get('page_size')} / total={records.get('total')} / "
135
- f"has_more={records.get('has_more')} / returned={len(items)}"
136
- )
137
- record = result.get("record") if isinstance(result.get("record"), dict) else {}
138
- if record:
139
- lines.append(f"Record ID: {record.get('record_id') or '-'}")
140
- items = result.get("items") if isinstance(result.get("items"), list) else []
141
- if items:
142
- lines.append(f"Items: {len(items)}")
143
-
144
- for key, label in (
145
- ("error_code", "Error Code"),
146
- ("message", "Message"),
147
- ("write_executed", "Write Executed"),
148
- ("retry_safe", "Retry Safe"),
149
- ):
150
- if key in result:
151
- lines.append(f"{label}: {result.get(key)}")
152
-
153
- field_errors = result.get("field_errors") if isinstance(result.get("field_errors"), list) else []
154
- if field_errors:
155
- lines.append("Field Errors:")
156
- _append_data_field_errors(lines, field_errors)
157
- for index, item in enumerate(items):
158
- if not isinstance(item, dict) or not isinstance(item.get("field_errors"), list):
159
- continue
160
- lines.append(f"Item {index} Field Errors:")
161
- _append_data_field_errors(lines, item["field_errors"])
162
-
163
- hints = result.get("hints") if isinstance(result.get("hints"), list) else []
164
- lines.append(f"Hints: {len(hints)}")
165
- for hint in hints:
166
- if isinstance(hint, dict):
167
- lines.append(f"- {hint.get('code') or '-'}: {hint.get('message') or '-'}")
168
- else:
169
- lines.append(f"- {hint}")
170
- return "\n".join(lines) + "\n"
171
-
172
-
173
- def _append_data_field_errors(lines: list[str], field_errors: list[Any]) -> None:
174
- for error in field_errors:
175
- if isinstance(error, dict):
176
- lines.append(
177
- f"- {error.get('path') or '-'}: {error.get('message') or '-'}"
178
- f" / fix_hint={error.get('fix_hint') or '-'}"
179
- )
180
-
181
-
182
93
  def _format_whoami(result: dict[str, Any]) -> str:
183
94
  lines = [
184
95
  f"User: {result.get('nick_name') or '-'} ({result.get('email') or '-'})",
@@ -1758,11 +1669,4 @@ _FORMATTERS = {
1758
1669
  "builder_summary": _format_builder_summary,
1759
1670
  "file_upload_local": _format_file_upload_local,
1760
1671
  "feedback_submit": _format_feedback_submit,
1761
- "data_query": _format_data_result,
1762
- "data_get": _format_data_result,
1763
- "data_insert": _format_data_result,
1764
- "data_update": _format_data_result,
1765
- "data_delete": _format_data_result,
1766
1672
  }
1767
-
1768
- _DATA_FORMAT_HINTS = {"data_query", "data_get", "data_insert", "data_update", "data_delete"}
@@ -40,11 +40,6 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
40
40
  PublicToolSpec(USER_DOMAIN, "workspace_select", ("workspace_select",), ("workspace", "select")),
41
41
  PublicToolSpec(USER_DOMAIN, "app_list", ("app_list",), ("app", "list"), cli_show_effective_context=True),
42
42
  PublicToolSpec(USER_DOMAIN, "app_get", ("app_get",), ("app", "get"), cli_show_effective_context=True),
43
- PublicToolSpec(USER_DOMAIN, "data_query", ("data_query",), ("data", "query"), cli_show_effective_context=True),
44
- PublicToolSpec(USER_DOMAIN, "data_get", ("data_get",), ("data", "get"), cli_show_effective_context=True),
45
- PublicToolSpec(USER_DOMAIN, "data_insert", ("data_insert",), ("data", "insert"), cli_show_effective_context=True, cli_context_write=True),
46
- PublicToolSpec(USER_DOMAIN, "data_update", ("data_update",), ("data", "update"), cli_show_effective_context=True, cli_context_write=True),
47
- PublicToolSpec(USER_DOMAIN, "data_delete", ("data_delete",), ("data", "delete"), cli_show_effective_context=True, cli_context_write=True),
48
43
  PublicToolSpec(USER_DOMAIN, "portal_list", ("portal_list",), ("portal", "list"), cli_show_effective_context=True),
49
44
  PublicToolSpec(USER_DOMAIN, "portal_get", ("portal_get",), ("portal", "get"), cli_show_effective_context=True),
50
45
  PublicToolSpec(USER_DOMAIN, "view_get", ("view_get",), ("view", "get"), cli_show_effective_context=True),
@@ -51,9 +51,8 @@ COMMON_ERROR_DROP_TOP = {
51
51
 
52
52
  SUCCESS_POLICY_BY_TOOL: dict[str, TransformFn] = {}
53
53
 
54
- #: 这些校验命令的产出契约明确包含 ok 字段(供阶段 2 SKILL.md 引用),
54
+ #: 这两个校验命令的产出契约明确包含 ok 字段(供阶段 2 SKILL.md 引用),
55
55
  #: 需在 success 裁剪时保留 ok(默认 COMMON_SUCCESS_DROP_TOP 会丢弃 ok)。
56
- #: `builder:app_views_validate`(views 声明式校验)同属校验类工具,ok 同样进产出契约。
57
56
  _KEEP_OK_TOOLS = {
58
57
  "builder:app_form_validate_formula",
59
58
  "builder:app_form_validate_code_block",
@@ -7,7 +7,6 @@ from mcp.server.fastmcp import FastMCP
7
7
  from .backend_client import BackendClient
8
8
  from .session_store import SessionStore
9
9
  from .tools.app_tools import AppTools
10
- from .tools.data_tools import DataTools
11
10
  from .tools.auth_tools import AuthTools
12
11
  from .tools.code_block_tools import CodeBlockTools
13
12
  from .tools.feedback_tools import FeedbackTools
@@ -250,7 +249,6 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
250
249
  TaskContextTools(sessions, backend).register(server)
251
250
  RoleTools(sessions, backend).register(server)
252
251
  AppTools(sessions, backend).register(server)
253
- DataTools(sessions, backend).register(server)
254
252
  QingbiReportTools(sessions, backend).register(server)
255
253
  PackageTools(sessions, backend).register(server)
256
254
  NavigationTools(sessions, backend).register(server)
@@ -10,7 +10,6 @@ from .config import DEFAULT_PROFILE
10
10
  from .response_trim import USER_SERVER_METHOD_MAP, trim_error_response, trim_public_response, wrap_trimmed_methods
11
11
  from .session_store import SessionStore
12
12
  from .tools.app_tools import AppTools
13
- from .tools.data_tools import DataTools
14
13
  from .tools.auth_tools import AuthTools
15
14
  from .tools.code_block_tools import CodeBlockTools
16
15
  from .tools.directory_tools import DirectoryTools
@@ -243,7 +242,6 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
243
242
  backend = BackendClient()
244
243
  auth = wrap_trimmed_methods(AuthTools(sessions, backend), USER_SERVER_METHOD_MAP)
245
244
  apps = wrap_trimmed_methods(AppTools(sessions, backend), USER_SERVER_METHOD_MAP)
246
- data_tools = wrap_trimmed_methods(DataTools(sessions, backend), USER_SERVER_METHOD_MAP)
247
245
  workspace = wrap_trimmed_methods(WorkspaceTools(sessions, backend), USER_SERVER_METHOD_MAP)
248
246
  file_tools = wrap_trimmed_methods(FileTools(sessions, backend), USER_SERVER_METHOD_MAP)
249
247
  imports = wrap_trimmed_methods(ImportTools(sessions, backend), USER_SERVER_METHOD_MAP)
@@ -397,8 +395,6 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
397
395
  def app_get(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
398
396
  return apps.app_get(profile=profile, app_key=app_key)
399
397
 
400
- data_tools.register(server)
401
-
402
398
  @server.tool()
403
399
  def portal_list(profile: str = DEFAULT_PROFILE) -> dict:
404
400
  return resources.portal_list(profile=profile)
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import csv
3
4
  import html
4
5
  import mimetypes
5
6
  import json
@@ -33,7 +34,6 @@ from ..list_type_labels import (
33
34
  )
34
35
  from .base import ToolBase
35
36
  from .directory_tools import _directory_has_more, _directory_items
36
- from .csv_export import CsvShardWriter
37
37
 
38
38
 
39
39
  DEFAULT_QUERY_PAGE_SIZE = 50
@@ -2284,22 +2284,57 @@ class RecordTools(ToolBase):
2284
2284
  }
2285
2285
 
2286
2286
  run_dir = _record_access_run_dir()
2287
+ run_dir.mkdir(parents=True, exist_ok=True)
2287
2288
  header = ["record_id"] + [str(field["column_name"]) for field in fields_payload]
2288
- shard_writer = CsvShardWriter(
2289
- output_dir=run_dir,
2290
- file_prefix="records",
2291
- header=header,
2292
- shard_rows=DEFAULT_RECORD_ACCESS_SHARD_ROWS,
2293
- )
2289
+ files: list[JSONObject] = []
2290
+ shard_part = 0
2291
+ shard_rows = 0
2292
+ row_count = 0
2293
+ file_handle: Any | None = None
2294
+ writer: Any | None = None
2295
+
2296
+ def close_shard() -> None:
2297
+ nonlocal file_handle, writer, shard_rows
2298
+ if file_handle is None:
2299
+ return
2300
+ path = Path(file_handle.name)
2301
+ file_handle.close()
2302
+ files.append(
2303
+ {
2304
+ "local_path": str(path),
2305
+ "row_count": shard_rows,
2306
+ "part": len(files) + 1,
2307
+ }
2308
+ )
2309
+ file_handle = None
2310
+ writer = None
2311
+ shard_rows = 0
2312
+
2313
+ def ensure_writer() -> Any:
2314
+ nonlocal file_handle, writer, shard_part
2315
+ if writer is not None:
2316
+ return writer
2317
+ shard_part += 1
2318
+ path = run_dir / f"records-{shard_part:04d}.csv"
2319
+ file_handle = path.open("w", newline="", encoding="utf-8-sig")
2320
+ writer = csv.writer(file_handle)
2321
+ writer.writerow(header)
2322
+ return writer
2294
2323
 
2295
2324
  def write_record(apply_id: int | None, answer_list: list[JSONValue]) -> None:
2325
+ nonlocal row_count, shard_rows
2296
2326
  if apply_id is None:
2297
2327
  return
2328
+ if shard_rows >= DEFAULT_RECORD_ACCESS_SHARD_ROWS:
2329
+ close_shard()
2330
+ csv_writer = ensure_writer()
2298
2331
  values: list[str] = [_public_record_id_text(apply_id) or ""]
2299
2332
  for field in selected_fields:
2300
2333
  answer = _find_answer_for_field(answer_list, field)
2301
2334
  values.append(_record_access_csv_cell(_normalize_answer_field_value_for_output(answer, field)))
2302
- shard_writer.write(values)
2335
+ csv_writer.writerow(values)
2336
+ row_count += 1
2337
+ shard_rows += 1
2303
2338
 
2304
2339
  current_page = 1
2305
2340
  has_more = False
@@ -2339,7 +2374,7 @@ class RecordTools(ToolBase):
2339
2374
  break
2340
2375
  page = fetch_page(current_page, BACKEND_RECORD_ACCESS_PAGE_SIZE)
2341
2376
  finally:
2342
- shard_writer.close_shard()
2377
+ close_shard()
2343
2378
 
2344
2379
  if stopped_reason == "TIME_BUDGET_EXCEEDED":
2345
2380
  warnings.append(_record_access_time_budget_warning(reported_total=reported_total, fetched_pages=fetched_pages))
@@ -2364,11 +2399,11 @@ class RecordTools(ToolBase):
2364
2399
  "view_id": view_route.view_id,
2365
2400
  "format": "csv",
2366
2401
  "local_dir": str(run_dir),
2367
- "row_count": shard_writer.row_count,
2402
+ "row_count": row_count,
2368
2403
  "complete": complete,
2369
2404
  "truncated": not complete,
2370
2405
  "safe_for_final_conclusion": safe_for_final_conclusion,
2371
- "files": shard_writer.files,
2406
+ "files": files,
2372
2407
  "fields": fields_payload,
2373
2408
  "warnings": warnings,
2374
2409
  "verification": verification_payload,
@@ -1,32 +0,0 @@
1
- # Qingflow CLI 直接数据操作
2
-
3
- 直接查询、读取、新增、更新和删除分别使用:
4
-
5
- - `qingflow data query`
6
- - `qingflow data get`
7
- - `qingflow data insert`
8
- - `qingflow data update`
9
- - `qingflow data delete`
10
-
11
- 参数、JSON 文件形状和可选项必须来自当前工具 schema、当次 `--help` 或响应字段能力,不得从其他命令推测。
12
-
13
- ## 字段能力合同
14
-
15
- 1. 首次操作当前应用时,只通过只读 `data query/get` 请求 `include_schema=true`,读取同一次响应中的 `schema` 与递归 `field_capabilities`;不得用 insert/update 探测能力。
16
- 2. 构造 `where` 时,只使用目标字段 `filter_operators` 实际暴露的 `op`,并匹配对应 `value_shape`;不要在本文复制操作符矩阵。
17
- 3. 写入前优先执行不设置 `columns` 的只读 `data query` 并请求 schema,检查响应中的 `access`,再检查 `insertable`、`updatable` 与 `required_on_insert`。
18
- 4. `write_kind`、schema 与当次 `--help` 共同决定 payload 形状;运行时字段错误和权限结果优先于预检。
19
-
20
- ## 执行与恢复
21
-
22
- - 已知 `app_key` 时直接执行目标业务命令;只有应用名称时先用 `app list --query` 查找并核对 `app_name/title`,目标明确且唯一时取其 `app_key`,否则请用户确认。不能把名称传给 `--app-key`。只有业务命令明确报告会话或工作区错误后才恢复认证上下文。
23
- - 定位目标记录时先 query,再按需 get。字段只使用当前 schema 与字段能力允许的 id、能力和形状。
24
- - insert、update、delete 会改变真实业务数据。删除、覆盖已有值或批量写入前,必须确认目标和影响范围。
25
- - 返回后先检查 `write_executed`、`retry_safe`、字段错误和 hints。已执行、部分成功或状态不明时不得重放原请求。
26
- - data 命令失败时只修正响应明确指出的问题,不得切换其他数据入口绕过权限或校验。
27
-
28
- ## 任务分流
29
-
30
- - 批量导入:阅读 [批量导入 SOP](../record/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md)。
31
- - 统计分析:阅读 [数据分析说明](../record/analysis/README.md),使用 `data query --all --output csv` 并读取全部 CSV 分片。
32
- - 待办和流程动作:阅读 [任务上下文 SOP](../task/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md)。