@josephyan/qingflow-app-user-mcp 0.2.0-beta.999 → 1.0.5

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 (43) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/pyproject.toml +1 -1
  4. package/skills/qingflow-app-user/references/data-gotchas.md +3 -1
  5. package/skills/qingflow-app-user/references/public-surface-sync.md +4 -3
  6. package/skills/qingflow-record-analysis/SKILL.md +46 -21
  7. package/skills/qingflow-record-analysis/agents/openai.yaml +1 -1
  8. package/skills/qingflow-record-analysis/references/analysis-gotchas.md +11 -9
  9. package/skills/qingflow-record-analysis/references/analysis-patterns.md +37 -16
  10. package/skills/qingflow-record-analysis/references/confidence-reporting.md +3 -1
  11. package/src/qingflow_mcp/__init__.py +1 -1
  12. package/src/qingflow_mcp/backend_client.py +109 -0
  13. package/src/qingflow_mcp/builder_facade/button_style_catalog.py +282 -0
  14. package/src/qingflow_mcp/builder_facade/models.py +44 -5
  15. package/src/qingflow_mcp/builder_facade/service.py +21 -8
  16. package/src/qingflow_mcp/cli/commands/__init__.py +2 -1
  17. package/src/qingflow_mcp/cli/commands/app.py +47 -1
  18. package/src/qingflow_mcp/cli/commands/builder.py +7 -0
  19. package/src/qingflow_mcp/cli/commands/exports.py +111 -0
  20. package/src/qingflow_mcp/cli/commands/record.py +20 -0
  21. package/src/qingflow_mcp/cli/commands/task.py +644 -22
  22. package/src/qingflow_mcp/cli/commands/workspace.py +49 -50
  23. package/src/qingflow_mcp/cli/context.py +3 -0
  24. package/src/qingflow_mcp/cli/formatters.py +139 -4
  25. package/src/qingflow_mcp/cli/interaction.py +72 -0
  26. package/src/qingflow_mcp/cli/main.py +2 -0
  27. package/src/qingflow_mcp/cli/terminal_ui.py +55 -9
  28. package/src/qingflow_mcp/errors.py +2 -2
  29. package/src/qingflow_mcp/export_store.py +14 -0
  30. package/src/qingflow_mcp/public_surface.py +6 -0
  31. package/src/qingflow_mcp/response_trim.py +40 -1
  32. package/src/qingflow_mcp/server.py +22 -0
  33. package/src/qingflow_mcp/server_app_builder.py +4 -0
  34. package/src/qingflow_mcp/server_app_user.py +104 -8
  35. package/src/qingflow_mcp/session_store.py +57 -6
  36. package/src/qingflow_mcp/tools/ai_builder_tools.py +59 -16
  37. package/src/qingflow_mcp/tools/auth_tools.py +26 -0
  38. package/src/qingflow_mcp/tools/custom_button_tools.py +0 -2
  39. package/src/qingflow_mcp/tools/export_tools.py +1565 -0
  40. package/src/qingflow_mcp/tools/import_tools.py +42 -2
  41. package/src/qingflow_mcp/tools/record_tools.py +551 -16
  42. package/src/qingflow_mcp/tools/resource_read_tools.py +40 -1
  43. package/src/qingflow_mcp/tools/task_context_tools.py +26 -8
package/README.md CHANGED
@@ -3,13 +3,13 @@
3
3
  Install:
4
4
 
5
5
  ```bash
6
- npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.999
6
+ npm install @josephyan/qingflow-app-user-mcp@1.0.5
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.999 qingflow-app-user-mcp
12
+ npx -y -p @josephyan/qingflow-app-user-mcp@1.0.5 qingflow-app-user-mcp
13
13
  ```
14
14
 
15
15
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-app-user-mcp",
3
- "version": "0.2.0-beta.999",
3
+ "version": "1.0.5",
4
4
  "description": "Operational end-user MCP for Qingflow records, tasks, comments, and directory workflows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qingflow-mcp"
7
- version = "0.2.0b999"
7
+ version = "1.0.5"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -4,10 +4,12 @@ For final statistics, grouped distributions, rankings, trends, or insight-style
4
4
 
5
5
  ## Record Reads
6
6
 
7
- - `record_list` is for browsing, export, and sample inspection only
7
+ - For analysis-style reads, use `record_access` through [$qingflow-record-analysis](../../qingflow-record-analysis/SKILL.md)
8
+ - `record_list` is for browsing and sample inspection only
8
9
  - `record_get` is for one exact record
