@josephyan/qingflow-app-user-mcp 0.2.0-beta.91 → 0.2.0-beta.93

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.91
6
+ npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.93
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.91 qingflow-app-user-mcp
12
+ npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.93 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.91",
3
+ "version": "0.2.0-beta.93",
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.0b91"
7
+ version = "0.2.0b93"
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.0b93"
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,176 @@ 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
+
12816
+ _FORM_SAVE_BASE_KEYS = (
12817
+ "formDesc",
12818
+ "formTheme",
12819
+ "formAttach",
12820
+ "formStyle",
12821
+ "serialNumType",
12822
+ "serialNumConfig",
12823
+ "attachVisibleOnlyConfig",
12824
+ "externalLang",
12825
+ "hideCopyright",
12826
+ )
12827
+
12828
+ _QUESTION_RELATION_SAVE_KEYS = (
12829
+ "queId",
12830
+ "relationType",
12831
+ "displayedQueId",
12832
+ "qlinkerAlias",
12833
+ "displayedQueInfo",
12834
+ "aliasConfig",
12835
+ "matchRules",
12836
+ "tableMatchRules",
12837
+ "matchRuleType",
12838
+ "matchRuleFormula",
12839
+ "sortConfig",
12840
+ )
12841
+
12842
+
12843
+ def _build_form_save_base_payload(current_schema: dict[str, Any], title: str) -> dict[str, Any]:
12844
+ payload: dict[str, Any] = {"formTitle": title}
12845
+ for key in _FORM_SAVE_BASE_KEYS:
12846
+ if key in current_schema:
12847
+ payload[key] = deepcopy(current_schema.get(key))
12848
+ payload["editVersionNo"] = int(current_schema.get("editVersionNo") or 1)
12849
+ payload["formQues"] = []
12850
+ payload["questionRelations"] = []
12851
+ return payload
12852
+
12853
+
12854
+ def _normalize_question_relations_for_save(question_relations: list[dict[str, Any]] | None) -> list[dict[str, Any]]:
12855
+ normalized: list[dict[str, Any]] = []
12856
+ for relation in question_relations or []:
12857
+ if not isinstance(relation, dict):
12858
+ continue
12859
+ item: dict[str, Any] = {}
12860
+ for key in _QUESTION_RELATION_SAVE_KEYS:
12861
+ if key not in relation:
12862
+ continue
12863
+ value = relation.get(key)
12864
+ if value is None:
12865
+ continue
12866
+ item[key] = deepcopy(value)
12867
+ if item:
12868
+ normalized.append(item)
12869
+ return normalized
12870
+
12871
+
12872
+ def _field_rename_maps(fields: list[dict[str, Any]]) -> tuple[dict[int, str], dict[str, str]]:
12873
+ by_que_id: dict[int, str] = {}
12874
+ by_title: dict[str, str] = {}
12875
+ for field in fields:
12876
+ if not isinstance(field, dict):
12877
+ continue
12878
+ template = field.get("_question_template")
12879
+ if not isinstance(template, dict):
12880
+ continue
12881
+ old_title = str(template.get("queTitle") or "").strip()
12882
+ new_title = str(field.get("name") or "").strip()
12883
+ if not old_title or not new_title or old_title == new_title:
12884
+ continue
12885
+ que_id = _coerce_nonnegative_int(field.get("que_id"))
12886
+ if que_id is None:
12887
+ que_id = _coerce_nonnegative_int(template.get("queId"))
12888
+ if que_id is not None:
12889
+ by_que_id[que_id] = new_title
12890
+ by_title[old_title] = new_title
12891
+ return by_que_id, by_title
12892
+
12893
+
12894
+ def _sync_question_title_references(value: Any, *, by_que_id: dict[int, str], by_title: dict[str, str]) -> None:
12895
+ if isinstance(value, list):
12896
+ for item in value:
12897
+ _sync_question_title_references(item, by_que_id=by_que_id, by_title=by_title)
12898
+ return
12899
+ if not isinstance(value, dict):
12900
+ return
12901
+
12902
+ title_keys = ("queTitle", "supQueTitle", "_field_id")
12903
+ que_id = _coerce_nonnegative_int(value.get("queId"))
12904
+ replacement = None
12905
+ if que_id is not None and que_id in by_que_id:
12906
+ replacement = by_que_id[que_id]
12907
+ elif que_id is None:
12908
+ for key in title_keys:
12909
+ current_title = str(value.get(key) or "").strip()
12910
+ if current_title and current_title in by_title:
12911
+ replacement = by_title[current_title]
12912
+ break
12913
+ if replacement is not None:
12914
+ for key in title_keys:
12915
+ current_title = str(value.get(key) or "").strip()
12916
+ if (que_id is not None and que_id in by_que_id and key in value) or (current_title and current_title in by_title):
12917
+ value[key] = replacement
12918
+ sup_id = _coerce_nonnegative_int(value.get("supId"))
12919
+ if sup_id is not None and sup_id in by_que_id and "supQueTitle" in value:
12920
+ value["supQueTitle"] = by_que_id[sup_id]
12921
+
12922
+ for child_value in value.values():
12923
+ if isinstance(child_value, (dict, list)):
12924
+ _sync_question_title_references(child_value, by_que_id=by_que_id, by_title=by_title)
12925
+
12926
+
12756
12927
  def _materialize_preserved_question(field: dict[str, Any]) -> dict[str, Any] | None:
