@qingflow-tech/qingflow-cli 1.1.57 → 1.1.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/npm/lib/runtime.mjs +13 -29
- package/package.json +1 -1
- package/pyproject.toml +1 -2
- package/skills/qingflow-cli/SKILL.md +319 -90
- package/skills/qingflow-cli/manifest.yaml +1 -1
- package/skills/qingflow-cli/reference/00-INDEX.md +4 -4
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +137 -3
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +4 -6
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_FIELD_DATA_TYPES.md +14 -14
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +19 -20
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +14 -18
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md +12 -7
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +1 -2
- package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +2 -2
- package/skills/qingflow-cli/reference/builder/30-schema-fields.md +5 -9
- package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +3 -3
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +95 -76
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_EXPLORATION_REPORT.md +4 -6
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_FIELD_DATA_TYPES.md +13 -13
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +19 -20
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +14 -18
- package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +1 -2
- package/skills/qingflow-cli/reference/record/analysis/README.md +113 -21
- package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +10 -10
- package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +9 -9
- package/skills/qingflow-cli/reference/record/analysis/business-context.md +1 -1
- package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +10 -10
- package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +80 -52
- package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +2 -2
- package/skills/qingflow-common/SKILL.md +4 -7
- package/src/qingflow_mcp/app_form_compat.py +0 -3
- package/src/qingflow_mcp/app_form_validator.py +17 -69
- package/src/qingflow_mcp/builder_facade/app_form.py +100 -497
- package/src/qingflow_mcp/builder_facade/models.py +2 -2
- package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +0 -1
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +30 -64
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +10 -6
- 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 +8 -4
- 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 +10 -6
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +15 -46
- 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 +27 -61
- 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 +12 -7
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/complete.json +3 -4
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/data-permission.json +3 -3
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/summary.json +2 -3
- package/src/qingflow_mcp/builder_facade/service.py +6 -43
- package/src/qingflow_mcp/builder_facade/views_extract.py +12 -2
- package/src/qingflow_mcp/cli/commands/__init__.py +1 -2
- package/src/qingflow_mcp/cli/commands/record.py +1 -1
- package/src/qingflow_mcp/cli/context.py +0 -3
- package/src/qingflow_mcp/cli/formatters.py +1 -97
- package/src/qingflow_mcp/public_surface.py +0 -5
- package/src/qingflow_mcp/response_trim.py +1 -2
- package/src/qingflow_mcp/server.py +0 -2
- package/src/qingflow_mcp/server_app_user.py +0 -4
- package/src/qingflow_mcp/tools/record_tools.py +46 -11
- package/skills/qingflow-cli/reference/data/README.md +0 -32
- package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_SPECIALIZED_WORKFLOW.md +0 -50
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/rich_text.json +0 -146
- package/src/qingflow_mcp/cli/commands/data.py +0 -134
- package/src/qingflow_mcp/tools/csv_export.py +0 -168
- package/src/qingflow_mcp/tools/data_tools.py +0 -284
|
@@ -136,10 +136,10 @@
|
|
|
136
136
|
]
|
|
137
137
|
}
|
|
138
138
|
],
|
|
139
|
-
"description": "字段选择器;用于定位应用的数据标题、数据封面、自定义编号取值来源或其他配置引用的字段。至少提供 queId 或 name
|
|
139
|
+
"description": "字段选择器;用于定位应用的数据标题、数据封面、自定义编号取值来源或其他配置引用的字段。至少提供 queId 或 name,两者同时提供时必须指向同一字段。",
|
|
140
140
|
"properties": {
|
|
141
141
|
"name": {
|
|
142
|
-
"description": "
|
|
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": "
|
|
262
|
-
"
|
|
263
|
-
|
|
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": "
|
|
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
|
|
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=所在部门发起;
|
|
338
|
-
"x-backend": "dataScope(ALL|ONESELF|DEPT|
|
|
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
|
|
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/
|
|
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=所在部门发起;
|
|
226
|
-
"x-backend": "viewgraph.dataScope(ALL|ONESELF|DEPT|
|
|
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", "
|
|
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
|
-
|
|
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":
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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="
|
|
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
|
|
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
|
-
#:
|
|
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
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":
|
|
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":
|
|
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)。
|