@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
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
# Qingflow CLI:record 日志与附件参考
|
|
2
|
-
|
|
3
|
-
> **适用范围**:任务需要完整数据/流程日志、附件或图片上下文时使用。普通应用数据查询使用 [`data query/get`](../core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md)。
|
|
4
|
-
|
|
5
|
-
## record 查询链
|
|
6
|
-
|
|
7
|
-
```text
|
|
8
|
-
app get(解析 accessible_views,选定 VIEW_ID)
|
|
9
|
-
→ record schema browse(读取该视图字段口径)
|
|
10
|
-
→ record list(样本浏览或模糊定位候选)
|
|
11
|
-
→ record get(可选:详情页首屏、附件与图片上下文)
|
|
12
|
-
→ record logs(可选:完整数据日志与流程日志)
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
## 命令形状
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
qingflow --json app get --app-key APP_KEY > tmp/qingflow_app_get.json
|
|
19
|
-
|
|
20
|
-
qingflow --json record schema browse \
|
|
21
|
-
--app-key APP_KEY --view-id VIEW_ID \
|
|
22
|
-
> tmp/qingflow_record_schema.json
|
|
23
|
-
|
|
24
|
-
qingflow --json record list \
|
|
25
|
-
--app-key APP_KEY --view-id VIEW_ID \
|
|
26
|
-
> tmp/qingflow_record_list.json
|
|
27
|
-
|
|
28
|
-
qingflow --json record get \
|
|
29
|
-
--app-key APP_KEY --record-id RECORD_ID --view-id VIEW_ID \
|
|
30
|
-
> tmp/qingflow_record_get.json
|
|
31
|
-
|
|
32
|
-
qingflow --json record logs \
|
|
33
|
-
--app-key APP_KEY --record-id RECORD_ID --view-id VIEW_ID \
|
|
34
|
-
> tmp/qingflow_record_logs.json
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
`VIEW_ID` 只能来自 `app get.accessible_views`、已经确认的前端 URL 或用户明确提供的上下文。显式视图失败时不要自动换成另一系统视图。
|
|
38
|
-
|
|
39
|
-
## 专有结果
|
|
40
|
-
|
|
41
|
-
- `record get`:用于详情页字段、首屏日志、引用、关联资源、`media_assets` 和 `file_assets`。附件与图片优先使用返回的本地路径,不直接访问远端附件 URL。
|
|
42
|
-
- `record logs`:用于完整可见数据日志和流程日志。响应只读取摘要与 JSONL 文件路径,按需筛选本地文件,不把全文放入上下文。
|
|
43
|
-
- `record list`:只在用户明确指定该命令时用于样本浏览/模糊定位,不用于完整统计结论。
|
|
44
|
-
|
|
45
|
-
## 边界
|
|
46
|
-
|
|
47
|
-
- 统计分析仍转 `data query --all --output csv -> Python/pandas`。
|
|
48
|
-
- `CUSTOM_VIEW_LIST_UNAVAILABLE` 只说明自定义视图列表读取受限;不要据此断言应用整体不可读。
|
|
49
|
-
- 待办、已办和抄送属于 `task list --task-box ...`,不使用 `record list --view-id system:todo/done/cc`。
|
|
50
|
-
- 所有大 JSON、附件清单和日志摘要必须落盘。
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$defs": {
|
|
3
|
-
"EmptyConfig": {
|
|
4
|
-
"additionalProperties": false,
|
|
5
|
-
"description": "该字段类型没有特殊配置。",
|
|
6
|
-
"type": "object"
|
|
7
|
-
},
|
|
8
|
-
"FieldCommon": {
|
|
9
|
-
"allOf": [
|
|
10
|
-
{
|
|
11
|
-
"if": {
|
|
12
|
-
"properties": {
|
|
13
|
-
"defaultType": {
|
|
14
|
-
"const": 2
|
|
15
|
-
}
|
|
16
|
-
},
|
|
17
|
-
"required": [
|
|
18
|
-
"defaultType"
|
|
19
|
-
]
|
|
20
|
-
},
|
|
21
|
-
"then": {
|
|
22
|
-
"properties": {
|
|
23
|
-
"defaultValue": {
|
|
24
|
-
"type": "null"
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
"if": {
|
|
31
|
-
"properties": {
|
|
32
|
-
"defaultType": {
|
|
33
|
-
"const": 3
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
"required": [
|
|
37
|
-
"defaultType"
|
|
38
|
-
]
|
|
39
|
-
},
|
|
40
|
-
"then": {
|
|
41
|
-
"properties": {
|
|
42
|
-
"defaultValue": {
|
|
43
|
-
"minLength": 1,
|
|
44
|
-
"type": [
|
|
45
|
-
"string",
|
|
46
|
-
"null"
|
|
47
|
-
]
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
],
|
|
53
|
-
"anyOf": [
|
|
54
|
-
{
|
|
55
|
-
"required": [
|
|
56
|
-
"queId"
|
|
57
|
-
]
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
"required": [
|
|
61
|
-
"name"
|
|
62
|
-
]
|
|
63
|
-
}
|
|
64
|
-
],
|
|
65
|
-
"description": "顶层业务字段的公共属性;字段类型由 TopField 的判别分支定义。",
|
|
66
|
-
"properties": {
|
|
67
|
-
"beingRequired": {
|
|
68
|
-
"description": "该字段是否必填;缺失时保留已有字段当前值。",
|
|
69
|
-
"type": "boolean"
|
|
70
|
-
},
|
|
71
|
-
"defaultType": {
|
|
72
|
-
"default": 1,
|
|
73
|
-
"description": "默认值类型,与 defaultValue 联动;1 表示静态值,2 表示关联/绑定值且 defaultValue 只能省略或为 null,3 表示公式值且提供 defaultValue 时必须为非空公式文本。省略 defaultType 但提供 defaultValue 时按 1 处理。",
|
|
74
|
-
"enum": [
|
|
75
|
-
1,
|
|
76
|
-
2,
|
|
77
|
-
3
|
|
78
|
-
],
|
|
79
|
-
"type": "integer"
|
|
80
|
-
},
|
|
81
|
-
"defaultValue": {
|
|
82
|
-
"$ref": "#/$defs/ScalarDefaultValue",
|
|
83
|
-
"description": "普通字段的默认值;后端 queDefaultValue 为字符串,因此数字等字面量也必须以字符串传入。defaultType 为 1 时表示静态文本,为 3 时表示公式文本,为 2 时不携带字面量默认值。表格、地址、附件、引用等结构化默认值必须放在具体字段 config.defaultValues。显式 null 表示清除默认值。"
|
|
84
|
-
},
|
|
85
|
-
"name": {
|
|
86
|
-
"description": "字段展示名称;新增字段必填,已有字段可在携带 queId 时省略;同父级允许同名(后端以 queId 为字段身份),配置引用建议携带 queId。",
|
|
87
|
-
"minLength": 1,
|
|
88
|
-
"type": "string"
|
|
89
|
-
},
|
|
90
|
-
"queId": {
|
|
91
|
-
"description": "已有字段的后端 ID;更新时用于精确定位并复用,新增字段通常省略。",
|
|
92
|
-
"minimum": 1,
|
|
93
|
-
"type": "integer"
|
|
94
|
-
},
|
|
95
|
-
"questionDescription": {
|
|
96
|
-
"description": "字段说明;null 或空字符串表示清空。",
|
|
97
|
-
"type": [
|
|
98
|
-
"string",
|
|
99
|
-
"null"
|
|
100
|
-
]
|
|
101
|
-
},
|
|
102
|
-
"questionType": {
|
|
103
|
-
"description": "字段类型;具体可用值由当前字段详情 Schema 的判别分支约束。",
|
|
104
|
-
"type": "string"
|
|
105
|
-
},
|
|
106
|
-
"width": {
|
|
107
|
-
"description": "字段在所在行的宽度百分比(1-100 整数);后端不校验总和与取值,存量数据存在 33/34 等三等分布值。",
|
|
108
|
-
"maximum": 100,
|
|
109
|
-
"minimum": 1,
|
|
110
|
-
"type": "integer"
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
"type": "object"
|
|
114
|
-
},
|
|
115
|
-
"ScalarDefaultValue": {
|
|
116
|
-
"description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。defaultType 为 3 时,通过 AppForm Schema 查询能力传入当前 schema_version 并设置 schema_kind=formula 获取公式语法、字段引用、运算符和全部函数说明;CLI 使用 builder app-form schema --schema-kind formula。需要配置结构化默认值时,传入当前 schema_version,并将字段的 questionType 作为 field_type 获取字段详细 Schema。",
|
|
117
|
-
"type": [
|
|
118
|
-
"null",
|
|
119
|
-
"string"
|
|
120
|
-
]
|
|
121
|
-
}
|
|
122
|
-
},
|
|
123
|
-
"$id": "https://builder.qingflow.com/schemas/app-form-apply/v1alpha1/fields/rich_text.json",
|
|
124
|
-
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
125
|
-
"allOf": [
|
|
126
|
-
{
|
|
127
|
-
"$ref": "#/$defs/FieldCommon"
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
"properties": {
|
|
131
|
-
"config": {
|
|
132
|
-
"$ref": "#/$defs/EmptyConfig"
|
|
133
|
-
},
|
|
134
|
-
"questionType": {
|
|
135
|
-
"const": "rich_text"
|
|
136
|
-
}
|
|
137
|
-
},
|
|
138
|
-
"required": [
|
|
139
|
-
"questionType"
|
|
140
|
-
]
|
|
141
|
-
}
|
|
142
|
-
],
|
|
143
|
-
"title": "Qingflow AppForm v1alpha1 rich_text field",
|
|
144
|
-
"type": "object",
|
|
145
|
-
"unevaluatedProperties": false
|
|
146
|
-
}
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import argparse
|
|
4
|
-
import textwrap
|
|
5
|
-
|
|
6
|
-
from ..context import CliContext
|
|
7
|
-
from .common import load_list_arg, require_list_arg, require_object_arg, raise_config_error
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
11
|
-
parser = subparsers.add_parser(
|
|
12
|
-
"data",
|
|
13
|
-
help="默认应用数据查询与直接增删改",
|
|
14
|
-
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
15
|
-
epilog=textwrap.dedent("""
|
|
16
|
-
JSON 文件约定:
|
|
17
|
-
columns.json [101, 102]
|
|
18
|
-
where.json [{"field_id": 101, "op": "eq", "value": "A"}]
|
|
19
|
-
sort.json [{"field_id": 101, "direction": "asc"}]
|
|
20
|
-
items.json [{"fields": {"101": "A"}}]
|
|
21
|
-
fields.json {"101": "B"}
|
|
22
|
-
query/get/insert/update 默认返回 field_capabilities;可用 --no-schema 关闭。
|
|
23
|
-
""").strip(),
|
|
24
|
-
)
|
|
25
|
-
commands = parser.add_subparsers(dest="data_command", required=True, metavar="{query,get,insert,update,delete}")
|
|
26
|
-
|
|
27
|
-
query = commands.add_parser(
|
|
28
|
-
"query", help="分页查询应用数据", formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
29
|
-
epilog='最小示例:qingflow data query --app-key APP_1 --where-file where.json\n'
|
|
30
|
-
'where.json: [{"field_id": 101, "op": "eq", "value": "A"}]',
|
|
31
|
-
)
|
|
32
|
-
query.add_argument("--app-key", required=True)
|
|
33
|
-
query.add_argument("--columns-file", help='JSON 数组,例如 [101, 102]')
|
|
34
|
-
query.add_argument(
|
|
35
|
-
"--where-file",
|
|
36
|
-
help='JSON 数组;项为 {"field_id":101,"op":"eq","value":"A"},in/not_in 的 value 必须为数组',
|
|
37
|
-
)
|
|
38
|
-
query.add_argument("--sort-file", help='JSON 数组,例如 [{"field_id":101,"direction":"asc"}]')
|
|
39
|
-
query.add_argument("--search", default="")
|
|
40
|
-
query.add_argument("--page", type=int, default=1)
|
|
41
|
-
query.add_argument("--page-size", type=int, default=100)
|
|
42
|
-
query.add_argument("--no-schema", action="store_false", dest="include_schema", default=True)
|
|
43
|
-
query.add_argument("--all", action="store_true", dest="all_pages")
|
|
44
|
-
query.add_argument("--output", choices=("json", "csv"), default="json")
|
|
45
|
-
query.set_defaults(handler=_handle_query, format_hint="data_query")
|
|
46
|
-
|
|
47
|
-
get = commands.add_parser("get", help="读取单条应用数据")
|
|
48
|
-
get.add_argument("--app-key", required=True)
|
|
49
|
-
get.add_argument("--record-id", required=True)
|
|
50
|
-
get.add_argument("--no-schema", action="store_false", dest="include_schema", default=True)
|
|
51
|
-
get.set_defaults(handler=_handle_get, format_hint="data_get")
|
|
52
|
-
|
|
53
|
-
insert = commands.add_parser(
|
|
54
|
-
"insert", help="新增应用数据", formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
55
|
-
epilog='最小示例:items.json = [{"fields": {"101": "A"}}]',
|
|
56
|
-
)
|
|
57
|
-
insert.add_argument("--app-key", required=True)
|
|
58
|
-
insert.add_argument("--items-file", required=True, help='JSON 数组,项为 {"fields":{"字段ID":值}}')
|
|
59
|
-
insert.add_argument("--no-schema", action="store_false", dest="include_schema", default=True)
|
|
60
|
-
insert.set_defaults(handler=_handle_insert, format_hint="data_insert")
|
|
61
|
-
|
|
62
|
-
update = commands.add_parser(
|
|
63
|
-
"update", help="更新应用数据", formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
64
|
-
epilog='最小示例:fields.json = {"101": "B"}',
|
|
65
|
-
)
|
|
66
|
-
update.add_argument("--app-key", required=True)
|
|
67
|
-
update.add_argument("--record-id", required=True)
|
|
68
|
-
update.add_argument("--fields-file", required=True, help='JSON 对象,键为字符串形式 field_id')
|
|
69
|
-
update.add_argument("--no-schema", action="store_false", dest="include_schema", default=True)
|
|
70
|
-
update.set_defaults(handler=_handle_update, format_hint="data_update")
|
|
71
|
-
|
|
72
|
-
delete = commands.add_parser("delete", help="删除应用数据")
|
|
73
|
-
delete.add_argument("--app-key", required=True)
|
|
74
|
-
delete.add_argument(
|
|
75
|
-
"--record-ids", action="append", default=[], metavar="ID[,ID...]",
|
|
76
|
-
help="待删除记录 ID;支持逗号分隔,也可重复传入",
|
|
77
|
-
)
|
|
78
|
-
delete.add_argument("--record-ids-file", help="附加的记录 ID JSON 数组文件")
|
|
79
|
-
delete.set_defaults(handler=_handle_delete, format_hint="data_delete")
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def _handle_query(args: argparse.Namespace, context: CliContext) -> dict:
|
|
83
|
-
if args.all_pages != (args.output == "csv"):
|
|
84
|
-
raise_config_error(
|
|
85
|
-
"--all 与 --output csv 必须同时使用。",
|
|
86
|
-
fix_hint="使用 `qingflow data query ... --all --output csv`,普通分页查询不要传这两个参数。",
|
|
87
|
-
)
|
|
88
|
-
return context.data.data_query(
|
|
89
|
-
profile=args.profile,
|
|
90
|
-
app_key=args.app_key,
|
|
91
|
-
columns=load_list_arg(args.columns_file, option_name="--columns-file"),
|
|
92
|
-
where=load_list_arg(args.where_file, option_name="--where-file"),
|
|
93
|
-
sort=load_list_arg(args.sort_file, option_name="--sort-file"),
|
|
94
|
-
search=args.search,
|
|
95
|
-
page=args.page,
|
|
96
|
-
page_size=args.page_size,
|
|
97
|
-
include_schema=args.include_schema,
|
|
98
|
-
all_pages=args.all_pages,
|
|
99
|
-
output=args.output,
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
|
|
104
|
-
return context.data.data_get(
|
|
105
|
-
profile=args.profile, app_key=args.app_key, record_id=args.record_id, include_schema=args.include_schema
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def _handle_insert(args: argparse.Namespace, context: CliContext) -> dict:
|
|
110
|
-
return context.data.data_insert(
|
|
111
|
-
profile=args.profile, app_key=args.app_key,
|
|
112
|
-
items=require_list_arg(args.items_file, option_name="--items-file"), include_schema=args.include_schema,
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def _handle_update(args: argparse.Namespace, context: CliContext) -> dict:
|
|
117
|
-
return context.data.data_update(
|
|
118
|
-
profile=args.profile, app_key=args.app_key, record_id=args.record_id,
|
|
119
|
-
fields=require_object_arg(args.fields_file, option_name="--fields-file"), include_schema=args.include_schema,
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def _handle_delete(args: argparse.Namespace, context: CliContext) -> dict:
|
|
124
|
-
record_ids = [item.strip() for group in args.record_ids for item in group.split(",") if item.strip()]
|
|
125
|
-
record_ids.extend(load_list_arg(args.record_ids_file, option_name="--record-ids-file"))
|
|
126
|
-
if not record_ids:
|
|
127
|
-
raise_config_error(
|
|
128
|
-
"至少传入一个记录 ID。",
|
|
129
|
-
fix_hint="使用 `--record-ids ID[,ID...]`;也可附加 `--record-ids-file ids.json`。",
|
|
130
|
-
)
|
|
131
|
-
return context.data.data_delete(
|
|
132
|
-
profile=args.profile, app_key=args.app_key,
|
|
133
|
-
record_ids=record_ids,
|
|
134
|
-
)
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import csv
|
|
4
|
-
import json
|
|
5
|
-
import time
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import Any, Callable
|
|
8
|
-
|
|
9
|
-
from ..json_types import JSONObject
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
PageFetcher = Callable[[int, int], JSONObject]
|
|
13
|
-
RowProjector = Callable[[JSONObject], list[str]]
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class CsvPageFailure(RuntimeError):
|
|
17
|
-
"""后续分页返回结构化业务错误;已写文件保留为不完整结果。"""
|
|
18
|
-
|
|
19
|
-
def __init__(self, payload: JSONObject) -> None:
|
|
20
|
-
super().__init__(str(payload.get("message") or "分页请求失败"))
|
|
21
|
-
self.payload = payload
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class CsvShardWriter:
|
|
25
|
-
"""视图无关的 UTF-8 BOM CSV 分片写入器。"""
|
|
26
|
-
|
|
27
|
-
def __init__(self, *, output_dir: Path, file_prefix: str, header: list[str], shard_rows: int) -> None:
|
|
28
|
-
self.output_dir = output_dir
|
|
29
|
-
self.file_prefix = file_prefix
|
|
30
|
-
self.header = header
|
|
31
|
-
self.shard_rows = shard_rows
|
|
32
|
-
self.files: list[JSONObject] = []
|
|
33
|
-
self.row_count = 0
|
|
34
|
-
self._part = 0
|
|
35
|
-
self._current_rows = 0
|
|
36
|
-
self._file: Any | None = None
|
|
37
|
-
self._writer: Any | None = None
|
|
38
|
-
output_dir.mkdir(parents=True, exist_ok=True)
|
|
39
|
-
|
|
40
|
-
def write(self, values: list[str]) -> None:
|
|
41
|
-
if self._current_rows >= self.shard_rows:
|
|
42
|
-
self.close_shard()
|
|
43
|
-
self._ensure_writer().writerow(values)
|
|
44
|
-
self._current_rows += 1
|
|
45
|
-
self.row_count += 1
|
|
46
|
-
|
|
47
|
-
def close_shard(self) -> None:
|
|
48
|
-
if self._file is None:
|
|
49
|
-
return
|
|
50
|
-
path = Path(self._file.name)
|
|
51
|
-
self._file.close()
|
|
52
|
-
self.files.append({"local_path": str(path), "row_count": self._current_rows, "part": len(self.files) + 1})
|
|
53
|
-
self._file = None
|
|
54
|
-
self._writer = None
|
|
55
|
-
self._current_rows = 0
|
|
56
|
-
|
|
57
|
-
def _ensure_writer(self) -> Any:
|
|
58
|
-
if self._writer is not None:
|
|
59
|
-
return self._writer
|
|
60
|
-
self._part += 1
|
|
61
|
-
path = self.output_dir / f"{self.file_prefix}-{self._part:04d}.csv"
|
|
62
|
-
self._file = path.open("w", newline="", encoding="utf-8-sig")
|
|
63
|
-
self._writer = csv.writer(self._file)
|
|
64
|
-
self._writer.writerow(self.header)
|
|
65
|
-
return self._writer
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def export_csv_pages(
|
|
69
|
-
*,
|
|
70
|
-
fetch_page: PageFetcher,
|
|
71
|
-
header: list[str],
|
|
72
|
-
project_row: RowProjector,
|
|
73
|
-
output_dir: Path,
|
|
74
|
-
file_prefix: str,
|
|
75
|
-
page_size: int,
|
|
76
|
-
shard_rows: int,
|
|
77
|
-
time_budget_seconds: float,
|
|
78
|
-
) -> JSONObject:
|
|
79
|
-
"""执行与业务权限无关的分页、CSV 分片和时间预算控制。"""
|
|
80
|
-
output_dir.mkdir(parents=True, exist_ok=True)
|
|
81
|
-
deadline = time.monotonic() + time_budget_seconds
|
|
82
|
-
files: list[JSONObject] = []
|
|
83
|
-
row_count = 0
|
|
84
|
-
fetched_pages = 0
|
|
85
|
-
current_page = 1
|
|
86
|
-
file_handle: Any | None = None
|
|
87
|
-
writer: Any | None = None
|
|
88
|
-
current_rows = 0
|
|
89
|
-
part = 0
|
|
90
|
-
has_more = False
|
|
91
|
-
stopped_reason: str | None = None
|
|
92
|
-
page_error: JSONObject | None = None
|
|
93
|
-
|
|
94
|
-
def close_file() -> None:
|
|
95
|
-
nonlocal file_handle, writer, current_rows
|
|
96
|
-
if file_handle is None:
|
|
97
|
-
return
|
|
98
|
-
path = Path(file_handle.name)
|
|
99
|
-
file_handle.close()
|
|
100
|
-
files.append({"local_path": str(path), "row_count": current_rows, "part": len(files) + 1})
|
|
101
|
-
file_handle = None
|
|
102
|
-
writer = None
|
|
103
|
-
current_rows = 0
|
|
104
|
-
|
|
105
|
-
def get_writer() -> Any:
|
|
106
|
-
nonlocal file_handle, writer, part
|
|
107
|
-
if writer is not None:
|
|
108
|
-
return writer
|
|
109
|
-
part += 1
|
|
110
|
-
path = output_dir / f"{file_prefix}-{part:04d}.csv"
|
|
111
|
-
file_handle = path.open("w", newline="", encoding="utf-8-sig")
|
|
112
|
-
writer = csv.writer(file_handle)
|
|
113
|
-
writer.writerow(header)
|
|
114
|
-
return writer
|
|
115
|
-
|
|
116
|
-
try:
|
|
117
|
-
while True:
|
|
118
|
-
page = fetch_page(current_page, page_size)
|
|
119
|
-
records = page.get("records") if isinstance(page, dict) else None
|
|
120
|
-
container = records if isinstance(records, dict) else page
|
|
121
|
-
raw_items = container.get("items", container.get("list", [])) if isinstance(container, dict) else []
|
|
122
|
-
items = raw_items if isinstance(raw_items, list) else []
|
|
123
|
-
has_more = bool(container.get("has_more")) if isinstance(container, dict) else False
|
|
124
|
-
for item in items:
|
|
125
|
-
if not isinstance(item, dict):
|
|
126
|
-
continue
|
|
127
|
-
if current_rows >= shard_rows:
|
|
128
|
-
close_file()
|
|
129
|
-
get_writer().writerow(project_row(item))
|
|
130
|
-
current_rows += 1
|
|
131
|
-
row_count += 1
|
|
132
|
-
fetched_pages += 1
|
|
133
|
-
if not has_more:
|
|
134
|
-
break
|
|
135
|
-
if time.monotonic() >= deadline:
|
|
136
|
-
stopped_reason = "TIME_BUDGET_EXCEEDED"
|
|
137
|
-
break
|
|
138
|
-
current_page += 1
|
|
139
|
-
except CsvPageFailure as exception:
|
|
140
|
-
stopped_reason = "PAGE_FETCH_FAILED"
|
|
141
|
-
page_error = exception.payload
|
|
142
|
-
except Exception:
|
|
143
|
-
# 已落盘分片仍是有效的部分结果;调用方据此禁止形成完整性结论。
|
|
144
|
-
stopped_reason = "PAGE_FETCH_FAILED"
|
|
145
|
-
finally:
|
|
146
|
-
close_file()
|
|
147
|
-
|
|
148
|
-
complete = stopped_reason is None and not has_more
|
|
149
|
-
result: JSONObject = {
|
|
150
|
-
"files": files,
|
|
151
|
-
"row_count": row_count,
|
|
152
|
-
"complete": complete,
|
|
153
|
-
"truncated": not complete,
|
|
154
|
-
"safe_for_final_conclusion": complete,
|
|
155
|
-
"fetched_pages": fetched_pages,
|
|
156
|
-
"stopped_reason": stopped_reason,
|
|
157
|
-
}
|
|
158
|
-
if page_error is not None:
|
|
159
|
-
result["page_error"] = page_error
|
|
160
|
-
return result
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def csv_cell(value: Any) -> str:
|
|
164
|
-
if value is None:
|
|
165
|
-
return ""
|
|
166
|
-
if isinstance(value, (dict, list)):
|
|
167
|
-
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
|
|
168
|
-
return str(value)
|