@qingflow-tech/qingflow-cli 1.1.48-beta.5 → 1.1.49

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 (53) 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 -1
  5. package/skills/qingflow-cli/SKILL.md +410 -91
  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/core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
  17. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +95 -76
  18. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_EXPLORATION_REPORT.md +4 -6
  19. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_FIELD_DATA_TYPES.md +13 -13
  20. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +19 -20
  21. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +14 -18
  22. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +1 -2
  23. package/skills/qingflow-cli/reference/record/analysis/README.md +113 -21
  24. package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +10 -10
  25. package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +9 -9
  26. package/skills/qingflow-cli/reference/record/analysis/business-context.md +1 -1
  27. package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +10 -10
  28. package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +80 -52
  29. package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +2 -2
  30. package/skills/qingflow-common/SKILL.md +17 -10
  31. package/src/qingflow_mcp/builder_facade/service.py +1 -119
  32. package/src/qingflow_mcp/cli/commands/__init__.py +1 -2
  33. package/src/qingflow_mcp/cli/commands/auth.py +1 -1
  34. package/src/qingflow_mcp/cli/commands/record.py +1 -1
  35. package/src/qingflow_mcp/cli/context.py +1 -4
  36. package/src/qingflow_mcp/cli/formatters.py +4 -159
  37. package/src/qingflow_mcp/cli/main.py +1 -4
  38. package/src/qingflow_mcp/public_surface.py +0 -6
  39. package/src/qingflow_mcp/response_trim.py +3 -3
  40. package/src/qingflow_mcp/server.py +0 -2
  41. package/src/qingflow_mcp/server_app_builder.py +0 -16
  42. package/src/qingflow_mcp/server_app_user.py +3 -10
  43. package/src/qingflow_mcp/tools/ai_builder_tools.py +39 -208
  44. package/src/qingflow_mcp/tools/app_tools.py +13 -52
  45. package/src/qingflow_mcp/tools/login_link_tools.py +6 -34
  46. package/src/qingflow_mcp/tools/record_tools.py +46 -11
  47. package/src/qingflow_mcp/tools/resource_read_tools.py +0 -13
  48. package/skills/qingflow-cli/reference/data/README.md +0 -32
  49. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_LEGACY_RECORD_RETRIEVAL_WORKFLOW.md +0 -50
  50. package/src/qingflow_mcp/cli/commands/data.py +0 -134
  51. package/src/qingflow_mcp/resource_uri.py +0 -241
  52. package/src/qingflow_mcp/tools/csv_export.py +0 -168
  53. package/src/qingflow_mcp/tools/data_tools.py +0 -271
@@ -10,7 +10,6 @@ from ..config import DEFAULT_PROFILE
10
10
  from ..errors import QingflowApiError, backend_code_int, is_auth_like_error, raise_tool_error
11
11
  from ..json_types import JSONObject
12
12
  from ..list_type_labels import SYSTEM_VIEW_DEFINITIONS, get_app_publish_status_label
13
- from ..resource_uri import try_build_resource_uri
14
13
  from ..solution.compiler.icon_utils import workspace_icon_config
15
14
  from .base import ToolBase
16
15
 
@@ -47,15 +46,6 @@ class AppTools(ToolBase):
47
46
  unfiltered_count = len(items)
48
47
  if normalized_query:
49
48
  items = self._filter_visible_apps(items, normalized_query)
