@josephyan/qingflow-app-user-mcp 0.2.0-beta.90 → 0.2.0-beta.92

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.90
6
+ npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.92
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.90 qingflow-app-user-mcp
12
+ npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.92 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.90",
3
+ "version": "0.2.0-beta.92",
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.0b87"
7
+ version = "0.2.0b92"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,5 +1,37 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from importlib.metadata import PackageNotFoundError, packages_distributions, version as _dist_version
4
+ from pathlib import Path
5
+
3
6
  __all__ = ["__version__"]
4
7
 
5
- __version__ = "0.2.0b87"
8
+ _FALLBACK_VERSION = "0.2.0b92"
9
+
10
+
11
+ def _resolve_local_pyproject_version() -> str | None:
12
+ module_path = Path(__file__).resolve()
13
+ for parent in module_path.parents:
14
+ candidate = parent / "pyproject.toml"
15
+ if not candidate.is_file():
16
+ continue
17
+ for line in candidate.read_text(encoding="utf-8").splitlines():
18
+ stripped = line.strip()
19
+ if stripped.startswith("version = "):
20
+ return stripped.split("=", 1)[1].strip().strip('"')
21
+ break
22
+ return None
23
+
24
+
25
+ def _resolve_runtime_version() -> str:
26
+ local_version = _resolve_local_pyproject_version()
27
+ if local_version:
28
+ return local_version
29
+ for dist_name in packages_distributions().get("qingflow_mcp", []):
30
+ try:
31
+ return _dist_version(dist_name)
32
+ except PackageNotFoundError:
33
+ continue
34
+ return _FALLBACK_VERSION
35
+
36
+
37
+ __version__ = _resolve_runtime_version()
@@ -9865,8 +9865,9 @@ def _parse_field(question: dict[str, Any], *, field_id_hint: str | None = None)
9865
9865
  "que_id": que_id,
9866
9866
  "que_type": que_type,
9867
9867
  "default_type": _coerce_positive_int(question.get("queDefaultType")) if "queDefaultType" in question else None,
9868
- "default_value": question.get("queDefaultValue") if "queDefaultValue" in question else None,
9869
9868
  }
9869
+ if "queDefaultValue" in question:
9870
+ field["default_value"] = question.get("queDefaultValue")
9870
9871
  if field_type in {FieldType.single_select.value, FieldType.multi_select.value, FieldType.boolean.value}:
9871
9872
  options = question.get("options")
9872
9873
  if isinstance(options, list):
@@ -12753,6 +12754,65 @@ def _field_needs_question_rebuild(field: dict[str, Any]) -> bool:
12753
12754
  return not isinstance(field.get("_question_template"), dict) or bool(field.get("_question_rebuild_required"))
12754
12755
 
12755
12756
 
