@qingflow-tech/qingflow-cli 1.1.56 → 1.1.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +4 -4
  2. package/npm/lib/runtime.mjs +13 -29
  3. package/package.json +1 -1
  4. package/pyproject.toml +1 -2
  5. package/skills/qingflow-cli/SKILL.md +319 -90
  6. package/skills/qingflow-cli/manifest.yaml +1 -1
  7. package/skills/qingflow-cli/reference/00-INDEX.md +4 -4
  8. package/skills/qingflow-cli/reference/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
  9. package/skills/qingflow-cli/reference/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +137 -3
  10. package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +4 -6
  11. package/skills/qingflow-cli/reference/QINGFLOW_CLI_FIELD_DATA_TYPES.md +14 -14
  12. package/skills/qingflow-cli/reference/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +19 -20
  13. package/skills/qingflow-cli/reference/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +14 -18
  14. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md +12 -7
  15. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +1 -2
  16. package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +2 -2
  17. package/skills/qingflow-cli/reference/builder/30-schema-fields.md +5 -9
  18. package/skills/qingflow-cli/reference/builder/50-views.md +2 -1
  19. package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +3 -3
  20. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
  21. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +95 -76
  22. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_EXPLORATION_REPORT.md +4 -6
  23. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_FIELD_DATA_TYPES.md +13 -13
  24. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +19 -20
  25. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +14 -18
  26. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +1 -2
  27. package/skills/qingflow-cli/reference/record/analysis/README.md +113 -21
  28. package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +10 -10
  29. package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +9 -9
  30. package/skills/qingflow-cli/reference/record/analysis/business-context.md +1 -1
  31. package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +10 -10
  32. package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +80 -52
  33. package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +2 -2
  34. package/skills/qingflow-common/SKILL.md +4 -7
  35. package/src/qingflow_mcp/app_form_compat.py +0 -3
  36. package/src/qingflow_mcp/app_form_validator.py +17 -69
  37. package/src/qingflow_mcp/builder_facade/app_form.py +100 -497
  38. package/src/qingflow_mcp/builder_facade/models.py +2 -2
  39. package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +0 -1
  40. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +30 -64
  41. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +8 -4
  42. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +8 -4
  43. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +10 -6
  44. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +10 -6
  45. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +18 -11
  46. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +8 -4
  47. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +8 -4
  48. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +10 -6
  49. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +8 -4
  50. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +8 -4
  51. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +8 -4
  52. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +10 -6
  53. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +8 -4
  54. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +8 -4
  55. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +8 -4
  56. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +10 -6
  57. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +15 -46
  58. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +8 -4
  59. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +8 -4
  60. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +27 -61
  61. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +8 -4
  62. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/formula.json +0 -43
  63. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +12 -7
  64. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/complete.json +35 -6
  65. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/data-permission.json +5 -5
  66. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/summary.json +5 -5
  67. package/src/qingflow_mcp/builder_facade/service.py +6 -43
  68. package/src/qingflow_mcp/builder_facade/views_extract.py +12 -2
  69. package/src/qingflow_mcp/cli/commands/__init__.py +1 -2
  70. package/src/qingflow_mcp/cli/commands/record.py +1 -1
  71. package/src/qingflow_mcp/cli/context.py +0 -3
  72. package/src/qingflow_mcp/cli/formatters.py +1 -97
  73. package/src/qingflow_mcp/public_surface.py +0 -5
  74. package/src/qingflow_mcp/response_trim.py +1 -2
  75. package/src/qingflow_mcp/server.py +0 -2
  76. package/src/qingflow_mcp/server_app_user.py +0 -4
  77. package/src/qingflow_mcp/tools/record_tools.py +46 -11
  78. package/skills/qingflow-cli/reference/data/README.md +0 -32
  79. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_SPECIALIZED_WORKFLOW.md +0 -50
  80. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/rich_text.json +0 -146
  81. package/src/qingflow_mcp/cli/commands/data.py +0 -134
  82. package/src/qingflow_mcp/tools/csv_export.py +0 -168
  83. package/src/qingflow_mcp/tools/data_tools.py +0 -284
