@qingflow-tech/qingflow-cli 1.1.50 → 1.1.51

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 (45) 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 +430 -89
  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 +4 -7
  31. package/src/qingflow_mcp/cli/commands/__init__.py +1 -2
  32. package/src/qingflow_mcp/cli/commands/record.py +1 -1
  33. package/src/qingflow_mcp/cli/context.py +0 -3
  34. package/src/qingflow_mcp/cli/formatters.py +1 -97
  35. package/src/qingflow_mcp/config.py +1 -0
  36. package/src/qingflow_mcp/public_surface.py +0 -5
  37. package/src/qingflow_mcp/server.py +0 -2
  38. package/src/qingflow_mcp/server_app_user.py +0 -4
  39. package/src/qingflow_mcp/tools/base.py +42 -10
  40. package/src/qingflow_mcp/tools/record_tools.py +46 -11
  41. package/skills/qingflow-cli/reference/data/README.md +0 -32
  42. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_SPECIALIZED_WORKFLOW.md +0 -50
  43. package/src/qingflow_mcp/cli/commands/data.py +0 -134
  44. package/src/qingflow_mcp/tools/csv_export.py +0 -168
  45. package/src/qingflow_mcp/tools/data_tools.py +0 -284
@@ -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"}
@@ -26,6 +26,7 @@ DEFAULT_REPOSITORY_PROD_BRANCH = "prod"
26
26
  DEFAULT_REPOSITORY_AUTHOR_NAME = "qingflow-mcp"
27
27
  DEFAULT_REPOSITORY_AUTHOR_EMAIL = "qingflow-mcp@local.invalid"
28
28
  DEFAULT_REPOSITORY_INTERNAL_SHARE_TOKEN_KEY = "tokenKey"
29
+ DEFAULT_MEMBER_CREDIT_AVAILABILITY_PATH = "/credit/member/availability"
29
30
  DEFAULT_CREDIT_USAGE_RECORD_PATH = "/user/credit/usage"
30
31
  DEFAULT_MCPORTER_CONFIG_PATH = "~/.openclaw/workspace/config/mcporter.json"
31
32
  DEFAULT_CLOUD_AUTH_CONTEXT_TTL_SECONDS = 604800
@@ -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),
@@ -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)
@@ -8,6 +8,7 @@ from ..backend_client import BackendRequestContext, BackendClient
8
8
  from ..auth_provider import CliSessionFacade
9
9
  from ..cloud_context import current_call_platform, current_cloud_context
10
10
  from ..config import (
11
+ DEFAULT_MEMBER_CREDIT_AVAILABILITY_PATH,
11
12
  get_credit_meter_enabled,
12
13
  get_credit_usage_base_url,
13
14
  get_credit_usage_path,
@@ -19,6 +20,16 @@ from ..session_store import BackendSession, SessionProfile, SessionStore
19
20
 
20
21
  T = TypeVar("T")
21
22
  _RUN_DEPTH: ContextVar[int] = ContextVar("qingflow_mcp_tool_run_depth", default=0)
23
+ _MEMBER_CREDIT_DENIAL_MESSAGES = {
24
+ "WORKSPACE_EXHAUSTED": "当前工作区流币余额不足,请充值后再使用 MCP",
25
+ "MEMBER_MONTHLY_EXHAUSTED": "本月个人流币额度已用完,请联系工作区管理员调整额度",
26
+ "MEMBER_TOTAL_EXHAUSTED": "个人流币总额度已用完,请联系工作区管理员调整额度",
27
+ "MEMBER_MONTHLY_AND_TOTAL_EXHAUSTED": (
28
+ "本月和个人总流币额度均已用完,请联系工作区管理员调整额度"
29
+ ),
30
+ "MEMBER_QUOTA_NOT_CONFIGURED": "当前成员尚未配置流币额度,请联系工作区管理员",
31
+ "MEMBER_USAGE_QUERY_FAILED": "成员流币额度校验失败,请稍后重试",
32
+ }
22
33
 
23
34
 
24
35
  class ToolBase:
@@ -332,28 +343,49 @@ class ToolBase:
332
343
  if context.ws_id is None:
333
344
  raise_tool_error(QingflowApiError(category="payment", message="credit meter requires wsId in current session context"))
334
345
 
346
+ credit_context = BackendRequestContext(
347
+ base_url=get_credit_usage_base_url() or context.base_url,
348
+ token=context.token,
349
+ ws_id=context.ws_id,
350
+ qf_request_id=context.qf_request_id,
351
+ qf_version=context.qf_version,
352
+ qf_version_source=context.qf_version_source,
353
+ )
354
+ self._require_member_credit_available(context=credit_context)
335
355
  self._record_credit_usage(
336
356
  tool_name=tool_name,
337
- context=context,
357
+ context=credit_context,
338
358
  )
339
359
 
360
+ def _require_member_credit_available(self, *, context: BackendRequestContext) -> None:
361
+ availability = self.backend.request("GET", context, DEFAULT_MEMBER_CREDIT_AVAILABILITY_PATH)
362
+ if not isinstance(availability, dict) or not isinstance(availability.get("allowed"), bool):
363
+ raise_tool_error(QingflowApiError(
364
+ category="payment",
365
+ message="成员流币额度校验失败,请稍后重试",
366
+ ))
367
+ if availability["allowed"]:
368
+ return
369
+
370
+ deny_reason = self._normalize_text(availability.get("denyReason")) or "UNKNOWN"
371
+ raise_tool_error(QingflowApiError(
372
+ category="payment",
373
+ message=_MEMBER_CREDIT_DENIAL_MESSAGES.get(
374
+ deny_reason,
375
+ "当前成员无法使用流币,请联系工作区管理员",
376
+ ),
377
+ details={"deny_reason": deny_reason},
378
+ ))
379
+
340
380
  def _record_credit_usage(
341
381
  self,
342
382
  *,
343
383
  tool_name: str,
344
384
  context: BackendRequestContext,
345
385
  ) -> None:
346
- usage_context = BackendRequestContext(
347
- base_url=get_credit_usage_base_url() or context.base_url,
348
- token=context.token,
349
- ws_id=context.ws_id,
350
- qf_request_id=context.qf_request_id,
351
- qf_version=context.qf_version,
352
- qf_version_source=context.qf_version_source,
353
- )
354
386
  self.backend.request(
355
387
  "POST",
356
- usage_context,
388
+ context,
357
389
  get_credit_usage_path(),
358
390
  json_body={
359
391
  "skuType": "MCP",
@@ -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` 时直接执行目标业务命令;只有业务命令明确报告会话或工作区错误后才恢复认证上下文。
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)。
@@ -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,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
- )