12757
+ def _extract_template_row_lengths(schema: dict[str, Any]) -> tuple[dict[int, int], dict[str, int]]:
12758
+ lengths_by_que_id: dict[int, int] = {}
12759
+ lengths_by_title: dict[str, int] = {}
12760
+
12761
+ def remember_row(row: Any) -> None:
12762
+ if not isinstance(row, list):
12763
+ return
12764
+ questions = [question for question in row if isinstance(question, dict)]
12765
+ row_length = len(questions)
12766
+ if row_length <= 0:
12767
+ return
12768
+ for question in questions:
12769
+ que_id = _coerce_nonnegative_int(question.get("queId"))
12770
+ if que_id is not None:
12771
+ lengths_by_que_id[que_id] = row_length
12772
+ title = str(question.get("queTitle") or "").strip()
12773
+ if title:
12774
+ lengths_by_title[title] = row_length
12775
+
12776
+ for row in schema.get("formQues", []) or []:
12777
+ if not isinstance(row, list):
12778
+ continue
12779
+ if len(row) == 1 and isinstance(row[0], dict) and _coerce_positive_int(row[0].get("queType")) == 24:
12780
+ for inner_row in row[0].get("innerQuestions", []) or []:
12781
+ remember_row(inner_row)
12782
+ continue
12783
+ remember_row(row)
12784
+ return lengths_by_que_id, lengths_by_title
12785
+
12786
+
12787
+ def _field_template_row_length(
12788
+ field: dict[str, Any],
12789
+ *,
12790
+ lengths_by_que_id: dict[int, int],
12791
+ lengths_by_title: dict[str, int],
12792
+ ) -> int | None:
12793
+ que_id = _coerce_nonnegative_int(field.get("que_id"))
12794
+ if que_id is not None and que_id in lengths_by_que_id:
12795
+ return lengths_by_que_id[que_id]
12796
+ template = field.get("_question_template")
12797
+ if isinstance(template, dict):
12798
+ template_que_id = _coerce_nonnegative_int(template.get("queId"))
12799
+ if template_que_id is not None and template_que_id in lengths_by_que_id:
12800
+ return lengths_by_que_id[template_que_id]
12801
+ template_title = str(template.get("queTitle") or "").strip()
12802
+ if template_title and template_title in lengths_by_title:
12803
+ return lengths_by_title[template_title]
12804
+ field_name = str(field.get("name") or "").strip()
12805
+ if field_name and field_name in lengths_by_title:
12806
+ return lengths_by_title[field_name]
12807
+ return None
12808
+
12809
+
12810
+ def _row_needs_width_reflow(expected_template_lengths: list[int], current_row_length: int) -> bool:
12811
+ if current_row_length <= 0:
12812
+ return False
12813
+ return any(length != current_row_length for length in expected_template_lengths)
12814
+
12815
+
12756
12816
  def _materialize_preserved_question(field: dict[str, Any]) -> dict[str, Any] | None:
12757
12817
  template = deepcopy(field.get("_question_template"))
12758
12818
  if not isinstance(template, dict):
@@ -12802,12 +12862,21 @@ def _field_to_question(field: dict[str, Any], *, temp_id: int) -> dict[str, Any]
12802
12862
  },
12803
12863
  temp_id,
12804
12864
  )
12865
+ relation_config_explicit = bool(field.get("_relation_config_explicit"))
12805
12866
  relation_question_template = (
12806
12867
  deepcopy(field.get("_question_template"))
12807
12868
  if field.get("type") == FieldType.relation.value and isinstance(field.get("_question_template"), dict)
12808
12869
  else None
12809
12870
  )
12810
- question = relation_question_template if relation_question_template is not None else built_question
12871
+ question = (
12872
+ relation_question_template
12873
+ if relation_question_template is not None and not relation_config_explicit
12874
+ else built_question
12875
+ )
12876
+ if relation_config_explicit and relation_question_template is not None:
12877
+ for key in ("queOriginType", "relationDisplayMode", "customRenderConfig"):
12878
+ if key in relation_question_template:
12879
+ question[key] = deepcopy(relation_question_template[key])
12811
12880
  if _coerce_nonnegative_int(field.get("que_id")) is not None:
12812
12881
  question["queId"] = field["que_id"]
12813
12882
  question.pop("queTempId", None)
@@ -12826,19 +12895,28 @@ def _field_to_question(field: dict[str, Any], *, temp_id: int) -> dict[str, Any]
12826
12895
  if field.get("type") == FieldType.relation.value:
12827
12896
  preserved_reference = (
12828
12897
  deepcopy(field.get("_reference_config_template"))
12829
- if not bool(field.get("_relation_config_explicit")) and isinstance(field.get("_reference_config_template"), dict)
12898
+ if not relation_config_explicit and isinstance(field.get("_reference_config_template"), dict)
12830
12899
  else None
12831
12900
  )
12832
12901
  if preserved_reference is not None:
12833
12902
  preserved_reference["referAppKey"] = field.get("target_app_key")
12834
12903
  question["referenceConfig"] = preserved_reference
12835
12904
  else:
