@josephyan/qingflow-app-user-mcp 0.2.0-beta.999 → 1.0.6
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-app-user/references/data-gotchas.md +3 -1
- package/skills/qingflow-app-user/references/public-surface-sync.md +4 -3
- package/skills/qingflow-record-analysis/SKILL.md +46 -21
- package/skills/qingflow-record-analysis/agents/openai.yaml +1 -1
- package/skills/qingflow-record-analysis/references/analysis-gotchas.md +11 -9
- package/skills/qingflow-record-analysis/references/analysis-patterns.md +37 -16
- package/skills/qingflow-record-analysis/references/confidence-reporting.md +3 -1
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/backend_client.py +109 -0
- package/src/qingflow_mcp/builder_facade/button_style_catalog.py +282 -0
- package/src/qingflow_mcp/builder_facade/models.py +44 -5
- package/src/qingflow_mcp/builder_facade/service.py +21 -8
- package/src/qingflow_mcp/cli/commands/__init__.py +2 -1
- package/src/qingflow_mcp/cli/commands/app.py +47 -1
- package/src/qingflow_mcp/cli/commands/builder.py +7 -0
- package/src/qingflow_mcp/cli/commands/exports.py +111 -0
- package/src/qingflow_mcp/cli/commands/record.py +20 -0
- package/src/qingflow_mcp/cli/commands/task.py +644 -22
- package/src/qingflow_mcp/cli/commands/workspace.py +49 -50
- package/src/qingflow_mcp/cli/context.py +3 -0
- package/src/qingflow_mcp/cli/formatters.py +139 -4
- package/src/qingflow_mcp/cli/interaction.py +72 -0
- package/src/qingflow_mcp/cli/main.py +2 -0
- package/src/qingflow_mcp/cli/terminal_ui.py +55 -9
- package/src/qingflow_mcp/errors.py +2 -2
- package/src/qingflow_mcp/export_store.py +14 -0
- package/src/qingflow_mcp/public_surface.py +6 -0
- package/src/qingflow_mcp/response_trim.py +40 -1
- package/src/qingflow_mcp/server.py +22 -0
- package/src/qingflow_mcp/server_app_builder.py +4 -0
- package/src/qingflow_mcp/server_app_user.py +104 -8
- package/src/qingflow_mcp/session_store.py +57 -6
- package/src/qingflow_mcp/tools/ai_builder_tools.py +59 -16
- package/src/qingflow_mcp/tools/auth_tools.py +26 -0
- package/src/qingflow_mcp/tools/custom_button_tools.py +0 -2
- package/src/qingflow_mcp/tools/export_tools.py +1565 -0
- package/src/qingflow_mcp/tools/import_tools.py +42 -2
- package/src/qingflow_mcp/tools/record_tools.py +515 -45
- package/src/qingflow_mcp/tools/resource_read_tools.py +40 -1
- 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@
|
|
6
|
+
npm install @josephyan/qingflow-app-user-mcp@1.0.6
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @josephyan/qingflow-app-user-mcp@
|
|
12
|
+
npx -y -p @josephyan/qingflow-app-user-mcp@1.0.6 qingflow-app-user-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -4,10 +4,12 @@ For final statistics, grouped distributions, rankings, trends, or insight-style
|
|
|
4
4
|
|
|
5
5
|
## Record Reads
|
|
6
6
|
|
|
7
|
-
-
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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
|
|
13
|
-
|
|
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:
|
|
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
|
|
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
|
|
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. `
|
|
25
|
+
1. `client_python_from_record_access`
|
|
27
26
|
- Default and preferred path
|
|
28
|
-
- Use `
|
|
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. `
|
|
31
|
-
-
|
|
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
|
-
-
|
|
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
|
|
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
|
-
- `
|
|
44
|
-
- the target view is unsupported for
|
|
45
|
-
- field semantics are ambiguous enough that
|
|
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 and follows the backend's native paging capability.
|
|
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
|
|
167
|
-
- `record_list` is not the
|
|
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
|
|
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
|
|
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,
|
|
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. `
|
|
10
|
-
2.
|
|
11
|
-
3.
|
|
12
|
-
4. run `
|
|
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
|
|
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` hides paging and follows the backend's native paging capability. `record_analyze` hides 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
|
|
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. `
|
|
19
|
-
2.
|
|
20
|
-
3.
|
|
21
|
-
4. `
|
|
22
|
-
5. `
|
|
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
|
-
|
|
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 `
|
|
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
|
|
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
|
|
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.
|
|
70
|
-
|
|
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.
|
|
82
|
-
4.
|
|
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/
|
|
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
|
|
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 “基于全部数据分析...”.
|
|
@@ -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,
|