@josephyan/qingflow-app-user-mcp 1.0.5 → 1.0.7
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 +2 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-record-analysis/SKILL.md +2 -2
- package/skills/qingflow-record-analysis/references/analysis-gotchas.md +2 -1
- package/skills/qingflow-record-analysis/references/analysis-patterns.md +4 -3
- package/src/qingflow_mcp/response_trim.py +2 -0
- package/src/qingflow_mcp/server_app_user.py +1 -1
- package/src/qingflow_mcp/tools/record_tools.py +161 -81
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @josephyan/qingflow-app-user-mcp@1.0.
|
|
6
|
+
npm install @josephyan/qingflow-app-user-mcp@1.0.7
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @josephyan/qingflow-app-user-mcp@1.0.
|
|
12
|
+
npx -y -p @josephyan/qingflow-app-user-mcp@1.0.7 qingflow-app-user-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -73,9 +73,9 @@ Use `record_access` to fetch detail rows into local CSV shards. It does not anal
|
|
|
73
73
|
}
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
Then run Python against every `files[].local_path`. CSV columns are stable: `record_id`, then `field_<field_id>`. Use `fields[]`
|
|
76
|
+
Then run Python against every `files[].local_path`. CSV columns are stable: `record_id`, then `field_<field_id>`. Use `fields[]` plus `metadata_files.schema` / `metadata_files.readme` in the same output directory to map titles and types, especially when reusing the CSV later.
|
|
77
77
|
|
|
78
|
-
- Never ask for `page`, `page_size`, `limit`, or `max_rows`; `record_access` owns paging internally.
|
|
78
|
+
- Never ask for `page`, `page_size`, `limit`, or `max_rows`; `record_access` owns paging internally and follows the backend's native paging capability.
|
|
79
79
|
- If multiple CSV files are returned, read them all.
|
|
80
80
|
- If `complete=false` or `safe_for_final_conclusion=false`, downgrade the answer and disclose the limitation.
|
|
81
81
|
- `record_export_direct` is only for explicit export/download/Excel requests, not default analysis.
|
|
@@ -89,11 +89,12 @@ Examples of the right recovery question:
|
|
|
89
89
|
|
|
90
90
|
## Do not try to control paging manually
|
|
91
91
|
|
|
92
|
-
`record_access` and `record_analyze`
|
|
92
|
+
`record_access` hides paging and follows the backend's native paging capability. `record_analyze` hides paging and scan budget on purpose.
|
|
93
93
|
|
|
94
94
|
- Do not invent `page_size`
|
|
95
95
|
- Do not invent `requested_pages`
|
|
96
96
|
- Do not invent `scan_max_pages`
|
|
97
|
+
- Do not rename CSV columns before analysis; use `metadata_files.schema` / `metadata_files.readme` to interpret `field_<id>` columns.
|
|
97
98
|
- Do not invent `auto_expand_pages`
|
|
98
99
|
- Do not invent `max_rows`
|
|
99
100
|
|
|
@@ -28,9 +28,10 @@ Result reading order:
|
|
|
28
28
|
1. `record_access.complete`
|
|
29
29
|
2. `record_access.safe_for_final_conclusion`
|
|
30
30
|
3. `record_access.files[].local_path`
|
|
31
|
-
4.
|
|
32
|
-
5.
|
|
33
|
-
6. `record_access.
|
|
31
|
+
4. `record_access.metadata_files.schema`
|
|
32
|
+
5. Python outputs
|
|
33
|
+
6. `record_access.fields`
|
|
34
|
+
7. `record_access.warnings`
|
|
34
35
|
|
|
35
36
|
Treat `record_browse_schema_get` as the browse-schema source of truth. Missing fields are permission boundaries, not invitations to guess hidden ids.
|
|
36
37
|
|
|
@@ -455,11 +455,13 @@ def _trim_record_access(payload: JSONObject) -> None:
|
|
|
455
455
|
"app_key",
|
|
456
456
|
"view_id",
|
|
457
457
|
"format",
|
|
458
|
+
"local_dir",
|
|
458
459
|
"row_count",
|
|
459
460
|
"complete",
|
|
460
461
|
"truncated",
|
|
461
462
|
"safe_for_final_conclusion",
|
|
462
463
|
"files",
|
|
464
|
+
"metadata_files",
|
|
463
465
|
"fields",
|
|
464
466
|
"warnings",
|
|
465
467
|
"verification",
|
|
@@ -74,7 +74,7 @@ Inside `optional_fields`, any field with `may_become_required=true` is still wri
|
|
|
74
74
|
|
|
75
75
|
Prefer `view_id` entries from `accessible_views` where `analysis_supported=true`.
|
|
76
76
|
|
|
77
|
-
Use `record_access` to write local CSV shard files, then use Python to compute counts, rankings, ratios, trends, and final conclusions. `record_access` does not return bulk `items`; read `files[].local_path`.
|
|
77
|
+
Use `record_access` to write local CSV shard files, then use Python to compute counts, rankings, ratios, trends, and final conclusions. `record_access` does not return bulk `items`; read `files[].local_path`. Use `fields[]` and the same-directory `metadata_files.schema` / `metadata_files.readme` to map stable `field_<id>` CSV columns back to titles and types.
|
|
78
78
|
Use `chart_get` only when the user provides a report URL / chart_id or explicitly asks to read an existing report. Do not use QingBI as the default analysis route.
|
|
79
79
|
|
|
80
80
|
Use this data-access DSL shape:
|
|
@@ -32,9 +32,8 @@ from .directory_tools import _directory_has_more, _directory_items
|
|
|
32
32
|
|
|
33
33
|
DEFAULT_QUERY_PAGE_SIZE = 50
|
|
34
34
|
DEFAULT_LIST_PAGE_SIZE = 200
|
|
35
|
+
BACKEND_RECORD_ACCESS_PAGE_SIZE = 1000
|
|
35
36
|
DEFAULT_RECORD_ACCESS_SHARD_ROWS = 5000
|
|
36
|
-
DEFAULT_RECORD_ACCESS_HARD_ROWS = 50000
|
|
37
|
-
DEFAULT_RECORD_ACCESS_TIMEOUT_SECONDS = 60
|
|
38
37
|
DEFAULT_ANALYSIS_PAGE_SIZE = 1000
|
|
39
38
|
DEFAULT_SCAN_MAX_PAGES = 10
|
|
40
39
|
DEFAULT_ANALYSIS_SCAN_MAX_PAGES = 100
|
|
@@ -1870,7 +1869,6 @@ class RecordTools(ToolBase):
|
|
|
1870
1869
|
primary_search_que_ids = primary_search_que_ids or None
|
|
1871
1870
|
match_rules = self._resolve_match_rules(context, filters, index)
|
|
1872
1871
|
sort_rules = self._resolve_sorts(sorts, index)
|
|
1873
|
-
dept_member_cache: dict[int, set[int]] = {}
|
|
1874
1872
|
used_list_type: int | None = None
|
|
1875
1873
|
fallback_list_types = (
|
|
1876
1874
|
[view_route.list_type if view_route.list_type is not None else DEFAULT_RECORD_LIST_TYPE]
|
|
@@ -1934,17 +1932,9 @@ class RecordTools(ToolBase):
|
|
|
1934
1932
|
current_page = 1
|
|
1935
1933
|
has_more = False
|
|
1936
1934
|
reported_total: int | None = None
|
|
1937
|
-
|
|
1938
|
-
timeout_reached = False
|
|
1939
|
-
started_at = time.perf_counter()
|
|
1935
|
+
created_at = datetime.now(UTC).isoformat()
|
|
1940
1936
|
try:
|
|
1941
1937
|
while True:
|
|
1942
|
-
if row_count >= DEFAULT_RECORD_ACCESS_HARD_ROWS:
|
|
1943
|
-
limit_reached = True
|
|
1944
|
-
break
|
|
1945
|
-
if time.perf_counter() - started_at >= DEFAULT_RECORD_ACCESS_TIMEOUT_SECONDS:
|
|
1946
|
-
timeout_reached = True
|
|
1947
|
-
break
|
|
1948
1938
|
if used_list_type is None:
|
|
1949
1939
|
last_error: QingflowApiError | None = None
|
|
1950
1940
|
page: JSONObject | None = None
|
|
@@ -1955,7 +1945,7 @@ class RecordTools(ToolBase):
|
|
|
1955
1945
|
app_key=app_key,
|
|
1956
1946
|
view_selection=view_selection,
|
|
1957
1947
|
page_num=current_page,
|
|
1958
|
-
page_size=
|
|
1948
|
+
page_size=BACKEND_RECORD_ACCESS_PAGE_SIZE,
|
|
1959
1949
|
query_key=None,
|
|
1960
1950
|
match_rules=match_rules,
|
|
1961
1951
|
sorts=sort_rules,
|
|
@@ -1982,7 +1972,7 @@ class RecordTools(ToolBase):
|
|
|
1982
1972
|
app_key=app_key,
|
|
1983
1973
|
view_selection=view_selection,
|
|
1984
1974
|
page_num=current_page,
|
|
1985
|
-
page_size=
|
|
1975
|
+
page_size=BACKEND_RECORD_ACCESS_PAGE_SIZE,
|
|
1986
1976
|
query_key=None,
|
|
1987
1977
|
match_rules=match_rules,
|
|
1988
1978
|
sorts=sort_rules,
|
|
@@ -1992,25 +1982,15 @@ class RecordTools(ToolBase):
|
|
|
1992
1982
|
page_rows = page.get("list")
|
|
1993
1983
|
items = page_rows if isinstance(page_rows, list) else []
|
|
1994
1984
|
if reported_total is None:
|
|
1995
|
-
reported_total = _effective_total(page,
|
|
1996
|
-
has_more = _page_has_more(page, current_page,
|
|
1985
|
+
reported_total = _effective_total(page, BACKEND_RECORD_ACCESS_PAGE_SIZE)
|
|
1986
|
+
has_more = _page_has_more(page, current_page, BACKEND_RECORD_ACCESS_PAGE_SIZE, len(items))
|
|
1997
1987
|
page_apply_order: list[int] = []
|
|
1998
1988
|
page_answer_map: dict[int, list[JSONValue]] = {}
|
|
1999
1989
|
for item in items:
|
|
2000
|
-
if row_count + len(page_apply_order) >= DEFAULT_RECORD_ACCESS_HARD_ROWS:
|
|
2001
|
-
limit_reached = True
|
|
2002
|
-
break
|
|
2003
1990
|
if not isinstance(item, dict):
|
|
2004
1991
|
continue
|
|
2005
1992
|
answers = item.get("answers")
|
|
2006
1993
|
answer_list = answers if isinstance(answers, list) else []
|
|
2007
|
-
if not self._matches_view_selection(
|
|
2008
|
-
context,
|
|
2009
|
-
answer_list,
|
|
2010
|
-
view_selection=view_selection,
|
|
2011
|
-
dept_member_cache=dept_member_cache,
|
|
2012
|
-
):
|
|
2013
|
-
continue
|
|
2014
1994
|
apply_id = _coerce_count(item.get("applyId")) or _coerce_count(item.get("id"))
|
|
2015
1995
|
if apply_id is None:
|
|
2016
1996
|
continue
|
|
@@ -2023,7 +2003,7 @@ class RecordTools(ToolBase):
|
|
|
2023
2003
|
app_key=app_key,
|
|
2024
2004
|
view_selection=view_selection,
|
|
2025
2005
|
page_num=current_page,
|
|
2026
|
-
page_size=
|
|
2006
|
+
page_size=BACKEND_RECORD_ACCESS_PAGE_SIZE,
|
|
2027
2007
|
query_key=None,
|
|
2028
2008
|
match_rules=match_rules,
|
|
2029
2009
|
sorts=sort_rules,
|
|
@@ -2045,11 +2025,8 @@ class RecordTools(ToolBase):
|
|
|
2045
2025
|
cast(list[JSONValue], extra_answer_list),
|
|
2046
2026
|
)
|
|
2047
2027
|
for apply_id in page_apply_order:
|
|
2048
|
-
if row_count >= DEFAULT_RECORD_ACCESS_HARD_ROWS:
|
|
2049
|
-
limit_reached = True
|
|
2050
|
-
break
|
|
2051
2028
|
write_record(apply_id, page_answer_map.get(apply_id, []))
|
|
2052
|
-
if
|
|
2029
|
+
if not has_more:
|
|
2053
2030
|
break
|
|
2054
2031
|
current_page += 1
|
|
2055
2032
|
finally:
|
|
@@ -2069,24 +2046,29 @@ class RecordTools(ToolBase):
|
|
|
2069
2046
|
),
|
|
2070
2047
|
}
|
|
2071
2048
|
)
|
|
2072
|
-
|
|
2073
|
-
warnings.append(
|
|
2074
|
-
{
|
|
2075
|
-
"code": "RECORD_ACCESS_INTERNAL_LIMIT_REACHED",
|
|
2076
|
-
"message": "record_access reached its internal row limit before all pages were fetched.",
|
|
2077
|
-
}
|
|
2078
|
-
)
|
|
2079
|
-
if timeout_reached:
|
|
2080
|
-
warnings.append(
|
|
2081
|
-
{
|
|
2082
|
-
"code": "RECORD_ACCESS_TIMEOUT",
|
|
2083
|
-
"message": "record_access reached its internal time limit before all pages were fetched.",
|
|
2084
|
-
}
|
|
2085
|
-
)
|
|
2086
|
-
complete = not has_more and not limit_reached and not timeout_reached
|
|
2049
|
+
complete = not has_more
|
|
2087
2050
|
safe_for_final_conclusion = complete and not any(
|
|
2088
2051
|
warning.get("code") == "CUSTOM_VIEW_FILTER_UNVERIFIED" for warning in warnings
|
|
2089
2052
|
)
|
|
2053
|
+
fields_payload = [_record_access_field_payload(field) for field in selected_fields]
|
|
2054
|
+
verification_payload: JSONObject = {
|
|
2055
|
+
**_view_filter_verification_payload(view_route),
|
|
2056
|
+
"reported_total": reported_total,
|
|
2057
|
+
"list_type_used": used_list_type,
|
|
2058
|
+
}
|
|
2059
|
+
metadata_files = _write_record_access_metadata_files(
|
|
2060
|
+
run_dir=run_dir,
|
|
2061
|
+
created_at=created_at,
|
|
2062
|
+
app_key=app_key,
|
|
2063
|
+
view_route=view_route,
|
|
2064
|
+
row_count=row_count,
|
|
2065
|
+
complete=complete,
|
|
2066
|
+
safe_for_final_conclusion=safe_for_final_conclusion,
|
|
2067
|
+
files=files,
|
|
2068
|
+
fields=fields_payload,
|
|
2069
|
+
warnings=warnings,
|
|
2070
|
+
verification=verification_payload,
|
|
2071
|
+
)
|
|
2090
2072
|
return {
|
|
2091
2073
|
"profile": profile,
|
|
2092
2074
|
"ws_id": session_profile.selected_ws_id,
|
|
@@ -2095,18 +2077,16 @@ class RecordTools(ToolBase):
|
|
|
2095
2077
|
"app_key": app_key,
|
|
2096
2078
|
"view_id": view_route.view_id,
|
|
2097
2079
|
"format": "csv",
|
|
2080
|
+
"local_dir": str(run_dir),
|
|
2098
2081
|
"row_count": row_count,
|
|
2099
2082
|
"complete": complete,
|
|
2100
2083
|
"truncated": not complete,
|
|
2101
2084
|
"safe_for_final_conclusion": safe_for_final_conclusion,
|
|
2102
2085
|
"files": files,
|
|
2103
|
-
"
|
|
2086
|
+
"metadata_files": metadata_files,
|
|
2087
|
+
"fields": fields_payload,
|
|
2104
2088
|
"warnings": warnings,
|
|
2105
|
-
"verification":
|
|
2106
|
-
**_view_filter_verification_payload(view_route),
|
|
2107
|
-
"reported_total": reported_total,
|
|
2108
|
-
"list_type_used": used_list_type,
|
|
2109
|
-
},
|
|
2089
|
+
"verification": verification_payload,
|
|
2110
2090
|
"request_route": self._request_route_payload(context),
|
|
2111
2091
|
}
|
|
2112
2092
|
|
|
@@ -5594,9 +5574,8 @@ class RecordTools(ToolBase):
|
|
|
5594
5574
|
source_pages: list[int] = []
|
|
5595
5575
|
result_amount: int | None = None
|
|
5596
5576
|
has_more = False
|
|
5597
|
-
dept_member_cache: dict[int, set[int]] = {}
|
|
5598
5577
|
view_selection = resolved_view.view_selection
|
|
5599
|
-
local_filtering = bool(filters)
|
|
5578
|
+
local_filtering = bool(filters)
|
|
5600
5579
|
group_stats: dict[tuple[tuple[str, object], ...], JSONObject] = {}
|
|
5601
5580
|
overall_metrics = self._initialize_metric_states(metrics)
|
|
5602
5581
|
matched_rows = 0
|
|
@@ -5640,13 +5619,6 @@ class RecordTools(ToolBase):
|
|
|
5640
5619
|
continue
|
|
5641
5620
|
answers = item.get("answers")
|
|
5642
5621
|
answer_list = answers if isinstance(answers, list) else []
|
|
5643
|
-
if not self._matches_view_selection(
|
|
5644
|
-
context,
|
|
5645
|
-
answer_list,
|
|
5646
|
-
view_selection=view_selection,
|
|
5647
|
-
dept_member_cache=dept_member_cache,
|
|
5648
|
-
):
|
|
5649
|
-
continue
|
|
5650
5622
|
if not self._matches_analyze_filters(answer_list, filters):
|
|
5651
5623
|
continue
|
|
5652
5624
|
matched_rows += 1
|
|
@@ -6732,7 +6704,6 @@ class RecordTools(ToolBase):
|
|
|
6732
6704
|
if view_selection is not None
|
|
6733
6705
|
else self._get_field_index(profile, context, app_key, force_refresh=False)
|
|
6734
6706
|
)
|
|
6735
|
-
dept_member_cache: dict[int, set[int]] = {}
|
|
6736
6707
|
result = self._search_page(
|
|
6737
6708
|
context,
|
|
6738
6709
|
app_key=app_key,
|
|
@@ -6747,17 +6718,7 @@ class RecordTools(ToolBase):
|
|
|
6747
6718
|
)
|
|
6748
6719
|
rows = result.get("list")
|
|
6749
6720
|
raw_rows = rows if isinstance(rows, list) else []
|
|
6750
|
-
filtered_rows = [
|
|
6751
|
-
item
|
|
6752
|
-
for item in raw_rows
|
|
6753
|
-
if isinstance(item, dict)
|
|
6754
|
-
and self._matches_view_selection(
|
|
6755
|
-
context,
|
|
6756
|
-
item.get("answers") if isinstance(item.get("answers"), list) else [],
|
|
6757
|
-
view_selection=view_selection,
|
|
6758
|
-
dept_member_cache=dept_member_cache,
|
|
6759
|
-
)
|
|
6760
|
-
]
|
|
6721
|
+
filtered_rows = [item for item in raw_rows if isinstance(item, dict)]
|
|
6761
6722
|
if isinstance(rows, list):
|
|
6762
6723
|
result = dict(result)
|
|
6763
6724
|
result["list"] = filtered_rows
|
|
@@ -7007,7 +6968,6 @@ class RecordTools(ToolBase):
|
|
|
7007
6968
|
def runner(session_profile, context):
|
|
7008
6969
|
index = self._get_field_index(profile, context, app_key, force_refresh=False)
|
|
7009
6970
|
view_selection = self._resolve_view_selection(profile, context, app_key, view_key=view_key, view_name=view_name)
|
|
7010
|
-
dept_member_cache: dict[int, set[int]] = {}
|
|
7011
6971
|
resolved_column_cap = _bounded_column_limit(
|
|
7012
6972
|
max_columns,
|
|
7013
6973
|
default_limit=MAX_LIST_COLUMN_LIMIT,
|
|
@@ -7113,13 +7073,6 @@ class RecordTools(ToolBase):
|
|
|
7113
7073
|
continue
|
|
7114
7074
|
answers = item.get("answers")
|
|
7115
7075
|
answer_list = answers if isinstance(answers, list) else []
|
|
7116
|
-
if not self._matches_view_selection(
|
|
7117
|
-
context,
|
|
7118
|
-
answer_list,
|
|
7119
|
-
view_selection=view_selection,
|
|
7120
|
-
dept_member_cache=dept_member_cache,
|
|
7121
|
-
):
|
|
7122
|
-
continue
|
|
7123
7076
|
matched_records += 1
|
|
7124
7077
|
apply_id = _coerce_count(item.get("applyId")) or _coerce_count(item.get("id"))
|
|
7125
7078
|
row = _build_flat_row(answer_list, selected_fields_from_primary, apply_id=apply_id)
|
|
@@ -11568,6 +11521,133 @@ def _record_access_field_payload(field: FormField) -> JSONObject:
|
|
|
11568
11521
|
}
|
|
11569
11522
|
|
|
11570
11523
|
|
|
11524
|
+
def _write_record_access_metadata_files(
|
|
11525
|
+
*,
|
|
11526
|
+
run_dir: Path,
|
|
11527
|
+
created_at: str,
|
|
11528
|
+
app_key: str,
|
|
11529
|
+
view_route: AccessibleViewRoute,
|
|
11530
|
+
row_count: int,
|
|
11531
|
+
complete: bool,
|
|
11532
|
+
safe_for_final_conclusion: bool,
|
|
11533
|
+
files: list[JSONObject],
|
|
11534
|
+
fields: list[JSONObject],
|
|
11535
|
+
warnings: list[JSONObject],
|
|
11536
|
+
verification: JSONObject,
|
|
11537
|
+
) -> JSONObject:
|
|
11538
|
+
schema_path = run_dir / "schema.json"
|
|
11539
|
+
readme_path = run_dir / "README.md"
|
|
11540
|
+
schema_payload: JSONObject = {
|
|
11541
|
+
"created_at": created_at,
|
|
11542
|
+
"app_key": app_key,
|
|
11543
|
+
"view_id": view_route.view_id,
|
|
11544
|
+
"view_name": view_route.name,
|
|
11545
|
+
"view_type": view_route.view_type,
|
|
11546
|
+
"format": "csv",
|
|
11547
|
+
"row_count": row_count,
|
|
11548
|
+
"complete": complete,
|
|
11549
|
+
"truncated": not complete,
|
|
11550
|
+
"safe_for_final_conclusion": safe_for_final_conclusion,
|
|
11551
|
+
"files": files,
|
|
11552
|
+
"fields": fields,
|
|
11553
|
+
"warnings": warnings,
|
|
11554
|
+
"verification": verification,
|
|
11555
|
+
"csv_columns": ["record_id"] + [str(field["column_name"]) for field in fields if field.get("column_name")],
|
|
11556
|
+
}
|
|
11557
|
+
schema_path.write_text(json.dumps(schema_payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
11558
|
+
readme_path.write_text(
|
|
11559
|
+
_record_access_readme(
|
|
11560
|
+
created_at=created_at,
|
|
11561
|
+
app_key=app_key,
|
|
11562
|
+
view_route=view_route,
|
|
11563
|
+
row_count=row_count,
|
|
11564
|
+
complete=complete,
|
|
11565
|
+
safe_for_final_conclusion=safe_for_final_conclusion,
|
|
11566
|
+
files=files,
|
|
11567
|
+
fields=fields,
|
|
11568
|
+
warnings=warnings,
|
|
11569
|
+
),
|
|
11570
|
+
encoding="utf-8",
|
|
11571
|
+
)
|
|
11572
|
+
return {
|
|
11573
|
+
"schema": str(schema_path),
|
|
11574
|
+
"readme": str(readme_path),
|
|
11575
|
+
}
|
|
11576
|
+
|
|
11577
|
+
|
|
11578
|
+
def _record_access_readme(
|
|
11579
|
+
*,
|
|
11580
|
+
created_at: str,
|
|
11581
|
+
app_key: str,
|
|
11582
|
+
view_route: AccessibleViewRoute,
|
|
11583
|
+
row_count: int,
|
|
11584
|
+
complete: bool,
|
|
11585
|
+
safe_for_final_conclusion: bool,
|
|
11586
|
+
files: list[JSONObject],
|
|
11587
|
+
fields: list[JSONObject],
|
|
11588
|
+
warnings: list[JSONObject],
|
|
11589
|
+
) -> str:
|
|
11590
|
+
lines = [
|
|
11591
|
+
"# Qingflow Record Access",
|
|
11592
|
+
"",
|
|
11593
|
+
f"- Created at: {created_at}",
|
|
11594
|
+
f"- App key: `{app_key}`",
|
|
11595
|
+
f"- View: {view_route.name} (`{view_route.view_id}`)",
|
|
11596
|
+
f"- Format: CSV",
|
|
11597
|
+
f"- Rows: {row_count}",
|
|
11598
|
+
f"- Complete: {str(complete).lower()}",
|
|
11599
|
+
f"- Safe for final conclusion: {str(safe_for_final_conclusion).lower()}",
|
|
11600
|
+
"",
|
|
11601
|
+
"## Data Files",
|
|
11602
|
+
"",
|
|
11603
|
+
]
|
|
11604
|
+
if files:
|
|
11605
|
+
for file_info in files:
|
|
11606
|
+
local_path = _normalize_optional_text(file_info.get("local_path")) or ""
|
|
11607
|
+
part = file_info.get("part")
|
|
11608
|
+
file_rows = file_info.get("row_count")
|
|
11609
|
+
lines.append(f"- `{Path(local_path).name}`: part {part}, {file_rows} rows")
|
|
11610
|
+
else:
|
|
11611
|
+
lines.append("- No CSV shard files were written because the query returned 0 rows.")
|
|
11612
|
+
lines.extend(
|
|
11613
|
+
[
|
|
11614
|
+
"",
|
|
11615
|
+
"## Columns",
|
|
11616
|
+
"",
|
|
11617
|
+
"| CSV column | Field ID | Title | Type |",
|
|
11618
|
+
"|---|---:|---|---|",
|
|
11619
|
+
"| `record_id` | - | Record ID | record_id |",
|
|
11620
|
+
]
|
|
11621
|
+
)
|
|
11622
|
+
for field in fields:
|
|
11623
|
+
column_name = _markdown_escape(str(field.get("column_name", "")))
|
|
11624
|
+
field_id = field.get("field_id", "")
|
|
11625
|
+
title = _markdown_escape(str(field.get("title", "")))
|
|
11626
|
+
field_type = _markdown_escape(str(field.get("type", "")))
|
|
11627
|
+
lines.append(f"| `{column_name}` | {field_id} | {title} | {field_type} |")
|
|
11628
|
+
if warnings:
|
|
11629
|
+
lines.extend(["", "## Warnings", ""])
|
|
11630
|
+
for warning in warnings:
|
|
11631
|
+
code = _normalize_optional_text(warning.get("code")) or "WARNING"
|
|
11632
|
+
message = _normalize_optional_text(warning.get("message")) or ""
|
|
11633
|
+
lines.append(f"- `{code}`: {message}")
|
|
11634
|
+
lines.extend(
|
|
11635
|
+
[
|
|
11636
|
+
"",
|
|
11637
|
+
"## Usage Notes",
|
|
11638
|
+
"",
|
|
11639
|
+
"- Use `records-*.csv` for Python or pandas processing; column names are stable and field-id based.",
|
|
11640
|
+
"- Use `schema.json` or this README to map `field_<field_id>` columns back to Qingflow field titles and types.",
|
|
11641
|
+
"",
|
|
11642
|
+
]
|
|
11643
|
+
)
|
|
11644
|
+
return "\n".join(lines)
|
|
11645
|
+
|
|
11646
|
+
|
|
11647
|
+
def _markdown_escape(value: str) -> str:
|
|
11648
|
+
return value.replace("\\", "\\\\").replace("|", "\\|").replace("\n", " ")
|
|
11649
|
+
|
|
11650
|
+
|
|
11571
11651
|
def _record_access_field_type(field: FormField) -> str:
|
|
11572
11652
|
if field.que_type in DATE_QUE_TYPES:
|
|
11573
11653
|
return "datetime"
|