9
10
  - Use `record_browse_schema_get` when field titles are uncertain instead of guessing ids
10
11
  - Do not present paged browse output as if it were a grouped or full-population conclusion
12
+ - Use `record_export_direct` only when the user explicitly asks for export/download/Excel output
11
13
 
12
14
  ## Direct Writes
13
15
 
@@ -9,10 +9,11 @@ It is not a user-facing product spec. It exists to prevent skill drift.
9
9
 
10
10
  - Read range first with `app_get`, then `record_browse_schema_get(view_id=...)`
11
11
  - Standard flows:
12
- - browse / export detail: `app_get -> record_browse_schema_get -> record_list / record_get`
12
+ - analyze: `app_get -> record_browse_schema_get -> record_access -> Python`
13
+ - browse detail: `app_get -> record_browse_schema_get -> record_list / record_get`
14
+ - explicit export/download/Excel: `view_get -> record_export_*` or `record_export_direct`
13
15
  - insert: `record_insert_schema_get -> record_insert`
14
16
  - update: `record_update_schema_get -> record_update`
15
- - analyze: `app_get -> record_browse_schema_get -> record_analyze`
16
17
 
17
18
  ### Tasks
18
19
 
@@ -55,7 +56,7 @@ It is not a user-facing product spec. It exists to prevent skill drift.
55
56
  - Package public tools: do not regress to `package_create` / `package_attach_app` as the public default story
56
57
  - App editability: do not let `can_edit_form` imply app base-info writes
57
58
  - Portal and chart visibility: keep the public story on `portal_apply` / `app_charts_apply`, not low-level internal writes
58
- - Analysis fallback: standard path stays `record_analyze`, but complex tables may require explicit fallback modes
59
+ - Analysis path: standard path stays `record_access -> Python`; `record_analyze` is a lightweight non-default helper
59
60
 
60
61
  ## Release Checklist For Skill Maintenance
61
62
 
@@ -2,47 +2,48 @@
2
2
  name: qingflow-record-analysis
3
3
  description: Analyze Qingflow record data safely after the MCP is already connected and authenticated. Use when the user wants grouped distributions, ratios, averages, rankings, trends, insights, or any final statistical conclusion across an existing app's data. Do not use this skill for schema changes, app design, or ordinary record CRUD unless they are strictly supporting an analysis flow.
4
4
  metadata:
5
- short-description: Analyze Qingflow record data with schema-first DSL execution
5
+ short-description: Analyze Qingflow record data with schema-first CSV access and Python
6
6
  ---
7
7
 
8
8
  # Qingflow Record Analysis
9
9
 
10
10
  This skill is for final statistical conclusions only.
11
11
  Assumes MCP is connected, authenticated, and on the correct workspace.
12
- Analysis tasks must start with `app_get`, then `record_browse_schema_get(view_id=...)`. Read top-level `fields` and `suggested_*`, then build field_id-based DSLs only.
13
- Analysis tasks must start with `record_browse_schema_get`.
14
- If `app_get.accessible_views` marks a view with `analysis_supported=false`, do not use that view for `record_list` or `record_analyze`. `boardView` and `ganttView` are special UI views, not list/analyze targets.
12
+ Analysis tasks must start with `app_get`, then `record_browse_schema_get(view_id=...)`. Read top-level `fields` and `suggested_*`, then choose field_id-based columns and filters only.
13
+ If `app_get.accessible_views` marks a view with `analysis_supported=false`, do not use that view for `record_access`, `record_list`, or `record_analyze`. `boardView` and `ganttView` are special UI views, not data-access targets.
15
14
 
16
- ## Step 1: `app_get` → Step 2: `record_browse_schema_get(view_id=...)` → Step 3: build DSL → Step 4: `record_analyze`
15
+ ## Step 1: `app_get` → Step 2: `record_browse_schema_get(view_id=...)` → Step 3: `record_access` → Step 4: Python
17
16
 
18
- This is the ONLY execution order. Never skip `app_get` when the browse range is unclear. Never call `record_analyze` without a browse schema.
17
+ This is the default execution order. Never skip `app_get` when the browse range is unclear. Never call `record_access` without a browse schema.
19
18
 