50
- for item in items:
51
- uri, warning = try_build_resource_uri(
52
- ws_id=session_profile.selected_ws_id,
53
- resource_type="app",
54
- app_key=item.get("app_key"),
55
- )
56
- if uri:
57
- item["uri"] = uri
58
- _append_unique_warning(warnings, warning)
59
49
  response = {
60
50
  "profile": profile,
61
51
  "ws_id": session_profile.selected_ws_id,
@@ -210,10 +200,9 @@ class AppTools(ToolBase):
210
200
  if not _is_optional_app_metadata_error(exc):
211
201
  raise
212
202
  visible_app = self._resolve_visible_app(context, app_key)
213
- if visible_app is None:
214
- raise
215
- app_name = str(visible_app.get("app_name") or visible_app.get("title") or app_key).strip() or app_key
216
- app_icon = str(visible_app.get("app_icon") or "").strip() or None
203
+ if visible_app is not None:
204
+ app_name = str(visible_app.get("app_name") or visible_app.get("title") or app_key).strip() or app_key
205
+ app_icon = str(visible_app.get("app_icon") or "").strip() or None
217
206
  warnings.append(
218
207
  _with_api_error_fields(
219
208
  {
@@ -235,45 +224,22 @@ class AppTools(ToolBase):
235
224
  accessible_views.extend(custom_views)
236
225
  import_capability, import_warnings = _derive_import_capability(base_info)
237
226
  warnings.extend(import_warnings)
238
- app_uri, uri_warning = try_build_resource_uri(
239
- ws_id=session_profile.selected_ws_id,
240
- resource_type="app",
241
- app_key=app_key,
242
- )
243
- _append_unique_warning(warnings, uri_warning)
244
- for view in accessible_views:
245
- view_id = str(view.get("view_id") or "")
246
- view_kind, separator, view_key = view_id.partition(":")
247
- if not separator:
248
- continue
249
- uri, warning = try_build_resource_uri(
250
- ws_id=session_profile.selected_ws_id,
251
- resource_type="view",
252
- app_key=app_key,
253
- view_kind=view_kind,
254
- view_key=view_key,
255
- )
256
- if uri:
257
- view["uri"] = uri
258
- _append_unique_warning(warnings, warning)
259
- data: JSONObject = {
260
- "app_key": app_key,
261
- "app_name": app_name,
262
- "app_icon": app_icon,
263
- "icon_config": workspace_icon_config(app_icon),
264
- **({"uri": app_uri} if app_uri else {}),
265
- **({"visible_app": visible_app} if base_info is None and visible_app is not None else {}),
266
- "can_create": can_create,
267
- "import_capability": import_capability,
268
- "accessible_views": accessible_views,
269
- }
270
227
  return {
271
228
  "profile": profile,
272
229
  "ws_id": session_profile.selected_ws_id,
273
230
  "ok": True,
274
231
  "request_route": {"base_url": context.base_url, "qf_version": context.qf_version},
275
232
  "warnings": warnings,
276
- "data": data,
233
+ "data": {
234
+ "app_key": app_key,
235
+ "app_name": app_name,
236
+ "app_icon": app_icon,
237
+ "icon_config": workspace_icon_config(app_icon),
238
+ **({"visible_app": visible_app} if base_info is None and visible_app is not None else {}),
239
+ "can_create": can_create,
240
+ "import_capability": import_capability,
241
+ "accessible_views": accessible_views,
242
+ },
277
243
  }
278
244
 
279
245
  return self._run(profile, runner, tool_name='应用详情')
@@ -1082,11 +1048,6 @@ def _derive_editability(base_info: Any) -> tuple[JSONObject, list[JSONObject]]:
1082
1048
  }, warnings
1083
1049
 
1084
1050
 
1085
- def _append_unique_warning(warnings: list[JSONObject], warning: JSONObject | None) -> None:
1086
- if warning and not any(item.get("code") == warning.get("code") for item in warnings):
1087
- warnings.append(warning)
1088
-
1089
-
1090
1051
  def _coerce_positive_int(value: Any) -> int | None:
1091
1052
  try:
1092
1053
  number = int(value)
@@ -6,30 +6,18 @@ from urllib.parse import parse_qs, unquote, urlencode, urlsplit
6
6
 
7
7
  from mcp.server.fastmcp import FastMCP
8
8
 
9
- from ..config import DEFAULT_PROFILE
9
+ from ..config import DEFAULT_PROFILE, get_web_base_url
10
10
  from ..errors import QingflowApiError
11
11
  from ..json_types import JSONObject
12
- from ..resource_uri import parse_resource_path, resolve_resource_web_origin
13
12
  from .base import ToolBase
14
13
 
15
14
  LOGIN_LINK_DESCRIPTION = (
16
15
  "轻流页面入口:查看页面、继续编辑、处理业务、查看结果或进入工作区(open/view/visit page in browser,login)。"
17
16
  "从 AI 对话进入页面时,使用当前 MCP 身份和工作区生成短时(约 5 分钟)一次性免登录链接。"
18
17
  "redirect_path 可选:传已确认的站内业务路径,可带 query/hash;省略进入首页,不接受外部地址或认证入口。"
19
- "用户指定页面但路径未知时先定位,不猜路径或静默改为首页。"
18
+ "纯数据查询无需调用;用户指定页面但路径未知时先定位,不猜路径或静默改为首页。"
20
19
  "返回 url 和毫秒整数 expires_at;必要安全验证及原有页面权限仍然生效。"
21
- "创建、修改或安装轻流资源后,最终回复主动提供主要结果的可点击页面入口,无需等待用户再次索要;"
22
- "采用“完成结果 + 查看入口”,文案使用“在轻流中查看应用/应用包/视图”,不默认称为“继续编辑”。"
23
- "安装方案默认提供应用包入口;直接创建或修改应用、视图时提供对应入口;用户明确要求多个主要对象时分别提供。"
24
- "使用业务结果中的稳定 `uri`,按 URI 去重,不为全部子资源预签票据;主要对象没有可靠 URI 时说明限制,不猜地址或替换为不相关页面。"
25
- "部分成功时仅交付已确认成功且可定位的主要结果,保留失败或未验证说明;"
26
- "纯数据问答、中间读取、失败资源和删除操作不自动签发,用户明确要求不提供链接时遵从。"
27
- "目标浏览器登录态未知时,临近交付用资源 URI 的同源 pathname 为每个实际展示的入口分别签发,使用返回的完整 URL,不额外追问登录态。"
28
- "只有已有用户确认同一浏览器、网页域名、账号和工作区已登录且无失效信号的事实时,才直接交付稳定 URI;"
29
- "调用过本工具或 Agent 自动化浏览器已登录不等于用户浏览器已登录,已消费链接不可复用。"
30
- "用户未要求打开时不提前打开或消费链接;要求打开时才用浏览器打开返回的完整链接。"
31
- "签发失败时说明免登录链接未生成,提供标注“需登录”的稳定 URI 作为备用入口,不重做业务操作。"
32
- "机器结果继续保留稳定 URI,临时登录链接仅用于面向用户交付。"
20
+ "用户要链接时仅交付,不提前消费;要求打开时才用浏览器打开返回的完整链接。"
33
21
  )
34
22
 
35
23
 
@@ -82,31 +70,15 @@ class LoginLinkTools(ToolBase):
82
70
  """执行工具方法逻辑。"""
83
71
 
84
72
  def runner(session_profile, context):
85
- requested_target = redirect_path or ""
86
- if requested_target.startswith("/resources"):
87
- target = requested_target
88
- try:
89
- resource = parse_resource_path(target)
90
- except ValueError:
91
- raise QingflowApiError(
92
- category="validation",
93
- message="redirect_path 必须是合法的稳定资源路径",
94
- ) from None
95
- if resource.ws_id != session_profile.selected_ws_id:
96
- raise QingflowApiError(
97
- category="validation",
98
- message="redirect_path 的资源工作区必须与当前选中工作区一致",
99
- )
100
- else:
101
- target = validate_redirect_path(requested_target)
102
- web_base_url = resolve_resource_web_origin()
73
+ target = validate_redirect_path(redirect_path)
74
+ web_base_url = get_web_base_url()
103
75
  result = self.backend.request(
104
76
  "POST",
105
77
  context,
106
78
  "/mcp/auth/login-link",
107
79
  json_body={"wsId": session_profile.selected_ws_id},
108
80
  )
109
- # 后端仅提供票据;CLI 使用 QINGFLOW_BASE_URL 对应网页环境,MCP 使用独立网页配置。
81
+ # 后端仅提供票据,网页地址由 MCP 配置确定,避免工作区域名为空。
110
82
  try:
111
83
  source_url = result.get("url") if isinstance(result, dict) else None
112
84
  if not isinstance(source_url, str):
@@ -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,
@@ -6,7 +6,6 @@ from typing import Any
6
6
  from ..errors import QingflowApiError, backend_code_int, is_auth_like_error, raise_tool_error
7
7
  from ..json_types import JSONObject
8
8
  from ..list_type_labels import SYSTEM_VIEW_DEFINITIONS
9
- from ..resource_uri import try_build_resource_uri
10
9
  from ..solution.compiler.icon_utils import workspace_icon_config
11
10
  from .app_tools import _analysis_supported_for_view_type
12
11
  from .base import ToolBase
@@ -228,17 +227,6 @@ class ResourceReadTools(ToolBase):
228
227
  "message": f"view_get supports exporting `{view_id}`, but the export call still needs an explicit `app_key` from the parent app context.",
229
228
  }
230
229
  )
231
- uri = None
232
- if resolved_app_key:
233
- uri, uri_warning = try_build_resource_uri(
234
- ws_id=session_profile.selected_ws_id,
235
- resource_type="view",
236
- app_key=resolved_app_key,
237
- view_kind="custom",
238
- view_key=view_key,
239
- )
240
- if uri_warning:
241
- warnings.append(uri_warning)
242
230
  return {
243
231
  "profile": profile,
244
232
  "ws_id": session_profile.selected_ws_id,
@@ -261,7 +249,6 @@ class ResourceReadTools(ToolBase):
261
249
  ],
262
250
  "analysis_supported": _analysis_supported_for_view_type(raw_view_type or None),
263
251
  "export_capability": export_capability,
264
- **({"uri": uri} if uri else {}),
265
252
  },
266
253
  }
267
254
 
@@ -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 文件形状和可选项必须来自当次 `--help` 或工具返回的 schema,不得根据旧 `record` 命令猜测。
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` 时直接执行目标业务命令;只有业务命令明确报告会话或工作区错误后才恢复认证上下文。
23
- - 定位目标记录时先 query,再按需 get。字段只使用当前 schema 与字段能力允许的 id、能力和形状。
24
- - insert、update、delete 会改变真实业务数据。删除、覆盖已有值或批量写入前,必须确认目标和影响范围。
25
- - 返回后先检查 `write_executed`、`retry_safe`、字段错误和 hints。已执行、部分成功或状态不明时不得重放原请求。
26
- - data 命令失败时只修正响应明确指出的问题,不得自动回退旧 `record` 写入链路绕过权限或校验。
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)。
@@ -1,50 +0,0 @@
1
- # Qingflow CLI:record 专项与显式命令参考
2
-
3
- > **适用范围**:用户明确指定 `record` 命令,或任务需要完整数据/流程日志、附件和图片上下文时使用。普通应用数据查询使用 [`data query/get`](../core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md),不得在 data 失败后自动改用本文链路。
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,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
- )