12757
12928
  template = deepcopy(field.get("_question_template"))
12758
12929
  if not isinstance(template, dict):
@@ -12802,12 +12973,21 @@ def _field_to_question(field: dict[str, Any], *, temp_id: int) -> dict[str, Any]
12802
12973
  },
12803
12974
  temp_id,
12804
12975
  )
12976
+ relation_config_explicit = bool(field.get("_relation_config_explicit"))
12805
12977
  relation_question_template = (
12806
12978
  deepcopy(field.get("_question_template"))
12807
12979
  if field.get("type") == FieldType.relation.value and isinstance(field.get("_question_template"), dict)
12808
12980
  else None
12809
12981
  )
12810
- question = relation_question_template if relation_question_template is not None else built_question
12982
+ question = (
12983
+ relation_question_template
12984
+ if relation_question_template is not None and not relation_config_explicit
12985
+ else built_question
12986
+ )
12987
+ if relation_config_explicit and relation_question_template is not None:
12988
+ for key in ("queOriginType", "relationDisplayMode", "customRenderConfig"):
12989
+ if key in relation_question_template:
12990
+ question[key] = deepcopy(relation_question_template[key])
12811
12991
  if _coerce_nonnegative_int(field.get("que_id")) is not None:
12812
12992
  question["queId"] = field["que_id"]
12813
12993
  question.pop("queTempId", None)
@@ -12826,19 +13006,28 @@ def _field_to_question(field: dict[str, Any], *, temp_id: int) -> dict[str, Any]
12826
13006
  if field.get("type") == FieldType.relation.value:
12827
13007
  preserved_reference = (
12828
13008
  deepcopy(field.get("_reference_config_template"))
12829
- if not bool(field.get("_relation_config_explicit")) and isinstance(field.get("_reference_config_template"), dict)
13009
+ if not relation_config_explicit and isinstance(field.get("_reference_config_template"), dict)
12830
13010
  else None
12831
13011
  )
12832
13012
  if preserved_reference is not None:
12833
13013
  preserved_reference["referAppKey"] = field.get("target_app_key")
12834
13014
  question["referenceConfig"] = preserved_reference
12835
13015
  else:
12836
- reference = deepcopy(question.get("referenceConfig")) if isinstance(question.get("referenceConfig"), dict) else {}
13016
+ reference = (
13017
+ deepcopy(built_question.get("referenceConfig"))
13018
+ if relation_config_explicit and isinstance(built_question.get("referenceConfig"), dict)
13019
+ else deepcopy(question.get("referenceConfig"))
13020
+ if isinstance(question.get("referenceConfig"), dict)
13021
+ else {}
13022
+ )
12837
13023
  built_reference = (
12838
13024
  deepcopy(built_question.get("referenceConfig"))
12839
13025
  if isinstance(built_question.get("referenceConfig"), dict)
12840
13026
  else {}
12841
13027
  )