20
- Core tools: `app_get`, `record_browse_schema_get`, `record_analyze`. Use `record_list`/`record_get` only for post-analysis samples or an explicit fallback. Task/comment work stays in [$qingflow-task-ops](../qingflow-task-ops/SKILL.md).
19
+ Core tools: `app_get`, `record_browse_schema_get`, `record_access`, Python. Use field_id-based DSLs only for columns, filters, sort clauses, and any optional lightweight `record_analyze` helper. Use `record_list`/`record_get` only for browse samples. Use `record_analyze` only as a lightweight non-default statistics helper when a compact grouped result is enough. Task/comment work stays in [$qingflow-task-ops](../qingflow-task-ops/SKILL.md).
21
20
 
22
21
  ## Execution Modes
23
22
 
24
23
  Choose the lightest mode that can still support a trustworthy conclusion:
25
24
 
26
- 1. `server_aggregate`
25
+ 1. `client_python_from_record_access`
27
26
  - Default and preferred path
28
- - Use `record_analyze` directly after `app_get -> record_browse_schema_get`
27
+ - Use `record_access` directly after `app_get -> record_browse_schema_get`
28
+ - Read CSV shard files from `files[].local_path` with Python
29
+ - Treat `complete=true` and `safe_for_final_conclusion=true` as required before giving a full-scope final conclusion
29
30
 
30
- 2. `client_aggregate_from_record_list`
31
- - Allowed fallback when `record_analyze` is unstable, unsupported on the target view, or the table is complex enough that service-side aggregation is not trustworthy
31
+ 2. `lightweight_record_analyze`
32
+ - Optional helper for small grouped summaries or quick sanity checks
32
33
  - Still requires `app_get -> record_browse_schema_get` first
33
- - Use only after disclosing that the result is a fallback built from detail rows
34
+ - Do not make this the default agent path
34
35
 
35
36
  3. `cross_app_manual_reconcile`
36
37
  - Use when the question depends on joining multiple apps, organization-history alias mapping, or other business logic that current public tools do not express directly
37
- - Be explicit that the conclusion is based on manual reconciliation rules, not one single DSL execution
38
+ - Be explicit that the conclusion is based on manual reconciliation rules, not one single app's CSV access
38
39
 
39
40
  ## Fallback Ladder
40
41
 
41
42
  Trigger a fallback when any of these are true:
42
43
 
43
- - `record_analyze` is unstable or cannot complete the scan reliably
44
- - the target view is unsupported for analysis
45
- - field semantics are ambiguous enough that a server aggregate would be misleading
44
+ - `record_access` returns `complete=false`, `truncated=true`, or `safe_for_final_conclusion=false`
45
+ - the target view is unsupported for data access
46
+ - field semantics are ambiguous enough that local aggregation would be misleading
46
47
  - the question requires cross-app reconciliation
47
48
  - the question depends on organization-tree scope, historical department aliases, or other business rules that are not first-class MCP filters
48
49
 
@@ -58,6 +59,30 @@ When you fall back:
58
59
 
59
60
  ## DSL Contract
60
61
 
62
+ ### `record_access` Contract
63
+
64
+ Use `record_access` to fetch detail rows into local CSV shards. It does not analyze, aggregate, or return large `items`.
65
+
66
+ ```json
67
+ {
68
+ "app_key": "APP_KEY",
69
+ "view_id": "system:all",
70
+ "columns": [{ "field_id": 2 }, { "field_id": 18 }],
71
+ "where": [{ "field_id": 2, "op": "between", "value": ["2026-05-01", "2026-05-31"] }],
72
+ "order_by": [{ "field_id": 2, "direction": "asc" }]
73
+ }
74
+ ```
75
+
76
+ Then run Python against every `files[].local_path`. CSV columns are stable: `record_id`, then `field_<field_id>`. Use `fields[]` metadata to map titles and types.
77
+
78
+ - Never ask for `page`, `page_size`, `limit`, or `max_rows`; `record_access` owns paging internally.
79
+ - If multiple CSV files are returned, read them all.
80
+ - If `complete=false` or `safe_for_final_conclusion=false`, downgrade the answer and disclose the limitation.
81
+ - `record_export_direct` is only for explicit export/download/Excel requests, not default analysis.
82
+ - QingBI/report reads are only for user-provided report URLs or `chart_id`; do not create or use reports as the default analysis path.
83
+
84
+ ### `record_analyze` Lightweight DSL
85
+
61
86
  ### DSL FORMAT (CRITICAL — read this FIRST)
62
87
 
