@josephyan/qingflow-app-user-mcp 0.2.0-beta.23 → 0.2.0-beta.24
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 +120 -225
- package/src/qingflow_mcp/tools/record_tools.py +33 -12
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.
|
|
6
|
+
npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.24
|
|
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.
|
|
12
|
+
npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.24 qingflow-app-user-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -7,192 +7,81 @@ metadata:
|
|
|
7
7
|
|
|
8
8
|
# Qingflow Record Analysis
|
|
9
9
|
|
|
10
|
-
##
|
|
11
|
-
|
|
12
|
-
This skill is for record analysis inside existing Qingflow apps. Use it when the task is about `分析 / 洞察 / 分布 / 占比 / 平均 / 排名 / 趋势 / 所有 / 全部 / 全国 / 高价值` or any final statistical conclusion.
|
|
13
|
-
|
|
14
|
-
This skill assumes the MCP is already connected and authenticated. If not, switch to `$qingflow-mcp-setup` first. If the task is about creating, updating, or deleting records rather than analyzing them, switch to `$qingflow-record-crud`. If it is about task-center actions, comments, approvals, rollback, transfer, or directory-driven workflow work, switch to `$qingflow-task-ops`.
|
|
15
|
-
|
|
16
|
-
Before running analysis in `prod`, confirm the intended environment. If browser parity or live route debugging matters, call `record_analyze` with `output_profile=\"verbose\"` and compare `debug.request_route` with the browser route.
|
|
17
|
-
|
|
18
|
-
## Tool Scope
|
|
19
|
-
|
|
20
|
-
Use these tools as the core analysis surface:
|
|
21
|
-
|
|
22
|
-
- `record_schema_get`
|
|
23
|
-
- `record_analyze`
|
|
24
|
-
|
|
25
|
-
Use `record_list` or `record_get` only when you need sample rows or a specific supporting example after the main analysis path.
|
|
26
|
-
|
|
27
|
-
`record_schema_get` now returns the **current user's applicant-node visible schema only**:
|
|
28
|
-
|
|
29
|
-
- hidden fields are omitted entirely
|
|
30
|
-
- absent fields should be interpreted as `当前用户在申请人节点下不可见/不可用`
|
|
31
|
-
- do not treat the schema as a builder/full-field metadata dump
|
|
32
|
-
|
|
33
|
-
## Hard Rules
|
|
34
|
-
|
|
35
|
-
- Analysis tasks must start with `record_schema_get`
|
|
36
|
-
- Build one or more small DSLs, then run `record_analyze` separately for each question
|
|
37
|
-
- DSL field references must use `field_id` only
|
|
38
|
-
- Normalize relative time phrases into explicit legal date ranges before building the DSL
|
|
39
|
-
- If the user asks for `最近一个完整自然月 / 上个月 / 最近30天 / 本季度 / 去年同期`, first convert that phrase into concrete dates, then verify the dates are legal before calling MCP
|
|
40
|
-
- Never send impossible dates such as `2026-02-29`; if the intended month is February 2026, the legal upper bound is `2026-02-28`
|
|
41
|
-
- If the schema still leaves multiple plausible fields, stop and ask the user to confirm from a short candidate list instead of guessing
|
|
42
|
-
- Do not keep retrying different guessed field names in a loop
|
|
43
|
-
- `record_list` is never the basis for a final statistical conclusion
|
|
44
|
-
- If `record_list` is capped or paged, treat it as sample-only evidence
|
|
45
|
-
- Do not mix full totals from `record_analyze` with sample-only list observations as one combined `全量结论`
|
|
46
|
-
- Do not manually tune paging or scan-budget parameters for analysis; `record_analyze` hides them
|
|
47
|
-
- For final conclusions, prefer `strict_full=true`
|
|
48
|
-
- Before choosing a DSL shape, first decide whether the question needs `count`, `sum`, `avg`, `distinct_count`, `ratio`, or `ranking`
|
|
49
|
-
- Do not guess a metric just because the user said `数量`, `单量`, `人数`, or `金额`
|
|
50
|
-
- If one business question depends on multiple metrics, split it into smaller structured questions and build multiple focused DSLs
|
|
51
|
-
- `渗透率 / 转化率 / 占比类结论必须先定义分子和分母`
|
|
52
|
-
- Do not claim a metric you did not query.
|
|
53
|
-
- Derived ratios must be computed outside the DSL after trusted numerator and denominator queries complete; do not invent `div`, `formula`, or expression metrics inside `record_analyze`
|
|
54
|
-
- If the requested business question requires unsupported derived math, split it into multiple DSLs and compute the final ratio only in the reasoning layer after the source metrics are confirmed
|
|
55
|
-
- If the user asks for multiple conclusions and only part of them is completed reliably, explicitly disclose which parts are complete and which parts remain unresolved
|
|
56
|
-
|
|
57
|
-
## Standard Operating Order
|
|
58
|
-
|
|
59
|
-
For analysis:
|
|
60
|
-
|
|
61
|
-
1. Confirm target app and environment
|
|
62
|
-
2. Run `record_schema_get`
|
|
63
|
-
3. Inspect fields, aliases, suggested dimensions, suggested metrics, and suggested time fields
|
|
64
|
-
4. Generate one or more field_id-based DSLs
|
|
65
|
-
5. Run `record_analyze` once per DSL
|
|
66
|
-
6. Run `record_list` only if you still need sample rows, examples, or manual inspection
|
|
67
|
-
7. Before answering, separate:
|
|
68
|
-
- `全量可信结论`
|
|
69
|
-
- `样本观察`
|
|
70
|
-
- `待验证假设`
|
|
71
|
-
|
|
72
|
-
## Semantic Guardrails
|
|
73
|
-
|
|
74
|
-
- If the user asks for penetration, conversion, share-of-total, win rate, non-standard ratio, or any `%` metric, first write down:
|
|
75
|
-
- numerator definition
|
|
76
|
-
- denominator definition
|
|
77
|
-
- whether each side needs its own DSL
|
|
78
|
-
- If you cannot name the denominator from real schema fields and filters, do not use words like `渗透率`, `转化率`, `占比`, `比例`, or `%`
|
|
79
|
-
- If a field is still ambiguous after `record_schema_get`, do not guess; either select one unique `field_id` from the schema or ask the user to confirm from a short candidate list
|
|
80
|
-
- If a business field is absent from `record_schema_get`, do not infer or guess a hidden `field_id`; explain that the field is not visible in the current applicant-node permission scope
|
|
81
|
-
- If a statement depends on `count`, query `count`
|
|
82
|
-
- If a statement depends on total amount, query `sum`
|
|
83
|
-
- If a statement depends on average level, query `avg` or derive it from trusted `sum + count`
|
|
84
|
-
- If a statement depends on trend, query a time dimension with `bucket`
|
|
85
|
-
- If a statement depends on a ratio that the DSL cannot express directly, run the numerator and denominator separately, then compute the ratio outside MCP only after both sides are complete and compatible
|
|
86
|
-
- Rankings must come from structured sorted results, not from loose natural-language restatement
|
|
87
|
-
- When grouped rows are truncated, describe them as `已返回分组中` or `主要分组`
|
|
88
|
-
- If `completeness.rows_truncated=true` or `completeness.statement_scope=returned_groups_only`, do not use words like `各部门`、`所有分组`、`完整名单`、`全部渠道`
|
|
89
|
-
- If grouped rows are truncated, explicitly downgrade the wording to `前 N 个分组` or `主要分组`, never `全部`
|
|
90
|
-
- Complex answers should default to `先结构、后解读`: present the table / metrics / ordering first, then add concise interpretation
|
|
91
|
-
- Final wording should stay as close as possible to schema titles, dimension aliases, and metric aliases; do not rename the business object or field title unless the user asked for a rewrite
|
|
92
|
-
|
|
93
|
-
## DSL Contract
|
|
94
|
-
|
|
95
|
-
Use `record_schema_get` as the source of truth for every DSL field reference:
|
|
96
|
-
|
|
97
|
-
- Use `fields[].field_id` in `dimensions[].field_id`, `metrics[].field_id`, and `filters[].field_id`
|
|
98
|
-
- Treat `suggested_dimensions`, `suggested_metrics`, and `suggested_time_fields` as hints, not as executable DSL by themselves
|
|
99
|
-
- Do not pass field titles, aliases, or guessed ids where `field_id` is required
|
|
100
|
-
|
|
101
|
-
The `record_analyze` call should be built from this argument shape:
|
|
10
|
+
## Step 1: `record_schema_get` → Step 2: build DSL → Step 3: `record_analyze`
|
|
102
11
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
12
|
+
This is the ONLY execution order. Never skip step 1. Never call `record_analyze` without a schema.
|
|
13
|
+
|
|
14
|
+
Tools: `record_schema_get`, `record_analyze`. Use `record_list`/`record_get` only for sample rows AFTER analysis.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## DSL FORMAT (CRITICAL — read this FIRST)
|
|
19
|
+
|
|
20
|
+
### ✅ Correct vs ❌ Wrong — learn from these before building ANY DSL
|
|
21
|
+
|
|
22
|
+
**dimension item:**
|
|
23
|
+
```
|
|
24
|
+
✅ CORRECT: { "field_id": 9500572, "alias": "报价类型" }
|
|
25
|
+
❌ WRONG: 9500572 ← bare integer, not a dict
|
|
26
|
+
❌ WRONG: "报价类型" ← string, not a dict
|
|
27
|
+
❌ WRONG: { "field_id": 9500572, "title": "报价类型" } ← "title" is forbidden
|
|
116
28
|
```
|
|
117
29
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
- `strict_full`: boolean. Prefer `true` for final conclusions. If `true`, incomplete scans return an error; if `false`, incomplete scans return partial results.
|
|
127
|
-
- `view_key` / `view_name`: optional. Use a view to narrow scope before analysis. Prefer `view_key` when both are available.
|
|
128
|
-
- `output_profile`: `normal` or `verbose`. Prefer `normal` unless you are debugging completeness or route issues.
|
|
129
|
-
|
|
130
|
-
Item contracts:
|
|
131
|
-
|
|
132
|
-
- `dimensions` item:
|
|
133
|
-
- shape: `{ "field_id": 2, "alias": "状态", "bucket": null }`
|
|
134
|
-
- `field_id`: required integer from `record_schema_get`
|
|
135
|
-
- `alias`: optional but recommended; if omitted, the field title becomes the alias
|
|
136
|
-
- `bucket`: optional; allowed values are `day`, `week`, `month`, `quarter`, `year`, or omitted / `null`
|
|
137
|
-
- `bucket` may only be used on fields from `suggested_time_fields`
|
|
138
|
-
- `metrics` item:
|
|
139
|
-
- shape: `{ "op": "sum", "field_id": 7, "alias": "总金额" }`
|
|
140
|
-
- `op`: one of `count`, `sum`, `avg`, `min`, `max`, `distinct_count`
|
|
141
|
-
- `field_id`: required for `sum`, `avg`, `min`, `max`, `distinct_count`; do not pass it for `count`
|
|
142
|
-
- `alias`: optional but strongly recommended because `sort.by` must reference aliases
|
|
143
|
-
- `filters` item:
|
|
144
|
-
- shape: `{ "field_id": 2, "op": "eq", "value": "进行中" }`
|
|
145
|
-
- `field_id`: required integer from `record_schema_get`
|
|
146
|
-
- `op`: optional; defaults to `eq`
|
|
147
|
-
- supported ops: `eq`, `neq`, `in`, `not_in`, `gt`, `gte`, `lt`, `lte`, `between`, `contains`, `is_null`, `not_null`
|
|
148
|
-
- value rules:
|
|
149
|
-
- `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `contains`: pass a single scalar value
|
|
150
|
-
- `in`, `not_in`: pass an array
|
|
151
|
-
- `between`: pass a two-item array like `[min, max]`
|
|
152
|
-
- `is_null`, `not_null`: omit `value`
|
|
153
|
-
- `sort` item:
|
|
154
|
-
- shape: `{ "by": "记录数", "order": "desc" }`
|
|
155
|
-
- `by`: required and must reference an alias already defined in `dimensions` or `metrics`
|
|
156
|
-
- `order`: optional; use `asc` or `desc`; default is `asc`
|
|
157
|
-
- do not sort by raw field title or `field_id`
|
|
158
|
-
|
|
159
|
-
Practical rules:
|
|
160
|
-
|
|
161
|
-
- Keep one DSL focused on one question. Prefer multiple small DSLs over one overloaded request.
|
|
162
|
-
- Always set explicit aliases for metrics you may sort by, compare, or quote in the final answer.
|
|
163
|
-
- For trend analysis, use one time dimension with `bucket`, then sort by that time alias ascending.
|
|
164
|
-
- For cross analysis, use multiple `dimensions` and a small set of metrics.
|
|
165
|
-
- Do not attempt formulas, joins, having clauses, cohort analysis, or manual paging controls in this DSL.
|
|
166
|
-
- Do not pass unsupported keys such as `formula`, `expr`, `numerator`, `denominator`, `left`, `right`, or `operator` inside metric items.
|
|
167
|
-
|
|
168
|
-
## Minimal DSL Templates
|
|
169
|
-
|
|
170
|
-
Summary:
|
|
30
|
+
**metric item — the key is `op`, NOT `type`/`agg`/`aggregation`:**
|
|
31
|
+
```
|
|
32
|
+
✅ CORRECT: { "op": "count", "alias": "记录数" }
|
|
33
|
+
✅ CORRECT: { "op": "sum", "field_id": 7, "alias": "总金额" }
|
|
34
|
+
❌ WRONG: { "type": "count" } ← "type" is NOT a valid key
|
|
35
|
+
❌ WRONG: { "agg": "count" } ← "agg" is NOT a valid key
|
|
36
|
+
❌ WRONG: { "aggregation": "count" } ← "aggregation" is NOT a valid key
|
|
37
|
+
```
|
|
171
38
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
"
|
|
175
|
-
"
|
|
176
|
-
{ "
|
|
177
|
-
|
|
178
|
-
"filters": [],
|
|
179
|
-
"sort": [],
|
|
180
|
-
"limit": 1,
|
|
181
|
-
"strict_full": true
|
|
182
|
-
}
|
|
39
|
+
**filter item — the key is `op`, NOT `operator`:**
|
|
40
|
+
```
|
|
41
|
+
✅ CORRECT: { "field_id": 2, "op": "between", "value": ["2024-03-01", "2024-03-31"] }
|
|
42
|
+
✅ CORRECT: { "field_id": 5, "op": "eq", "value": "已完成" }
|
|
43
|
+
❌ WRONG: { "field_id": 2, "operator": "between", "value": [...] } ← "operator" is forbidden
|
|
44
|
+
❌ WRONG: { "field_id": 2, "op": ">=", "value": "2024-03-01" } ← ">=" is not valid, use "gte"
|
|
183
45
|
```
|
|
184
46
|
|
|
185
|
-
|
|
47
|
+
**sort item:**
|
|
48
|
+
```
|
|
49
|
+
✅ CORRECT: { "by": "记录数", "order": "desc" } ← "by" references an alias
|
|
50
|
+
❌ WRONG: { "by": 9500572, "order": "desc" } ← field_id not allowed in sort
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Allowed keys per item (ANY other key = error)
|
|
54
|
+
|
|
55
|
+
| Item | Allowed keys only |
|
|
56
|
+
|------|-------------------|
|
|
57
|
+
| dimension | `field_id`, `alias`, `bucket` |
|
|
58
|
+
| metric | `op`, `field_id`, `alias` |
|
|
59
|
+
| filter | `field_id`, `op`, `value` |
|
|
60
|
+
| sort | `by`, `order` |
|
|
61
|
+
|
|
62
|
+
### `op` values
|
|
63
|
+
|
|
64
|
+
- metrics: `count`, `sum`, `avg`, `min`, `max`, `distinct_count`
|
|
65
|
+
- filters: `eq`, `neq`, `in`, `not_in`, `gt`, `gte`, `lt`, `lte`, `between`, `contains`, `is_null`, `not_null`
|
|
66
|
+
- For `count` metric: do NOT pass `field_id`. For all others: `field_id` is required.
|
|
67
|
+
- If `metrics` is omitted or `[]`, defaults to `[{"op":"count","alias":"记录数"}]`.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## COMPLETE DSL TEMPLATE — copy, replace field_id, done
|
|
186
72
|
|
|
187
73
|
```json
|
|
188
74
|
{
|
|
75
|
+
"app_key": "YOUR_APP_KEY",
|
|
189
76
|
"dimensions": [
|
|
190
|
-
{ "field_id":
|
|
77
|
+
{ "field_id": FIELD_ID_FROM_SCHEMA, "alias": "维度名" }
|
|
191
78
|
],
|
|
192
79
|
"metrics": [
|
|
193
80
|
{ "op": "count", "alias": "记录数" }
|
|
194
81
|
],
|
|
195
|
-
"filters": [
|
|
82
|
+
"filters": [
|
|
83
|
+
{ "field_id": TIME_FIELD_ID, "op": "between", "value": ["2024-03-01", "2024-03-31"] }
|
|
84
|
+
],
|
|
196
85
|
"sort": [
|
|
197
86
|
{ "by": "记录数", "order": "desc" }
|
|
198
87
|
],
|
|
@@ -201,67 +90,73 @@ Single-dimension distribution:
|
|
|
201
90
|
}
|
|
202
91
|
```
|
|
203
92
|
|
|
204
|
-
|
|
93
|
+
More templates:
|
|
205
94
|
|
|
95
|
+
**Whole-table count (no grouping):**
|
|
96
|
+
```json
|
|
97
|
+
{ "dimensions": [], "metrics": [{"op":"count","alias":"记录数"}], "strict_full": true }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Monthly trend:**
|
|
206
101
|
```json
|
|
207
102
|
{
|
|
208
|
-
"dimensions": [
|
|
209
|
-
|
|
210
|
-
],
|
|
211
|
-
"
|
|
212
|
-
{ "op": "count", "alias": "记录数" }
|
|
213
|
-
],
|
|
214
|
-
"filters": [],
|
|
215
|
-
"sort": [
|
|
216
|
-
{ "by": "月份", "order": "asc" }
|
|
217
|
-
],
|
|
218
|
-
"limit": 24,
|
|
219
|
-
"strict_full": true
|
|
103
|
+
"dimensions": [{"field_id": 3, "alias": "月份", "bucket":"month"}],
|
|
104
|
+
"metrics": [{"op":"count","alias":"记录数"}],
|
|
105
|
+
"sort": [{"by":"月份","order":"asc"}],
|
|
106
|
+
"limit": 24, "strict_full": true
|
|
220
107
|
}
|
|
221
108
|
```
|
|
222
109
|
|
|
223
|
-
|
|
224
|
-
|
|
110
|
+
**Cross analysis with sum:**
|
|
225
111
|
```json
|
|
226
112
|
{
|
|
227
|
-
"dimensions": [
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
"metrics": [
|
|
232
|
-
{ "op": "count", "alias": "记录数" },
|
|
233
|
-
{ "op": "sum", "field_id": 7, "alias": "总金额" }
|
|
234
|
-
],
|
|
235
|
-
"filters": [],
|
|
236
|
-
"sort": [
|
|
237
|
-
{ "by": "记录数", "order": "desc" }
|
|
238
|
-
],
|
|
239
|
-
"limit": 100,
|
|
240
|
-
"strict_full": true
|
|
113
|
+
"dimensions": [{"field_id": 2, "alias": "状态"}, {"field_id": 5, "alias": "负责人"}],
|
|
114
|
+
"metrics": [{"op":"count","alias":"记录数"}, {"op":"sum","field_id": 7, "alias":"总金额"}],
|
|
115
|
+
"sort": [{"by":"记录数","order":"desc"}],
|
|
116
|
+
"limit": 100, "strict_full": true
|
|
241
117
|
}
|
|
242
118
|
```
|
|
243
119
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
-
|
|
247
|
-
-
|
|
248
|
-
-
|
|
249
|
-
-
|
|
250
|
-
-
|
|
251
|
-
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
120
|
+
### Top-level arguments
|
|
121
|
+
|
|
122
|
+
- `app_key`: required.
|
|
123
|
+
- `dimensions`: `[]` = whole-table summary; `[{...}]` = grouped.
|
|
124
|
+
- `strict_full`: `true` for final conclusions. `false` allows partial results.
|
|
125
|
+
- `limit`: limits returned rows only, not scan scope.
|
|
126
|
+
- `view_key`/`view_name`: optional scope narrowing.
|
|
127
|
+
- `bucket` in dimensions: only for `suggested_time_fields`. Values: `day`/`week`/`month`/`quarter`/`year`/`null`.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## RULES
|
|
132
|
+
|
|
133
|
+
- All `field_id` MUST come from `record_schema_get`. Never guess or use field titles.
|
|
134
|
+
- One DSL per question. Multiple small DSLs > one overloaded request.
|
|
135
|
+
- Normalize relative dates to concrete ranges BEFORE building DSL. Never send impossible dates (e.g. `2026-02-29`).
|
|
136
|
+
- If schema has ambiguous fields, ask user to pick from a short list. Do not guess.
|
|
137
|
+
- `record_list` is NEVER the basis for final statistics.
|
|
138
|
+
- Derived ratios: run numerator and denominator as separate DSLs, compute ratio in your reasoning.
|
|
139
|
+
- Set `alias` for any metric you will sort by, compare, or quote.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## OUTPUT (CRITICAL — final answer must show concrete numbers)
|
|
144
|
+
|
|
145
|
+
### 必须逐行列出数据(硬性要求)
|
|
146
|
+
|
|
147
|
+
final_answer MUST include a table with every row from `result.rows`:
|
|
148
|
+
|
|
149
|
+
| {维度别名} | {指标别名} | 占比 |
|
|
150
|
+
|------------|-----------|------|
|
|
151
|
+
| {row.dimensions.X} | {row.metrics.Y} | {Y / total * 100}% |
|
|
152
|
+
|
|
153
|
+
- 占比 = 行指标值 / `result.totals.metric_totals` 总值, 保留一位小数
|
|
154
|
+
- 如 `metric_totals` 不存在, 用各行之和作分母
|
|
155
|
+
- 超过 20 行展示 Top 20 并注明
|
|
156
|
+
- 不得只写"共 N 种类型"而省略明细
|
|
157
|
+
|
|
158
|
+
### 结论分级
|
|
159
|
+
|
|
160
|
+
- `safe_for_final_conclusion=true` → `全量可信结论`
|
|
161
|
+
- 不完整 → `初步观察`
|
|
162
|
+
- `rows_truncated=true` → 用 `前 N 个分组`, 不用 `全部`/`所有`
|
|
@@ -38,6 +38,7 @@ ATTACHMENT_QUE_TYPES = {13}
|
|
|
38
38
|
RELATION_QUE_TYPES = {25}
|
|
39
39
|
SUBTABLE_QUE_TYPES = {18}
|
|
40
40
|
VERIFY_UNSUPPORTED_WRITE_QUE_TYPES = {14, 34, 35, 36}
|
|
41
|
+
LAYOUT_ONLY_QUE_TYPES = {24}
|
|
41
42
|
DEPARTMENT_MEMBER_JUDGE_PREFIX = "deptId_"
|
|
42
43
|
JUDGE_EQUAL = 0
|
|
43
44
|
JUDGE_UNEQUAL = 1
|
|
@@ -1131,10 +1132,12 @@ class RecordTools(ToolBase):
|
|
|
1131
1132
|
self._ensure_allowed_analyze_keys(
|
|
1132
1133
|
item,
|
|
1133
1134
|
location=f"metrics[{idx}]",
|
|
1134
|
-
allowed_keys={"op", "field_id", "fieldId", "alias"},
|
|
1135
|
+
allowed_keys={"op", "type", "agg", "aggregation", "field_id", "fieldId", "alias"},
|
|
1135
1136
|
example="{'op': 'sum', 'field_id': 7, 'alias': '总金额'}",
|
|
1136
1137
|
)
|
|
1137
|
-
op = _normalize_optional_text(
|
|
1138
|
+
op = _normalize_optional_text(
|
|
1139
|
+
item.get("op") or item.get("type") or item.get("agg") or item.get("aggregation")
|
|
1140
|
+
)
|
|
1138
1141
|
if op not in supported_ops:
|
|
1139
1142
|
raise RecordInputError(
|
|
1140
1143
|
message=f"metrics[{idx}] uses unsupported op '{op}'",
|
|
@@ -1153,12 +1156,8 @@ class RecordTools(ToolBase):
|
|
|
1153
1156
|
details={"location": f"metrics[{idx}]", "field": _field_ref_payload(field), "op": op},
|
|
1154
1157
|
)
|
|
1155
1158
|
elif item.get("field_id", item.get("fieldId")) is not None:
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
error_code="INVALID_ANALYZE_METRIC",
|
|
1159
|
-
fix_hint="Remove field_id from count metrics.",
|
|
1160
|
-
details={"location": f"metrics[{idx}]", "op": op},
|
|
1161
|
-
)
|
|
1159
|
+
# LLM 经常给 count 传 field_id,静默忽略而非报错
|
|
1160
|
+
pass
|
|
1162
1161
|
alias = _normalize_optional_text(item.get("alias"))
|
|
1163
1162
|
if alias is None:
|
|
1164
1163
|
if op == "count":
|
|
@@ -3995,16 +3994,19 @@ def _build_field_index(schema: JSONObject) -> FieldIndex:
|
|
|
3995
3994
|
*[(question, False) for question in _flatten_questions(schema.get("formQues"))],
|
|
3996
3995
|
]
|
|
3997
3996
|
for question, is_base_question in all_questions:
|
|
3997
|
+
if not _should_index_question(question):
|
|
3998
|
+
continue
|
|
3998
3999
|
que_id = _coerce_count(question.get("queId"))
|
|
3999
4000
|
title = _stringify_json(question.get("queTitle")).strip()
|
|
4000
4001
|
if que_id is None or que_id < 0 or not title:
|
|
4001
4002
|
continue
|
|
4003
|
+
can_edit = question.get("canEdit")
|
|
4002
4004
|
field = FormField(
|
|
4003
4005
|
que_id=que_id,
|
|
4004
4006
|
que_title=title,
|
|
4005
4007
|
que_type=_coerce_count(question.get("queType")),
|
|
4006
4008
|
required=bool(question.get("required") or question.get("beingRequired")),
|
|
4007
|
-
readonly=bool(question.get("readonly") or question.get("beingReadonly") or is_base_question),
|
|
4009
|
+
readonly=bool(question.get("readonly") or question.get("beingReadonly") or is_base_question or can_edit is False),
|
|
4008
4010
|
system=bool(question.get("system") or question.get("beingSystem") or is_base_question),
|
|
4009
4011
|
options=_extract_question_options(question),
|
|
4010
4012
|
aliases=[],
|
|
@@ -4023,16 +4025,35 @@ def _build_field_index(schema: JSONObject) -> FieldIndex:
|
|
|
4023
4025
|
def _flatten_questions(payload: JSONValue) -> list[JSONObject]:
|
|
4024
4026
|
flattened: list[JSONObject] = []
|
|
4025
4027
|
if isinstance(payload, dict):
|
|
4026
|
-
|
|
4028
|
+
is_question = "queId" in payload or "queTitle" in payload
|
|
4029
|
+
if is_question:
|
|
4027
4030
|
flattened.append(payload)
|
|
4028
|
-
for
|
|
4029
|
-
|
|
4031
|
+
for key in ("subQuestions", "innerQuestions", "subQues"):
|
|
4032
|
+
value = payload.get(key)
|
|
4033
|
+
if isinstance(value, list):
|
|
4034
|
+
flattened.extend(_flatten_questions(value))
|
|
4035
|
+
if not is_question:
|
|
4036
|
+
for key in ("baseQues", "formQues"):
|
|
4037
|
+
value = payload.get(key)
|
|
4038
|
+
if isinstance(value, list):
|
|
4039
|
+
flattened.extend(_flatten_questions(value))
|
|
4030
4040
|
elif isinstance(payload, list):
|
|
4031
4041
|
for item in payload:
|
|
4032
4042
|
flattened.extend(_flatten_questions(item))
|
|
4033
4043
|
return flattened
|
|
4034
4044
|
|
|
4035
4045
|
|
|
4046
|
+
def _should_index_question(question: JSONObject) -> bool:
|
|
4047
|
+
if bool(question.get("beingHide") or question.get("hidden")):
|
|
4048
|
+
return False
|
|
4049
|
+
if _coerce_count(question.get("quoteId")) is not None:
|
|
4050
|
+
return False
|
|
4051
|
+
que_type = _coerce_count(question.get("queType"))
|
|
4052
|
+
if que_type in LAYOUT_ONLY_QUE_TYPES:
|
|
4053
|
+
return False
|
|
4054
|
+
return True
|
|
4055
|
+
|
|
4056
|
+
|
|
4036
4057
|
def _extract_question_options(question: JSONObject) -> list[str]:
|
|
4037
4058
|
options = question.get("options")
|
|
4038
4059
|
if not isinstance(options, list):
|