@@ -1,284 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from datetime import datetime
4
- from pathlib import Path
5
- from typing import Any, Literal, TypedDict
6
-
7
- from mcp.server.fastmcp import FastMCP
8
-
9
- from ..backend_client import BackendRequestContext
10
- from ..config import DEFAULT_PROFILE
11
- from ..errors import QingflowApiError, raise_tool_error
12
- from ..json_types import JSONObject, JSONValue
13
- from .base import ToolBase
14
- from .csv_export import CsvPageFailure, csv_cell, export_csv_pages
15
-
16
-
17
- DATA_PAGE_SIZE = 1000
18
- DATA_CSV_SHARD_ROWS = 20_000
19
- DATA_UNBOUNDED_ROW_THRESHOLD = 50_000
20
- DATA_CSV_TIME_BUDGET_SECONDS = 55.0
21
-
22
- DataFilterOperator = Literal[
23
- "eq", "ne", "contains", "not_contains", "gt", "gte", "lt", "lte", "in", "not_in"
24
- ]
25
- DataSortDirection = Literal["asc", "desc"]
26
-
27
-
28
- class DataWhereItem(TypedDict):
29
- """数据面筛选项;in/not_in 使用数组,其余操作符使用单值。"""
30
-
31
- field_id: int
32
- op: DataFilterOperator
33
- value: JSONValue
34
-
35
-
36
- class DataSortItem(TypedDict):
37
- """数据面排序项。"""
38
-
39
- field_id: int
40
- direction: DataSortDirection
41
-
42
-
43
- class DataInsertItem(TypedDict):
44
- """数据面新增项,fields 的键为字符串形式 field_id。"""
45
-
46
- fields: dict[str, JSONValue]
47
-
48
-
49
- class DataTools(ToolBase):
50
- """视图无关的应用数据面工具。"""
51
-
52
- def register(self, mcp: FastMCP) -> None:
53
- @mcp.tool()
54
- def data_query(
55
- profile: str = DEFAULT_PROFILE,
56
- app_key: str = "",
57
- columns: list[int] | None = None,
58
- where: list[DataWhereItem] | None = None,
59
- sort: list[DataSortItem] | None = None,
60
- search: str = "",
61
- page: int = 1,
62
- page_size: int = 100,
63
- include_schema: bool = True,
64
- all_pages: bool = False,
65
- output: str = "json",
66
- ) -> JSONObject:
67
- """按字段 ID 查询应用数据;先读取 field_capabilities 再构造 where 和 sort。"""
68
- return self.data_query(
69
- profile=profile, app_key=app_key, columns=columns or [], where=where or [], sort=sort or [],
70
- search=search, page=page, page_size=page_size, include_schema=include_schema,
71
- all_pages=all_pages, output=output,
72
- )
73
-
74
- @mcp.tool()
75
- def data_get(
76
- profile: str = DEFAULT_PROFILE, app_key: str = "", record_id: str = "", include_schema: bool = True
77
- ) -> JSONObject:
78
- return self.data_get(profile=profile, app_key=app_key, record_id=record_id, include_schema=include_schema)
79
-
80
- @mcp.tool()
81
- def data_insert(
82
- profile: str = DEFAULT_PROFILE, app_key: str = "", items: list[DataInsertItem] | None = None,
83
- include_schema: bool = True,
84
- ) -> JSONObject:
85
- """新增应用数据;每项形如 {"fields": {"字段ID": 值}}。"""
86
- return self.data_insert(profile=profile, app_key=app_key, items=items or [], include_schema=include_schema)
87
-
88
- @mcp.tool()
89
- def data_update(
90
- profile: str = DEFAULT_PROFILE, app_key: str = "", record_id: str = "",
91
- fields: dict | None = None, include_schema: bool = True,
92
- ) -> JSONObject:
93
- return self.data_update(
94
- profile=profile, app_key=app_key, record_id=record_id, fields=fields or {},
95
- include_schema=include_schema,
96
- )
97
-
98
- @mcp.tool()
99
- def data_delete(
100
- profile: str = DEFAULT_PROFILE, app_key: str = "", record_ids: list[str | int] | None = None,
101
- ) -> JSONObject:
102
- return self.data_delete(profile=profile, app_key=app_key, record_ids=record_ids or [])
103
-
104
- def data_query(
105
- self, *, profile: str, app_key: str, columns: list[int], where: list[DataWhereItem],
106
- sort: list[DataSortItem],
107
- search: str = "", page: int = 1, page_size: int = 100, include_schema: bool = True,
108
- all_pages: bool = False, output: str = "json",
109
- ) -> JSONObject:
110
- self._require_app_key(app_key)
111
- if all_pages and output != "csv":
112
- raise_tool_error(QingflowApiError.config_error("all_pages 仅支持 output=csv"))
113
-
114
- def runner(session_profile, context):
115
- body: JSONObject = {
116
- "page": page, "page_size": page_size, "columns": columns, "where": where, "sort": sort,
117
- "search": search, "include_schema": include_schema,
118
- }
119
- if not all_pages:
120
- return self._attach_context(
121
- self._request_data_plane(context, "POST", self._base_path(app_key) + "/query", json_body=body),
122
- profile, session_profile.selected_ws_id,
123
- )
124
- return self._export_all(
125
- context=context, profile=profile, ws_id=session_profile.selected_ws_id, app_key=app_key,
126
- body=body, has_scope=bool(where or search),
127
- )
128
-
129
- return self._run(profile, runner, tool_name="数据面查询")
130
-
131
- def data_get(self, *, profile: str, app_key: str, record_id: str | int, include_schema: bool = True) -> JSONObject:
132
- self._require_app_key(app_key)
133
- self._require_record_id(record_id)
134
- return self._run(
135
- profile,
136
- lambda session_profile, context: self._attach_context(
137
- self._request_data_plane(
138
- context, "GET", f"{self._base_path(app_key)}/{record_id}",
139
- params={"include_schema": include_schema},
140
- ), profile, session_profile.selected_ws_id,
141
- ),
142
- tool_name="数据面单条读取",
143
- )
144
-
145
- def data_insert(
146
- self, *, profile: str, app_key: str, items: list[DataInsertItem], include_schema: bool = True
147
- ) -> JSONObject:
148
- self._require_app_key(app_key)
149
- return self._write(profile, app_key, "POST", self._base_path(app_key), {"items": items, "include_schema": include_schema}, "数据面新增")
150
-
151
- def data_update(
152
- self, *, profile: str, app_key: str, record_id: str | int, fields: dict, include_schema: bool = True
153
- ) -> JSONObject:
154
- self._require_app_key(app_key)
155
- self._require_record_id(record_id)
156
- return self._write(
157
- profile, app_key, "POST", f"{self._base_path(app_key)}/{record_id}/update",
158
- {"fields": fields, "include_schema": include_schema}, "数据面更新",
159
- )
160
-
161
- def data_delete(self, *, profile: str, app_key: str, record_ids: list[str | int]) -> JSONObject:
162
- self._require_app_key(app_key)
163
- return self._write(profile, app_key, "POST", self._base_path(app_key) + "/delete", {"record_ids": record_ids}, "数据面删除")
164
-
165
- def _write(self, profile: str, app_key: str, method: str, path: str, body: JSONObject, tool_name: str) -> JSONObject:
166
- return self._run(
167
- profile,
168
- lambda session_profile, context: self._attach_context(
169
- self._request_data_plane(context, method, path, json_body=body), profile, session_profile.selected_ws_id,
170
- ),
171
- tool_name=tool_name,
172
- )
173
-
174
- def _request_data_plane(
175
- self, context: BackendRequestContext, method: str, path: str, *, params: JSONObject | None = None,
176
- json_body: JSONValue = None,
177
- ) -> JSONObject:
178
- raw = self.backend.request(method, context, path, params=params, json_body=json_body, unwrap=False)
179
- if not isinstance(raw, dict):
180
- raise QingflowApiError(category="backend", message="数据面返回了非对象响应")
181
- code = raw.get("code")
182
- payload = raw.get("data")
183
- result: JSONObject = dict(payload) if isinstance(payload, dict) else {}
184
- success = code in (None, 0, 200, 20000) or str(code).lower() in {"success", "ok"}
185
- result.setdefault("ok", success)
186
- result.setdefault("status", "success" if success else "failed")
187
- result.setdefault("hints", [])
188
- if not success:
189
- result.setdefault("message", str(raw.get("message") or raw.get("msg") or "数据面请求失败"))
190
- result.setdefault("backend_code", code)
191
- return result
192
-
193
- def _export_all(
194
- self, *, context: BackendRequestContext, profile: str, ws_id: int | None, app_key: str,
195
- body: JSONObject, has_scope: bool,
196
- ) -> JSONObject:
197
- probe_body = dict(body)
198
- probe_body.update({"page": 1, "page_size": 1, "include_schema": True})
199
- probe = self._request_data_plane(context, "POST", self._base_path(app_key) + "/query", json_body=probe_body)
200
- if probe.get("ok") is False:
201
- # 探测请求没有产生文件,保留后端结构化错误,不把失败误报为空数据成功。
202
- return self._attach_context(probe, profile, ws_id)
203
- records = probe.get("records") if isinstance(probe.get("records"), dict) else {}
204
- total = records.get("total") if isinstance(records, dict) else None
205
- if isinstance(total, int) and total > DATA_UNBOUNDED_ROW_THRESHOLD and not has_scope:
206
- return self._attach_context({
207
- "ok": True, "status": "needs_scope", "app_key": app_key, "format": "csv", "row_count": 0,
208
- "complete": False, "truncated": True, "safe_for_final_conclusion": False, "files": [],
209
- "hints": probe.get("hints", []),
210
- "schema": probe.get("schema", {}),
211
- "field_capabilities": probe.get("field_capabilities", []),
212
- "scope": {"reported_total": total, "row_threshold": DATA_UNBOUNDED_ROW_THRESHOLD},
213
- }, profile, ws_id)
214
- schema = probe.get("schema") if isinstance(probe.get("schema"), dict) else {}
215
- fields = schema.get("fields") if isinstance(schema, dict) else None
216
- field_list = fields if isinstance(fields, list) else []
217
- business_fields = [field for field in field_list if isinstance(field, dict) and field.get("field_id") is not None]
218
- columns = [(str(field.get("field_id")), str(field.get("title") or field.get("field_id"))) for field in business_fields]
219
- header = ["record_id"] + [f"{title}__field_{field_id}" for field_id, title in columns]
220
- run_dir = Path.home() / ".qingflow" / "data-exports" / datetime.now().strftime("%Y%m%d-%H%M%S-%f")
221
- latest_hints = probe.get("hints", [])
222
- latest_schema = schema
223
- latest_capabilities = probe.get("field_capabilities", [])
224
-
225
- def fetch_page(page: int, page_size: int) -> JSONObject:
226
- nonlocal latest_hints, latest_schema, latest_capabilities
227
- request_body = dict(body)
228
- # 每页都请求 schema,使最后一页的后端原始 hints 同时准确表达完整性和字段能力。
229
- request_body.update({"page": page, "page_size": page_size, "include_schema": True})
230
- response = self._request_data_plane(
231
- context, "POST", self._base_path(app_key) + "/query", json_body=request_body
232
- )
233
- if response.get("ok") is False:
234
- raise CsvPageFailure(response)
235
- latest_hints = response.get("hints", [])
236
- if isinstance(response.get("schema"), dict):
237
- latest_schema = response["schema"]
238
- if isinstance(response.get("field_capabilities"), list):
239
- latest_capabilities = response["field_capabilities"]
240
- return response
241
-
242
- def project_row(record: JSONObject) -> list[str]:
243
- values = record.get("fields") if isinstance(record.get("fields"), dict) else record
244
- return [str(record.get("record_id") or "")] + [csv_cell(values.get(field_id)) for field_id, _ in columns]
245
-
246
- exported = export_csv_pages(
247
- fetch_page=fetch_page, header=header, project_row=project_row, output_dir=run_dir,
248
- file_prefix="records", page_size=DATA_PAGE_SIZE, shard_rows=DATA_CSV_SHARD_ROWS,
249
- time_budget_seconds=DATA_CSV_TIME_BUDGET_SECONDS,
250
- )
251
- page_error = exported.pop("page_error", None)
252
- result: JSONObject = {
253
- "ok": True, "status": "success" if exported["complete"] else "partial", "app_key": app_key,
254
- "format": "csv", "local_dir": str(run_dir), "fields": business_fields,
255
- "schema": latest_schema, "field_capabilities": latest_capabilities, "hints": latest_hints,
256
- **exported,
257
- }
258
- if isinstance(page_error, dict):
259
- # 后续页失败时保留已落盘分片,同时把后端稳定错误字段原样带回。
260
- result["ok"] = False
261
- for key in ("error_code", "message", "backend_code", "write_executed", "retry_safe", "field_errors"):
262
- if key in page_error:
263
- result[key] = page_error[key]
264
- if "hints" in page_error:
265
- result["hints"] = page_error["hints"]
266
- return self._attach_context(result, profile, ws_id)
267
-
268
- @staticmethod
269
- def _base_path(app_key: str) -> str:
270
- return f"/app/{app_key}/data/v1/records"
271
-
272
- @staticmethod
273
- def _attach_context(result: JSONObject, profile: str, ws_id: int | None) -> JSONObject:
274
- return {"profile": profile, "ws_id": ws_id, **result}
275
-
276
- @staticmethod
277
- def _require_app_key(app_key: str) -> None:
278
- if not str(app_key or "").strip():
279
- raise_tool_error(QingflowApiError.config_error("app_key is required"))
280
-
281
- @staticmethod
282
- def _require_record_id(record_id: str | int) -> None:
283
- if not str(record_id or "").strip():
284
- raise_tool_error(QingflowApiError.config_error("record_id is required"))