63
88
  ### ✅ Correct vs ❌ Wrong — learn from these before building ANY DSL
@@ -143,7 +168,7 @@ Top-level arguments:
143
168
  - `strict_full`: `true` for final conclusions. `false` allows partial results.
144
169
  - `limit`: limits returned rows only, not scan scope.
145
170
  - `view_id`: the canonical browse selector. Prefer choosing it from `app_get.accessible_views`.
146
- - Prefer `view_id` entries where `analysis_supported=true`. If a view is `boardView` or `ganttView`, switch to a system or table-style custom view before calling `record_analyze`.
171
+ - Prefer `view_id` entries where `analysis_supported=true`. If a view is `boardView` or `ganttView`, switch to a system or table-style custom view before calling `record_access` or `record_analyze`.
147
172
  - If a chosen `view_id` is `custom:*`, treat the output as analysis over an unverified saved-filter scope unless `verification.view_filter_verified=true`. For critical conclusions, prefer `system:all` plus explicit filters in the DSL.
148
173
  - `bucket` in dimensions: only for `suggested_time_fields`. Values: `day`/`week`/`month`/`quarter`/`year`/`null`.
149
174
 
@@ -163,8 +188,8 @@ Top-level arguments:
163
188
  - Final wording should stay as close as possible to schema titles.
164
189
  - Do not pass field titles, aliases, or guessed ids.
165
190
  - If `completeness.statement_scope=returned_groups_only` or `completeness.rows_truncated=true`, downgrade wording to returned groups only.
166
- - One DSL per question. Multiple small DSLs > one overloaded request.
167
- - `record_list` is not the default basis for final statistics. Use it only in an explicit fallback mode and disclose that fallback in the final answer.
191
+ - One data-access request per coherent dataset. Multiple small Python computations over the same CSV files are fine.
192
+ - `record_list` is not the basis for final statistics. Use it only for browse/sample inspection and disclose that scope if quoted.
168
193
  - Set `alias` for any metric you will sort by, compare, or quote.
169
194
 
170
195
  ---
@@ -172,14 +197,14 @@ Top-level arguments:
172
197
  ## OUTPUT
173
198
 
174
199
  - Final answer must show concrete numbers.
175
- - Final answer must state which execution mode was used whenever the answer is not a straightforward `server_aggregate`
200
+ - Final answer must state which execution mode was used whenever the answer is not the default `client_python_from_record_access`
176
201
  - If `result.rows` exists, list each returned row; if there are more than 20 rows, show Top 20 and say so.
177
202
  - 占比 = 行指标值 / `result.totals.metric_totals` 总值;如 `metric_totals` 缺失,用各行之和作分母。
178
203
  - Prefer the structured `ranking` block when it exists.
179
204
  - `safe_for_final_conclusion=true` → `全量可信结论`
180
205
  - Otherwise → `初步观察`
181
206
  - `rows_truncated=true` → 用 `前 N 个分组`, 不用 `全部`/`所有`
182
- - If you used a fallback mode, explicitly disclose:
207
+ - If you used a fallback mode or `record_access.safe_for_final_conclusion=false`, explicitly disclose:
183
208
  - whether this is full-scope or a manually curated subset
184
209
  - which time field was used
185
210
  - which organization scope was used
@@ -1,4 +1,4 @@
1
1
  interface:
2
2
  display_name: "Qingflow Record Analysis"
3
3
  short_description: "Analyze Qingflow record data with schema-first DSL execution"
4
- default_prompt: "Use $qingflow-record-analysis for grouped distributions, ratios, rankings, trends, and final statistical conclusions in Qingflow apps. Start with record_browse_schema_get, build one or more field_id-based DSLs, then run record_analyze. Treat record_list as sample-only when capped or paged, and separate full conclusions from sample observations."
4
+ default_prompt: "Use $qingflow-record-analysis for grouped distributions, ratios, rankings, trends, and final statistical conclusions in Qingflow apps. Start with app_get and record_browse_schema_get, run record_access with field_id-based columns/where/order_by, then analyze the returned CSV shard files with Python. Treat record_list as sample-only and record_analyze as a lightweight non-default helper."
@@ -2,14 +2,15 @@
2
2
 
3
3
  ## Do not skip schema
4
4
 
5
- If the task is analysis-style and you jump straight to `record_list` or `record_analyze`, you are already off the stable path.
5
+ If the task is analysis-style and you jump straight to `record_list`, `record_export_direct`, or `record_analyze`, you are already off the stable path.
6
6
 