12836
- reference = deepcopy(question.get("referenceConfig")) if isinstance(question.get("referenceConfig"), dict) else {}
12905
+ reference = (
12906
+ deepcopy(built_question.get("referenceConfig"))
12907
+ if relation_config_explicit and isinstance(built_question.get("referenceConfig"), dict)
12908
+ else deepcopy(question.get("referenceConfig"))
12909
+ if isinstance(question.get("referenceConfig"), dict)
12910
+ else {}
12911
+ )
12837
12912
  built_reference = (
12838
12913
  deepcopy(built_question.get("referenceConfig"))
12839
12914
  if isinstance(built_question.get("referenceConfig"), dict)
12840
12915
  else {}
12841
12916
  )
12917
+ if relation_config_explicit:
12918
+ for stale_key in ("customButtonText", "customAdvancedSetting", "configShowForm", "dataShowForm"):
12919
+ reference.pop(stale_key, None)
12842
12920
  for key in (
12843
12921
  "referQueId",
12844
12922
  "referQuestions",
@@ -13007,6 +13085,7 @@ def _build_form_payload_for_edit_fields(
13007
13085
  question_relations: list[dict[str, Any]] | None = None,
13008
13086
  ) -> dict[str, Any]:
13009
13087
  _, section_templates = _extract_question_templates(current_schema)
13088
+ template_row_lengths_by_que_id, template_row_lengths_by_title = _extract_template_row_lengths(current_schema)
13010
13089
  fields_by_name = {
13011
13090
  str(field.get("name") or ""): field
13012
13091
  for field in fields
@@ -13017,18 +13096,26 @@ def _build_form_payload_for_edit_fields(
13017
13096
 
13018
13097
  for row in layout.get("root_rows", []) or []:
13019
13098
  questions: list[dict[str, Any]] = []
13099
+ expected_template_lengths: list[int] = []
13020
13100
  row_preserved = True
13021
13101
  for name in row:
13022
13102
  field = fields_by_name.get(str(name))
13023
13103
  if field is None:
13024
13104
  continue
13105
+ template_row_length = _field_template_row_length(
13106
+ field,
13107
+ lengths_by_que_id=template_row_lengths_by_que_id,
13108
+ lengths_by_title=template_row_lengths_by_title,
13109
+ )
13110
+ if template_row_length is not None:
13111
+ expected_template_lengths.append(template_row_length)
13025
13112
  question, preserved = _materialize_edit_question(field, temp_id=temp_id)
13026
13113
  questions.append(question)
13027
13114
  row_preserved = row_preserved and preserved
13028
13115
  temp_id -= 100
13029
13116
  if not questions:
13030
13117
  continue
13031
- if not row_preserved:
13118
+ if not row_preserved or _row_needs_width_reflow(expected_template_lengths, len(questions)):
13032
13119
  _apply_row_widths(questions)
13033
13120
  form_rows.append(questions)
13034
13121
 
@@ -13036,18 +13123,26 @@ def _build_form_payload_for_edit_fields(
13036
13123
  inner_rows: list[list[dict[str, Any]]] = []
13037
13124
  for row in section.get("rows", []) or []:
13038
13125
  questions: list[dict[str, Any]] = []
13126
+ expected_template_lengths: list[int] = []
13039
13127
  row_preserved = True
13040
13128
  for name in row:
13041
13129
  field = fields_by_name.get(str(name))
13042
13130
  if field is None:
13043
13131
  continue
13132
+ template_row_length = _field_template_row_length(
13133
+ field,
13134
+ lengths_by_que_id=template_row_lengths_by_que_id,
13135
+ lengths_by_title=template_row_lengths_by_title,
13136
+ )
13137
+ if template_row_length is not None:
13138
+ expected_template_lengths.append(template_row_length)
13044
13139
  question, preserved = _materialize_edit_question(field, temp_id=temp_id)
13045
13140
  questions.append(question)
13046
13141
  row_preserved = row_preserved and preserved
13047
13142
  temp_id -= 100
13048
13143
  if not questions:
13049
13144
  continue
13050
- if not row_preserved:
13145
+ if not row_preserved or _row_needs_width_reflow(expected_template_lengths, len(questions)):
13051
13146
  _apply_row_widths(questions)
13052
13147
  inner_rows.append(questions)
13053
13148
  if not inner_rows: