@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 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.23
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.23 qingflow-app-user-mcp
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-app-user-mcp",
3
- "version": "0.2.0-beta.23",
3
+ "version": "0.2.0-beta.24",
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.0b23"
7
+ version = "0.2.0b24"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -7,192 +7,81 @@ metadata:
7
7
 
8
8
  # Qingflow Record Analysis
9
9
 
10
- ## Overview
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
- ```json
104
- {
105
- "app_key": "APP_1",
106
- "dimensions": [],
107
- "metrics": [],
108
- "filters": [],
109
- "sort": [],
110
- "limit": 50,
111
- "strict_full": true,
112
- "view_key": null,
113
- "view_name": null,
114
- "output_profile": "normal"
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
- Top-level argument rules:
119
-
120
- - `app_key`: required. The target Qingflow app.
121
- - `dimensions`: required list. Use `[]` for whole-table summary. Use one item per grouping dimension for grouped analysis.
122
- - `metrics`: optional list. If omitted or empty, `record_analyze` defaults to a single `count` metric.
123
- - `filters`: optional list. Filters restrict the analyzed dataset before results are interpreted.
124
- - `sort`: optional list. Sorting applies to result rows, not raw source rows.
125
- - `limit`: positive integer. It only limits returned result rows; it does not reduce the internal scan scope.
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
- ```json
173
- {
174
- "dimensions": [],
175
- "metrics": [
176
- { "op": "count", "alias": "记录数" }
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
- Single-dimension distribution:
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": 2, "alias": "状态" }
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
- Time trend:
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
- { "field_id": 3, "alias": "月份", "bucket": "month" }
210
- ],
211
- "metrics": [
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
- Two-dimensional cross analysis:
224
-
110
+ **Cross analysis with sum:**
225
111
  ```json
226
112
  {
227
- "dimensions": [
228
- { "field_id": 2, "alias": "状态" },
229
- { "field_id": 5, "alias": "负责人" }
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
- ## Output Gate
245
-
246
- - Read aggregate rows from `result.rows`
247
- - Read overall totals from `result.totals.metric_totals`
248
- - Read sort intent from `query.sort`
249
- - Read ranked output from `ranking` when it is not `null`
250
- - Read ratio output from `ratios` when it is not `null`; `ratios=null` is normal when MCP did not produce a native ratio block
251
- - Read warning codes from `completeness.warnings`
252
-
253
- - Only write `全量可信结论` when the supporting `record_analyze` calls report `completeness.status=complete` and `safe_for_final_conclusion=true`
254
- - If any key analysis call is incomplete, downgrade the answer to `初步观察` or `部分结果`
255
- - Treat `safe_for_final_conclusion=true` as necessary but not sufficient when the metric definition is incomplete or grouped rows are truncated
256
- - If `completeness.statement_scope=returned_groups_only`, you may still give full-population conclusions about totals or ratios, but not a full grouped enumeration claim
257
- - If aggregate-style output is full but list evidence is sample-only, split the answer into:
258
- - `全量可信结论`
259
- - `样本观察(不作为最终结论)`
260
- - optional `待验证假设`
261
-
262
- ## Resources
263
-
264
- - Analysis patterns: [references/analysis-patterns.md](references/analysis-patterns.md)
265
- - Confidence reporting: [references/confidence-reporting.md](references/confidence-reporting.md)
266
- - Analysis gotchas: [references/analysis-gotchas.md](references/analysis-gotchas.md)
267
- - Shared environment guidance: [/Users/yanqidong/Documents/qingflow-next/.codex/skills/qingflow-app-user/references/environments.md](/Users/yanqidong/Documents/qingflow-next/.codex/skills/qingflow-app-user/references/environments.md)
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(item.get("op"))
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
- raise RecordInputError(
1157
- message=f"metrics[{idx}] with op 'count' must not include field_id",
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
- if "queId" in payload or "queTitle" in payload:
4028
+ is_question = "queId" in payload or "queTitle" in payload
4029
+ if is_question:
4027
4030
  flattened.append(payload)
4028
- for value in payload.values():
4029
- flattened.extend(_flatten_questions(value))
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):