7
7
  Correct recovery:
8
8
 
9
- 1. `record_browse_schema_get`
10
- 2. inspect the schema and choose fields
11
- 3. build one or more small DSLs
12
- 4. run `record_analyze`
9
+ 1. `app_get`
10
+ 2. `record_browse_schema_get`
11
+ 3. inspect the schema and choose fields
12
+ 4. run `record_access`
13
+ 5. use Python over the returned CSV shards
13
14
 
14
15
  The schema here is applicant-node visible-only. If a field is absent, treat it as not available to the current user rather than switching to guessed ids or builder-side memory.
15
16
 
@@ -23,7 +24,7 @@ Examples:
23
24
 
24
25
  Do not pass vague time phrases or impossible dates into MCP.
25
26
 
26
- ## Do not treat 200-row list output as full data
27
+ ## Do not treat paged list output as full data
27
28
 
28
29
  `record_list` can hit:
29
30
 
@@ -44,7 +45,7 @@ It is not acceptable to use that result alone for:
44
45
 
45
46
  ## Do not mix full analyze totals with sample rows
46
47
 
47
- If `record_analyze` gives full-population coverage, but list rows are capped, do not merge them into one final statement.
48
+ If `record_access` or `record_analyze` gives full-population coverage, but list rows are capped, do not merge them into one final statement.
48
49
 
49
50
  Split them into:
50
51
 
@@ -88,17 +89,18 @@ Examples of the right recovery question:
88
89
 
89
90
  ## Do not try to control paging manually
90
91
 
91
- `record_analyze` hides paging and scan budget on purpose.
92
+ `record_access` and `record_analyze` hide paging and scan budget on purpose.
92
93
 
93
94
  - Do not invent `page_size`
94
95
  - Do not invent `requested_pages`
95
96
  - Do not invent `scan_max_pages`
96
97
  - Do not invent `auto_expand_pages`
98
+ - Do not invent `max_rows`
97
99
 
98
100
  When the result is incomplete:
99
101
 
100
102
  1. narrow the scope with views or filters
101
- 2. reduce the analysis problem into smaller DSLs
103
+ 2. reduce the analysis problem into smaller field_id-based access requests
102
104
  3. keep the answer at `初步观察` or `部分结果` if completeness is still not enough
103
105
 
104
106
  ## Do not guess metric semantics from loose business wording
@@ -15,11 +15,33 @@ Use this skill when the user asks for:
15
15
 
16
16
  ## Canonical analysis sequence
17
17
 
18
- 1. `record_browse_schema_get`
19
- 2. decide whether the question needs `count`, `sum`, `avg`, `distinct_count`, `ratio`, or `ranking`
20
- 3. build one or more field_id-based DSLs
21
- 4. `record_analyze`
22
- 5. `record_list` only for sample inspection
18
+ 1. `app_get`
19
+ 2. `record_browse_schema_get`
20
+ 3. decide whether the question needs `count`, `sum`, `avg`, `distinct_count`, `ratio`, or `ranking`
21
+ 4. choose field_id-based `columns`, `where`, and `order_by`
22
+ 5. `record_access`
23
+ 6. Python over every returned CSV shard
24
+ 7. `record_list` only for sample inspection
25
+
26
+ Result reading order:
27
+
28
+ 1. `record_access.complete`
29
+ 2. `record_access.safe_for_final_conclusion`
30
+ 3. `record_access.files[].local_path`
31
+ 4. Python outputs
32
+ 5. `record_access.fields`
33
+ 6. `record_access.warnings`
34
+
35
+ 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
+ ## Lightweight `record_analyze` helper sequence
38
+
39
+ 1. `app_get`
40
+ 2. `record_browse_schema_get`
41
+ 3. decide whether the question needs `count`, `sum`, `avg`, `distinct_count`, `ratio`, or `ranking`
42
+ 4. build one or more field_id-based DSLs
43
+ 5. `record_analyze`
44
+ 6. `record_list` only for sample inspection
23
45
 
24
46
  Result reading order:
25
47
 
@@ -30,7 +52,7 @@ Result reading order:
30
52
  5. `completeness`
31
53
  6. `presentation`
32
54
 
33
- Treat `record_browse_schema_get` as the browse-schema source of truth. Missing fields are permission boundaries, not invitations to guess hidden ids.
55
+ Use this only when a compact grouped result is enough; it is not the default path.
34
56
 
