@josephyan/qingflow-app-user-mcp 1.0.7 → 1.0.8
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/public-surface-sync.md +1 -0
- package/skills/qingflow-record-analysis/SKILL.md +2 -1
- package/skills/qingflow-record-analysis/references/analysis-patterns.md +1 -1
- package/src/qingflow_mcp/server_app_user.py +3 -1
- package/src/qingflow_mcp/tools/record_tools.py +190 -46
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @josephyan/qingflow-app-user-mcp@1.0.
|
|
6
|
+
npm install @josephyan/qingflow-app-user-mcp@1.0.8
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @josephyan/qingflow-app-user-mcp@1.0.
|
|
12
|
+
npx -y -p @josephyan/qingflow-app-user-mcp@1.0.8 qingflow-app-user-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -8,6 +8,7 @@ It is not a user-facing product spec. It exists to prevent skill drift.
|
|
|
8
8
|
### User data
|
|
9
9
|
|
|
10
10
|
- Read range first with `app_get`, then `record_browse_schema_get(view_id=...)`
|
|
11
|
+
- Treat `record_browse_schema_get.fields` as the selected Qingflow table view header schema; `record_access.fields` and `schema.json` must stay aligned with it.
|
|
11
12
|
- Standard flows:
|
|
12
13
|
- analyze: `app_get -> record_browse_schema_get -> record_access -> Python`
|
|
13
14
|
- browse detail: `app_get -> record_browse_schema_get -> record_list / record_get`
|
|
@@ -9,7 +9,7 @@ metadata:
|
|
|
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 choose field_id-based columns and filters only.
|
|
12
|
+
Analysis tasks must start with `app_get`, then `record_browse_schema_get(view_id=...)`. `record_browse_schema_get.fields` is the selected Qingflow table view's readable header schema and is the same field source used by `record_access.fields` / `schema.json`. Read top-level `fields` and `suggested_*`, then choose field_id-based columns and filters only.
|
|
13
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.
|
|
14
14
|
|
|
15
15
|
## Step 1: `app_get` → Step 2: `record_browse_schema_get(view_id=...)` → Step 3: `record_access` → Step 4: Python
|
|
@@ -76,6 +76,7 @@ Use `record_access` to fetch detail rows into local CSV shards. It does not anal
|
|
|
76
76
|
Then run Python against every `files[].local_path`. CSV columns are stable: `record_id`, then `field_<field_id>`. Use `fields[]` plus `metadata_files.schema` / `metadata_files.readme` in the same output directory to map titles and types, especially when reusing the CSV later.
|
|
77
77
|
|
|
78
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
|
+
- Never treat backend `searchQueIds` as column selection; it is only a full-text search scope.
|
|
79
80
|
- If multiple CSV files are returned, read them all.
|
|
80
81
|
- If `complete=false` or `safe_for_final_conclusion=false`, downgrade the answer and disclose the limitation.
|
|
81
82
|
- `record_export_direct` is only for explicit export/download/Excel requests, not default analysis.
|
|
@@ -33,7 +33,7 @@ Result reading order:
|
|
|
33
33
|
6. `record_access.fields`
|
|
34
34
|
7. `record_access.warnings`
|
|
35
35
|
|
|
36
|
-
Treat `record_browse_schema_get` as the browse-schema source of truth. Missing fields are permission boundaries, not invitations to guess hidden ids.
|
|
36
|
+
Treat `record_browse_schema_get` as the browse-schema source of truth. It matches the selected Qingflow table view header and is the same schema source used by `record_access.fields` and `schema.json`. Missing fields are permission boundaries, not invitations to guess hidden ids.
|
|
37
37
|
|
|
38
38
|
## Lightweight `record_analyze` helper sequence
|
|
39
39
|
|
|
@@ -60,7 +60,9 @@ Call `record_import_schema_get` when the import field mapping is unclear before
|
|
|
60
60
|
`record_insert_schema_get` returns the current user's insert-ready applicant schema; read `required_fields`, `optional_fields`, `runtime_linked_required_fields`, and `payload_template`.
|
|
61
61
|
Inside `optional_fields`, any field with `may_become_required=true` is still writable, but may become required when linked visibility or option-driven runtime rules activate.
|
|
62
62
|
`record_update_schema_get` returns the current record's overall update-ready writable field set across matched accessible views; read `writable_fields` and `payload_template`.
|
|
63
|
-
`record_browse_schema_get(view_id=...)` returns
|
|
63
|
+
`record_browse_schema_get(view_id=...)` returns the same readable fields shown in the selected Qingflow table view header.
|
|
64
|
+
`record_access.fields` and its `schema.json` use that exact same view schema; a missing field means it is not readable in that view.
|
|
65
|
+
`searchQueIds` is a backend full-text search scope, not an output-column/projection mechanism.
|
|
64
66
|
`record_code_block_schema_get` returns code-block-ready schema for exact code block field selection.
|
|
65
67
|
`record_import_schema_get` returns import-ready column metadata.
|
|
66
68
|
|
|
@@ -602,7 +602,7 @@ class RecordTools(ToolBase):
|
|
|
602
602
|
),
|
|
603
603
|
}
|
|
604
604
|
)
|
|
605
|
-
browse_scope = self.
|
|
605
|
+
browse_scope = self._build_browse_read_scope(
|
|
606
606
|
profile,
|
|
607
607
|
context,
|
|
608
608
|
app_key,
|
|
@@ -1848,25 +1848,20 @@ class RecordTools(ToolBase):
|
|
|
1848
1848
|
sorts = self._normalize_record_list_order_by(order_by)
|
|
1849
1849
|
|
|
1850
1850
|
def runner(session_profile, context):
|
|
1851
|
-
|
|
1852
|
-
|
|
1851
|
+
browse_scope = self._build_browse_read_scope(
|
|
1852
|
+
profile,
|
|
1853
|
+
context,
|
|
1854
|
+
app_key,
|
|
1855
|
+
view_route,
|
|
1856
|
+
force_refresh=False,
|
|
1857
|
+
)
|
|
1858
|
+
index = cast(FieldIndex, browse_scope["index"])
|
|
1859
|
+
selected_fields = self._resolve_record_access_columns(
|
|
1853
1860
|
normalized_columns,
|
|
1854
1861
|
index,
|
|
1855
|
-
|
|
1856
|
-
default_limit=MAX_LIST_COLUMN_LIMIT,
|
|
1862
|
+
view_route=view_route,
|
|
1857
1863
|
)
|
|
1858
|
-
selected_field_batches = _chunk_fields(selected_fields, BACKEND_LIST_SEARCH_FIELD_LIMIT)
|
|
1859
|
-
primary_search_que_ids: list[int] | None = [field.que_id for field in selected_field_batches[0]]
|
|
1860
1864
|
view_selection = view_route.view_selection
|
|
1861
|
-
if view_selection is not None and not _view_selection_supported_by_search_ids(
|
|
1862
|
-
view_selection,
|
|
1863
|
-
primary_search_que_ids,
|
|
1864
|
-
):
|
|
1865
|
-
primary_search_que_ids = None
|
|
1866
|
-
remaining_field_batches: list[list[FormField]] = []
|
|
1867
|
-
else:
|
|
1868
|
-
remaining_field_batches = selected_field_batches[1:]
|
|
1869
|
-
primary_search_que_ids = primary_search_que_ids or None
|
|
1870
1865
|
match_rules = self._resolve_match_rules(context, filters, index)
|
|
1871
1866
|
sort_rules = self._resolve_sorts(sorts, index)
|
|
1872
1867
|
used_list_type: int | None = None
|
|
@@ -1949,7 +1944,7 @@ class RecordTools(ToolBase):
|
|
|
1949
1944
|
query_key=None,
|
|
1950
1945
|
match_rules=match_rules,
|
|
1951
1946
|
sorts=sort_rules,
|
|
1952
|
-
search_que_ids=
|
|
1947
|
+
search_que_ids=None,
|
|
1953
1948
|
list_type=candidate_list_type,
|
|
1954
1949
|
)
|
|
1955
1950
|
used_list_type = None if view_selection is not None else candidate_list_type
|
|
@@ -1976,7 +1971,7 @@ class RecordTools(ToolBase):
|
|
|
1976
1971
|
query_key=None,
|
|
1977
1972
|
match_rules=match_rules,
|
|
1978
1973
|
sorts=sort_rules,
|
|
1979
|
-
search_que_ids=
|
|
1974
|
+
search_que_ids=None,
|
|
1980
1975
|
list_type=used_list_type,
|
|
1981
1976
|
)
|
|
1982
1977
|
page_rows = page.get("list")
|
|
@@ -1996,34 +1991,6 @@ class RecordTools(ToolBase):
|
|
|
1996
1991
|
continue
|
|
1997
1992
|
page_apply_order.append(apply_id)
|
|
1998
1993
|
page_answer_map[apply_id] = cast(list[JSONValue], answer_list)
|
|
1999
|
-
if page_apply_order and remaining_field_batches:
|
|
2000
|
-
for batch in remaining_field_batches:
|
|
2001
|
-
extra_page = self._search_page(
|
|
2002
|
-
context,
|
|
2003
|
-
app_key=app_key,
|
|
2004
|
-
view_selection=view_selection,
|
|
2005
|
-
page_num=current_page,
|
|
2006
|
-
page_size=BACKEND_RECORD_ACCESS_PAGE_SIZE,
|
|
2007
|
-
query_key=None,
|
|
2008
|
-
match_rules=match_rules,
|
|
2009
|
-
sorts=sort_rules,
|
|
2010
|
-
search_que_ids=[field.que_id for field in batch],
|
|
2011
|
-
list_type=used_list_type or DEFAULT_RECORD_LIST_TYPE,
|
|
2012
|
-
)
|
|
2013
|
-
extra_rows = extra_page.get("list")
|
|
2014
|
-
extra_items = extra_rows if isinstance(extra_rows, list) else []
|
|
2015
|
-
for extra_item in extra_items:
|
|
2016
|
-
if not isinstance(extra_item, dict):
|
|
2017
|
-
continue
|
|
2018
|
-
apply_id = _coerce_count(extra_item.get("applyId")) or _coerce_count(extra_item.get("id"))
|
|
2019
|
-
if apply_id is None or apply_id not in page_answer_map:
|
|
2020
|
-
continue
|
|
2021
|
-
extra_answers = extra_item.get("answers")
|
|
2022
|
-
extra_answer_list = extra_answers if isinstance(extra_answers, list) else []
|
|
2023
|
-
page_answer_map[apply_id] = _merge_answer_lists_by_field_id(
|
|
2024
|
-
page_answer_map.get(apply_id, []),
|
|
2025
|
-
cast(list[JSONValue], extra_answer_list),
|
|
2026
|
-
)
|
|
2027
1994
|
for apply_id in page_apply_order:
|
|
2028
1995
|
write_record(apply_id, page_answer_map.get(apply_id, []))
|
|
2029
1996
|
if not has_more:
|
|
@@ -7427,6 +7394,39 @@ class RecordTools(ToolBase):
|
|
|
7427
7394
|
self._form_cache[cache_key] = normalized
|
|
7428
7395
|
return normalized
|
|
7429
7396
|
|
|
7397
|
+
def _get_system_browse_base_info_schema(
|
|
7398
|
+
self,
|
|
7399
|
+
profile: str,
|
|
7400
|
+
context, # type: ignore[no-untyped-def]
|
|
7401
|
+
app_key: str,
|
|
7402
|
+
*,
|
|
7403
|
+
list_type: int,
|
|
7404
|
+
force_refresh: bool,
|
|
7405
|
+
) -> JSONObject:
|
|
7406
|
+
"""Return system view apply/baseInfo without falling back to app form metadata."""
|
|
7407
|
+
cache_key = (profile, app_key, "browse_system_base_info", list_type)
|
|
7408
|
+
if not force_refresh and cache_key in self._form_cache:
|
|
7409
|
+
return self._form_cache[cache_key]
|
|
7410
|
+
payload = self.backend.request(
|
|
7411
|
+
"GET",
|
|
7412
|
+
context,
|
|
7413
|
+
f"/app/{app_key}/apply/baseInfo",
|
|
7414
|
+
params={"type": list_type},
|
|
7415
|
+
)
|
|
7416
|
+
normalized = _normalize_data_list_base_info_schema(payload)
|
|
7417
|
+
self._form_cache[cache_key] = normalized
|
|
7418
|
+
return normalized
|
|
7419
|
+
|
|
7420
|
+
def _get_custom_view_browse_schema(self, profile: str, context, view_key: str, *, force_refresh: bool) -> JSONObject: # type: ignore[no-untyped-def]
|
|
7421
|
+
"""Return the same baseInfo schema used by the Qingflow table view UI."""
|
|
7422
|
+
cache_key = (profile, f"view:{view_key}", "browse_view_base_info", None)
|
|
7423
|
+
if not force_refresh and cache_key in self._form_cache:
|
|
7424
|
+
return self._form_cache[cache_key]
|
|
7425
|
+
payload = self.backend.request("GET", context, f"/view/{view_key}/apply/baseInfo")
|
|
7426
|
+
normalized = _normalize_data_list_base_info_schema(payload)
|
|
7427
|
+
self._form_cache[cache_key] = normalized
|
|
7428
|
+
return normalized
|
|
7429
|
+
|
|
7430
7430
|
def _get_view_field_index(self, profile: str, context, view_key: str, *, force_refresh: bool) -> FieldIndex: # type: ignore[no-untyped-def]
|
|
7431
7431
|
"""执行内部辅助逻辑。"""
|
|
7432
7432
|
return _build_field_index(self._get_view_form_schema(profile, context, view_key, force_refresh=force_refresh))
|
|
@@ -7469,6 +7469,80 @@ class RecordTools(ToolBase):
|
|
|
7469
7469
|
force_refresh=force_refresh,
|
|
7470
7470
|
)["index"]
|
|
7471
7471
|
|
|
7472
|
+
def _build_browse_read_scope(
|
|
7473
|
+
self,
|
|
7474
|
+
profile: str,
|
|
7475
|
+
context, # type: ignore[no-untyped-def]
|
|
7476
|
+
app_key: str,
|
|
7477
|
+
resolved_view: AccessibleViewRoute | None,
|
|
7478
|
+
*,
|
|
7479
|
+
force_refresh: bool,
|
|
7480
|
+
) -> JSONObject:
|
|
7481
|
+
"""Build the UI/table-view readable field scope from apply/baseInfo."""
|
|
7482
|
+
applicant_index: FieldIndex | None
|
|
7483
|
+
applicant_writable_field_ids: set[int]
|
|
7484
|
+
try:
|
|
7485
|
+
applicant_index = self._get_applicant_top_level_field_index(profile, context, app_key, force_refresh=force_refresh)
|
|
7486
|
+
except QingflowApiError as exc:
|
|
7487
|
+
if exc.backend_code != 40002:
|
|
7488
|
+
raise
|
|
7489
|
+
applicant_index = None
|
|
7490
|
+
applicant_writable_field_ids = set()
|
|
7491
|
+
else:
|
|
7492
|
+
applicant_writable_field_ids = {
|
|
7493
|
+
field.que_id
|
|
7494
|
+
for field in applicant_index.by_id.values()
|
|
7495
|
+
if bool(self._schema_write_hints(field)["writable"])
|
|
7496
|
+
}
|
|
7497
|
+
|
|
7498
|
+
if resolved_view is not None and resolved_view.kind == "custom" and resolved_view.view_selection is not None:
|
|
7499
|
+
schema = self._get_custom_view_browse_schema(
|
|
7500
|
+
profile,
|
|
7501
|
+
context,
|
|
7502
|
+
resolved_view.view_selection.view_key,
|
|
7503
|
+
force_refresh=force_refresh,
|
|
7504
|
+
)
|
|
7505
|
+
index = _build_top_level_field_index(schema)
|
|
7506
|
+
elif resolved_view is not None and resolved_view.kind == "system" and resolved_view.list_type is not None:
|
|
7507
|
+
schema = self._get_system_browse_base_info_schema(
|
|
7508
|
+
profile,
|
|
7509
|
+
context,
|
|
7510
|
+
app_key,
|
|
7511
|
+
list_type=resolved_view.list_type,
|
|
7512
|
+
force_refresh=force_refresh,
|
|
7513
|
+
)
|
|
7514
|
+
index = _build_top_level_field_index(schema)
|
|
7515
|
+
else:
|
|
7516
|
+
index = applicant_index or _build_top_level_field_index(
|
|
7517
|
+
self._get_form_schema(profile, context, app_key, force_refresh=force_refresh)
|
|
7518
|
+
)
|
|
7519
|
+
|
|
7520
|
+
if applicant_index is not None and index.by_id:
|
|
7521
|
+
enriched_fields = [
|
|
7522
|
+
_enrich_read_field_from_applicant(field, applicant_index.by_id.get(str(field.que_id)))
|
|
7523
|
+
for field in index.by_id.values()
|
|
7524
|
+
]
|
|
7525
|
+
index = _build_top_level_field_index({"formQues": [[_form_field_to_question(field) for field in enriched_fields]]})
|
|
7526
|
+
|
|
7527
|
+
visible_question_ids = {field.que_id for field in index.by_id.values()}
|
|
7528
|
+
if applicant_index is None:
|
|
7529
|
+
writable_field_ids = {
|
|
7530
|
+
field.que_id
|
|
7531
|
+
for field in index.by_id.values()
|
|
7532
|
+
if bool(self._schema_write_hints(field)["writable"])
|
|
7533
|
+
}
|
|
7534
|
+
else:
|
|
7535
|
+
writable_field_ids = {
|
|
7536
|
+
field_id
|
|
7537
|
+
for field_id in visible_question_ids
|
|
7538
|
+
if field_id in applicant_writable_field_ids
|
|
7539
|
+
}
|
|
7540
|
+
return {
|
|
7541
|
+
"index": index,
|
|
7542
|
+
"writable_field_ids": writable_field_ids,
|
|
7543
|
+
"visible_question_ids": visible_question_ids,
|
|
7544
|
+
}
|
|
7545
|
+
|
|
7472
7546
|
def _build_browse_write_scope(
|
|
7473
7547
|
self,
|
|
7474
7548
|
profile: str,
|
|
@@ -9538,6 +9612,44 @@ class RecordTools(ToolBase):
|
|
|
9538
9612
|
break
|
|
9539
9613
|
return fields
|
|
9540
9614
|
|
|
9615
|
+
def _resolve_record_access_columns(
|
|
9616
|
+
self,
|
|
9617
|
+
selectors: list[int],
|
|
9618
|
+
index: FieldIndex,
|
|
9619
|
+
*,
|
|
9620
|
+
view_route: AccessibleViewRoute,
|
|
9621
|
+
) -> list[FormField]:
|
|
9622
|
+
"""Resolve record_access columns against the selected view's baseInfo schema."""
|
|
9623
|
+
if not selectors:
|
|
9624
|
+
raise_tool_error(QingflowApiError.config_error("columns is required"))
|
|
9625
|
+
fields: list[FormField] = []
|
|
9626
|
+
seen: set[int] = set()
|
|
9627
|
+
for selector in selectors:
|
|
9628
|
+
try:
|
|
9629
|
+
field = self._resolve_field_selector(selector, index, location="record_access.columns")
|
|
9630
|
+
except RecordInputError as exc:
|
|
9631
|
+
if exc.error_code == "FIELD_NOT_FOUND":
|
|
9632
|
+
raise RecordInputError(
|
|
9633
|
+
message=(
|
|
9634
|
+
f"record_access column field_id '{selector}' is not in the selected view schema "
|
|
9635
|
+
f"({view_route.view_id})."
|
|
9636
|
+
),
|
|
9637
|
+
error_code="FIELD_NOT_IN_VIEW_SCHEMA",
|
|
9638
|
+
fix_hint="Call record_browse_schema_get for this exact view_id and pass only field_id values from its fields[].",
|
|
9639
|
+
details={
|
|
9640
|
+
"location": "record_access.columns",
|
|
9641
|
+
"requested": selector,
|
|
9642
|
+
"view_id": view_route.view_id,
|
|
9643
|
+
"view_name": view_route.name,
|
|
9644
|
+
},
|
|
9645
|
+
) from exc
|
|
9646
|
+
raise
|
|
9647
|
+
if field.que_id in seen:
|
|
9648
|
+
continue
|
|
9649
|
+
fields.append(field)
|
|
9650
|
+
seen.add(field.que_id)
|
|
9651
|
+
return fields
|
|
9652
|
+
|
|
9541
9653
|
def _resolve_summary_preview_fields(
|
|
9542
9654
|
self,
|
|
9543
9655
|
selectors: list[str | int],
|
|
@@ -11295,6 +11407,38 @@ def _clone_form_field(field: FormField, *, readonly: bool | None = None) -> Form
|
|
|
11295
11407
|
)
|
|
11296
11408
|
|
|
11297
11409
|
|
|
11410
|
+
def _enrich_read_field_from_applicant(field: FormField, applicant_field: FormField | None) -> FormField:
|
|
11411
|
+
if applicant_field is None:
|
|
11412
|
+
return field
|
|
11413
|
+
raw = dict(applicant_field.raw) if isinstance(applicant_field.raw, dict) else {}
|
|
11414
|
+
raw.update(dict(field.raw) if isinstance(field.raw, dict) else {})
|
|
11415
|
+
raw["queId"] = field.que_id
|
|
11416
|
+
raw["queTitle"] = field.que_title
|
|
11417
|
+
que_type = field.que_type if field.que_type is not None else applicant_field.que_type
|
|
11418
|
+
if que_type is not None:
|
|
11419
|
+
raw["queType"] = que_type
|
|
11420
|
+
readonly_source = field if any(key in field.raw for key in ("readonly", "beingReadonly", "canEdit")) else applicant_field
|
|
11421
|
+
enriched = FormField(
|
|
11422
|
+
que_id=field.que_id,
|
|
11423
|
+
que_title=field.que_title,
|
|
11424
|
+
que_type=que_type,
|
|
11425
|
+
required=field.required or applicant_field.required,
|
|
11426
|
+
readonly=readonly_source.readonly,
|
|
11427
|
+
system=field.system or applicant_field.system,
|
|
11428
|
+
options=list(field.options or applicant_field.options),
|
|
11429
|
+
aliases=[],
|
|
11430
|
+
target_app_key=field.target_app_key or applicant_field.target_app_key,
|
|
11431
|
+
target_app_name_hint=field.target_app_name_hint or applicant_field.target_app_name_hint,
|
|
11432
|
+
member_select_scope_type=field.member_select_scope_type if field.member_select_scope_type is not None else applicant_field.member_select_scope_type,
|
|
11433
|
+
member_select_scope=field.member_select_scope or applicant_field.member_select_scope,
|
|
11434
|
+
dept_select_scope_type=field.dept_select_scope_type if field.dept_select_scope_type is not None else applicant_field.dept_select_scope_type,
|
|
11435
|
+
dept_select_scope=field.dept_select_scope or applicant_field.dept_select_scope,
|
|
11436
|
+
raw=raw,
|
|
11437
|
+
)
|
|
11438
|
+
enriched.aliases = sorted(_field_alias_candidates(enriched))
|
|
11439
|
+
return enriched
|
|
11440
|
+
|
|
11441
|
+
|
|
11298
11442
|
def _form_field_to_question(field: FormField) -> JSONObject:
|
|
11299
11443
|
question = dict(field.raw) if isinstance(field.raw, dict) else {}
|
|
11300
11444
|
question.setdefault("queId", field.que_id)
|