13028
+ if relation_config_explicit:
13029
+ for stale_key in ("customButtonText", "customAdvancedSetting", "configShowForm", "dataShowForm"):
13030
+ reference.pop(stale_key, None)
12842
13031
  for key in (
12843
13032
  "referQueId",
12844
13033
  "referQuestions",
@@ -12994,7 +13183,9 @@ def _build_form_payload_from_fields(
12994
13183
  _apply_row_widths(row)
12995
13184
  payload = default_form_payload(title, form_rows)
12996
13185
  payload["editVersionNo"] = int(current_schema.get("editVersionNo") or 1)
12997
- payload["questionRelations"] = deepcopy(question_relations if question_relations is not None else (current_schema.get("questionRelations") or []))
13186
+ payload["questionRelations"] = _normalize_question_relations_for_save(
13187
+ question_relations if question_relations is not None else (current_schema.get("questionRelations") or [])
13188
+ )
12998
13189
  return payload
12999
13190
 
13000
13191
 
@@ -13007,6 +13198,7 @@ def _build_form_payload_for_edit_fields(
13007
13198
  question_relations: list[dict[str, Any]] | None = None,
13008
13199
  ) -> dict[str, Any]:
13009
13200
  _, section_templates = _extract_question_templates(current_schema)
13201
+ template_row_lengths_by_que_id, template_row_lengths_by_title = _extract_template_row_lengths(current_schema)
13010
13202
  fields_by_name = {
13011
13203
  str(field.get("name") or ""): field
13012
13204
  for field in fields
@@ -13017,18 +13209,26 @@ def _build_form_payload_for_edit_fields(
13017
13209
 
13018
13210
  for row in layout.get("root_rows", []) or []:
13019
13211
  questions: list[dict[str, Any]] = []
13212
+ expected_template_lengths: list[int] = []
13020
13213
  row_preserved = True
13021
13214
  for name in row:
13022
13215
  field = fields_by_name.get(str(name))
13023
13216
  if field is None:
13024
13217
  continue
13218
+ template_row_length = _field_template_row_length(
13219
+ field,
13220
+ lengths_by_que_id=template_row_lengths_by_que_id,
13221
+ lengths_by_title=template_row_lengths_by_title,
13222
+ )
13223
+ if template_row_length is not None:
13224
+ expected_template_lengths.append(template_row_length)
13025
13225
  question, preserved = _materialize_edit_question(field, temp_id=temp_id)
13026
13226
  questions.append(question)
13027
13227
  row_preserved = row_preserved and preserved
13028
13228
  temp_id -= 100
13029
13229
  if not questions:
13030
13230
  continue
13031
- if not row_preserved:
13231
+ if not row_preserved or _row_needs_width_reflow(expected_template_lengths, len(questions)):
13032
13232
  _apply_row_widths(questions)
13033
13233
  form_rows.append(questions)
13034
13234
 
@@ -13036,18 +13236,26 @@ def _build_form_payload_for_edit_fields(
13036
13236
  inner_rows: list[list[dict[str, Any]]] = []
13037
13237
  for row in section.get("rows", []) or []:
13038
13238
  questions: list[dict[str, Any]] = []
13239
+ expected_template_lengths: list[int] = []
13039
13240
  row_preserved = True
13040
13241
  for name in row:
13041
13242
  field = fields_by_name.get(str(name))
13042
13243
  if field is None:
13043
13244
  continue
13245
+ template_row_length = _field_template_row_length(
13246
+ field,
13247
+ lengths_by_que_id=template_row_lengths_by_que_id,
13248
+ lengths_by_title=template_row_lengths_by_title,
13249
+ )
13250
+ if template_row_length is not None:
13251
+ expected_template_lengths.append(template_row_length)
13044
13252
  question, preserved = _materialize_edit_question(field, temp_id=temp_id)
13045
13253
  questions.append(question)
13046
13254
  row_preserved = row_preserved and preserved
13047
13255
  temp_id -= 100
13048
13256
  if not questions:
13049
13257
  continue
13050
- if not row_preserved:
13258
+ if not row_preserved or _row_needs_width_reflow(expected_template_lengths, len(questions)):
13051
13259
  _apply_row_widths(questions)
13052
13260
  inner_rows.append(questions)
13053
13261
  if not inner_rows:
@@ -13080,10 +13288,18 @@ def _build_form_payload_for_edit_fields(
13080
13288
  wrapper["innerQuestions"] = inner_rows
13081
13289
  form_rows.append([wrapper])
13082
13290
 
13083
- payload = deepcopy(current_schema)
13084
- payload["formTitle"] = title
13291
+ rename_by_que_id, rename_by_title = _field_rename_maps(fields)
13292
+ if rename_by_que_id or rename_by_title:
13293
+ _sync_question_title_references(form_rows, by_que_id=rename_by_que_id, by_title=rename_by_title)
13294
+ normalized_relations = _normalize_question_relations_for_save(
13295
+ question_relations if question_relations is not None else (current_schema.get("questionRelations") or [])
13296
+ )
13297
+ if rename_by_que_id or rename_by_title:
13298
+ _sync_question_title_references(normalized_relations, by_que_id=rename_by_que_id, by_title=rename_by_title)
13299
+
13300
+ payload = _build_form_save_base_payload(current_schema, title)
13085
13301
  payload["formQues"] = form_rows
13086
- payload["questionRelations"] = deepcopy(question_relations if question_relations is not None else (current_schema.get("questionRelations") or []))
13302
+ payload["questionRelations"] = normalized_relations
13087
13303
  payload["editVersionNo"] = int(current_schema.get("editVersionNo") or 1)
13088
13304
  return payload
13089
13305
 
@@ -14497,9 +14713,9 @@ def _build_form_payload_from_existing_schema(
14497
14713
  wrapper["queWidth"] = 100
14498
14714
  form_rows.append([wrapper])
14499
14715
 
14500
- payload = deepcopy(current_schema)
14716
+ payload = _build_form_save_base_payload(current_schema, str(current_schema.get("formTitle") or "未命名应用"))
14501
14717
  payload["formQues"] = form_rows
14502
- payload["questionRelations"] = deepcopy(current_schema.get("questionRelations") or [])
14718
+ payload["questionRelations"] = _normalize_question_relations_for_save(current_schema.get("questionRelations") or [])
14503
14719
  payload["editVersionNo"] = int(current_schema.get("editVersionNo") or 1)
14504
14720
  payload.setdefault("formTitle", current_schema.get("formTitle") or "未命名应用")
14505
14721
  return payload