35
57
  ## Distribution / ratio pattern
36
58
 
@@ -41,7 +63,8 @@ Treat `record_browse_schema_get` as the browse-schema source of truth. Missing f
41
63
  - one dimension
42
64
  - `count`
43
65
  - sort by the count alias
44
- 5. Run `record_analyze`
66
+ 5. Run `record_access`
67
+ 6. Use Python to group the CSV rows
45
68
  6. Report:
46
69
  - `result.totals.metric_totals`
47
70
  - `safe_for_final_conclusion`
@@ -56,20 +79,18 @@ Treat `record_browse_schema_get` as the browse-schema source of truth. Missing f
56
79
  - numerator
57
80
  - denominator
58
81
  - grouping dimension, if any
59
- 3. Build separate DSLs when numerator and denominator are not the same filtered population
82
+ 3. Build separate `record_access` requests when numerator and denominator are not the same filtered population
60
83
  4. Query the numerator first
61
84
  5. Query the denominator second
62
- 6. Only compute the ratio outside MCP after both source results are complete and use compatible scopes
85
+ 6. Only compute the ratio in Python after both source results are complete and use compatible scopes
63
86
  7. If the denominator is missing, do not call the output `渗透率`, `转化率`, `占比`, or `%`
64
87
 
65
88
  ## Average / ranking pattern
66
89
 
67
90
  1. Run `record_browse_schema_get`
68
91
  2. Choose one dimension field and one numeric metric field
69
- 3. Build a DSL with:
70
- - `dimensions=[...]`
71
- - `metrics=[count,sum]` or `metrics=[count,avg,min,max]`
72
- 4. Run `record_analyze`
92
+ 3. Fetch the dimension and numeric metric fields with `record_access`
93
+ 4. Use Python for grouped `count/sum/avg/min/max`
73
94
  5. If the answer uses ranking language, make the ranking come from structured sorted results
74
95
  6. Prefer the structured `ranking` block when it exists instead of inferring order from loose text
75
96
  7. Use list mode only to inspect examples after the aggregate result is understood
@@ -78,14 +99,14 @@ Treat `record_browse_schema_get` as the browse-schema source of truth. Missing f
78
99
 
79
100
  1. Run `record_browse_schema_get`
80
101
  2. Choose a date/time field from `suggested_time_fields`
81
- 3. Build a DSL with `bucket=day|week|month|quarter|year`
82
- 4. Run `record_analyze`
102
+ 3. Fetch the date/time field and needed metric fields with `record_access`
103
+ 4. Use Python to bucket by day/week/month/quarter/year
83
104
  5. Treat the result as final only if `safe_for_final_conclusion=true`
84
105
  6. If the user asked for a relative time phrase such as `最近一个完整自然月`, translate it into an explicit legal date range before building the DSL
85
106
 
86
107
  ## Sample inspection pattern
87
108
 
88
- Only use `record_list` after schema/analyze when you need:
109
+ Only use `record_list` after schema/access when you need:
89
110
 
90
111
  - example rows
91
112
  - spot checks
@@ -13,7 +13,7 @@ When analysis is intended as a final answer, use this order:
13
13
  Only write `全量可信结论` when:
14
14
 
15
15
  - `record_browse_schema_get` was used
16
- - the analysis path used one or more `record_analyze` calls
16
+ - the analysis path used `record_access` and Python, or an explicitly chosen lightweight `record_analyze` helper
17
17
  - every key analysis result has `safe_for_final_conclusion=true`
18
18
  - `safe_for_final_conclusion=true is necessary but not sufficient`
19
19
  - no key result depends on an invalid time phrase, an undefined denominator, or an unsupported derived metric
@@ -27,6 +27,7 @@ Put evidence into `样本观察` when:
27
27
  - the tool reports `row_cap_hit`
28
28
  - the tool reports `sample_only`
29
29
  - the result is compact/capped and not complete
30
+ - `record_access.complete=false` or `record_access.truncated=true`
30
31
 
31
32
  ## Downgrade rule
32
33
 
@@ -37,6 +38,7 @@ If `record_browse_schema_get` was not used for an analysis task, downgrade the o
37
38
  Do not combine:
38
39
 
39
40
  - full totals from `record_analyze`
41
+ - full CSV-derived conclusions from `record_access`
40
42
  - sample-only details from `record_list`
41
43
 
42
44
  into one sentence like “基于全部数据分析...”.
@@ -5,7 +5,7 @@ from pathlib import Path
5
5
 
6
6
  __all__ = ["__version__"]
7
7
 
8
- _FALLBACK_VERSION = "0.2.0b999"
8
+ _FALLBACK_VERSION = "0.2.0b1017"
9
9
 
10
10
 
11
11
  def _resolve_local_pyproject_version() -> str | None:
@@ -474,6 +474,115 @@ class BackendClient:
474
474
  pass
475
475
  return import_result
476
476
 
477
+ def start_socket_record_export(
478
+ self,
479
+ context: BackendRequestContext,
480
+ *,
481
+ app_key: str,
482
+ view_id: str,
483
+ filter_bean: JSONObject,
484
+ export_config: JSONObject,
485
+ view_key: str | None = None,
486
+ result_amount: int = 0,
487
+ ack_timeout_seconds: float = 8.0,
488
+ ) -> dict[str, Any]:
489
+ try:
490
+ import socketio # type: ignore[import-not-found]
491
+ except ImportError as exc:
492
+ raise QingflowApiError(
493
+ category="config",
494
+ message=f"socket.io client dependency is missing: {exc}",
495
+ )
496
+
497
+ socket_base_url = self._build_socket_base_url(context.base_url)
498
+ export_result: dict[str, Any] = {
499
+ "backend_export_id": None,
500
+ "warnings": [],
501
+ }
502
+ sio = socketio.Client(reconnection=False, logger=False, engineio_logger=False)
503
+ event_name = "excelViewgraph" if view_key else "excel"
504
+ event_args: tuple[Any, ...]
505
+ if view_key:
506
+ event_args = (
507
+ context.token,
508
+ None,
509
+ view_key,
510
+ filter_bean,
511
+ export_config,
512
+ int(result_amount),
513
+ )
514
+ else:
515
+ event_args = (
516
+ context.token,
517
+ app_key,
518
+ filter_bean,
519
+ export_config,
520
+ int(result_amount),
521
+ )
522
+ try:
523
+ sio.connect(
524
+ socket_base_url,
525
+ transports=["websocket"],
526
+ socketio_path="socket.io",
527
+ headers=self._base_headers(
528
+ context.token,
529
+ context.ws_id,
530
+ qf_version=context.qf_version,
531
+ ),
532
+ wait_timeout=ack_timeout_seconds,
533
+ )
534
+ sio.emit("token", context.token)
535
+ sleep(0.2)
536
+ ack = sio.call(
537
+ event_name,
538
+ event_args,
539
+ timeout=ack_timeout_seconds,
540
+ )
541
+ ack_payload = ack[0] if isinstance(ack, list) and ack else ack
542
+ export_id: Any = ack_payload
543
+ if isinstance(ack_payload, dict):
544
+ error_code = ack_payload.get("error")
545
+ ack_message = ack_payload.get("message")
546
+ export_id = ack_payload.get("data")
547
+ if isinstance(export_id, dict):
548
+ export_id = (
549
+ export_id.get("exportId")
550
+ or export_id.get("export_id")
551
+ or export_id.get("id")
552
+ )
553
+ if error_code not in (None, 0):
554
+ raise QingflowApiError(
555
+ category="backend",
556
+ message=str(ack_message or f"socket export rejected with error {error_code}"),
557
+ details={
558
+ "socket_error_code": error_code,
559
+ "app_key": app_key,
560
+ "view_id": view_id,
561
+ "view_key": view_key,
562
+ },
563
+ )
564
+ if not export_id:
565
+ raise QingflowApiError(category="backend", message="socket export ack did not return export_id")
566
+ export_result["backend_export_id"] = str(export_id)
567
+ except Exception as exc:
568
+ message = str(exc)
569
+ if "timeout" in message.lower():
570
+ raise QingflowApiError(
571
+ category="network",
572
+ message="socket export ack timed out",
573
+ details={"error_code": "EXPORT_SOCKET_ACK_TIMEOUT"},
574
+ )
575
+ if isinstance(exc, QingflowApiError):
576
+ raise
577
+ raise QingflowApiError(category="network", message=message or "socket export failed")
578
+ finally:
579
+ try:
580
+ if sio.connected:
581
+ sio.disconnect()
582
+ except Exception:
583
+ pass
584
+ return export_result
585
+
477
586
  def _request_with_meta(
478
587
  self,
479
588
  method: str,