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

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.92
6
+ npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.94
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.92 qingflow-app-user-mcp
12
+ npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.94 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.92",
3
+ "version": "0.2.0-beta.94",
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.0b92"
7
+ version = "0.2.0b94"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -5,7 +5,7 @@ from pathlib import Path
5
5
 
6
6
  __all__ = ["__version__"]
7
7
 
8
- _FALLBACK_VERSION = "0.2.0b92"
8
+ _FALLBACK_VERSION = "0.2.0b94"
9
9
 
10
10
 
11
11
  def _resolve_local_pyproject_version() -> str | None:
@@ -12813,6 +12813,344 @@ def _row_needs_width_reflow(expected_template_lengths: list[int], current_row_le
12813
12813
  return any(length != current_row_length for length in expected_template_lengths)
12814
12814
 
12815
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
+ _RELATION_QUESTION_SAVE_KEYS = (
12843
+ "queId",
12844
+ "queTempId",
12845
+ "queType",
12846
+ "queOriginType",
12847
+ "queTitle",
12848
+ "queWidth",
12849
+ "scanType",
12850
+ "status",
12851
+ "required",
12852
+ "queHint",
12853
+ "linkedQuestions",
12854
+ "logicalShow",
12855
+ "queDefaultType",
12856
+ "queDefaultValue",
12857
+ "queDefaultValues",
12858
+ "subQueWidth",
12859
+ "innerQuestions",
12860
+ "minOpts",
12861
+ "maxOpts",
12862
+ "beingHide",
12863
+ "beingDesensitized",
12864
+ "relationDisplayMode",
12865
+ "customRenderConfig",
12866
+ )
12867
+
12868
+ _REFERENCE_CONFIG_SAVE_KEYS = (
12869
+ "referAppKey",
12870
+ "referQueId",
12871
+ "customButtonText",
12872
+ "beingTableSource",
12873
+ "referMatchRules",
12874
+ "canAddData",
12875
+ "dataAdditionButtonText",
12876
+ "canViewProcessLog",
12877
+ "optionalDataNum",
12878
+ "beingDataLogVisible",
12879
+ "beingDefaultFormulaAutoFillEnabled",
12880
+ "defaultValueMatchRules",
12881
+ "configShowForm",
12882
+ "configSortFieldId",
12883
+ "configAsc",
12884
+ "dataShowForm",
12885
+ "defaultRow",
12886
+ "fieldNameShow",
12887
+ "dataSortFieldId",
12888
+ "dataSortAsc",
12889
+ )
12890
+
12891
+ _REFERENCE_QUESTION_SAVE_KEYS = (
12892
+ "queId",
12893
+ "queTitle",
12894
+ "queType",
12895
+ "queAuth",
12896
+ "ordinal",
12897
+ )
12898
+
12899
+ _REFERENCE_FILL_RULE_SAVE_KEYS = (
12900
+ "queId",
12901
+ "relatedQueId",
12902
+ "queTitle",
12903
+ "relatedQueTitle",
12904
+ )
12905
+
12906
+ _REFERENCE_AUTH_QUESTION_SAVE_KEYS = (
12907
+ "queId",
12908
+ "queAuth",
12909
+ )
12910
+
12911
+
12912
+ def _copy_present_keys(
12913
+ source: dict[str, Any],
12914
+ keys: tuple[str, ...],
12915
+ *,
12916
+ keep_none_keys: tuple[str, ...] = (),
12917
+ ) -> dict[str, Any]:
12918
+ payload: dict[str, Any] = {}
12919
+ keep_none = set(keep_none_keys)
12920
+ for key in keys:
12921
+ if key not in source:
12922
+ continue
12923
+ value = source.get(key)
12924
+ if value is None and key not in keep_none:
12925
+ continue
12926
+ payload[key] = deepcopy(value)
12927
+ return payload
12928
+
12929
+
12930
+ def _normalize_reference_question_for_save(value: Any, *, ordinal: int) -> dict[str, Any] | None:
12931
+ if not isinstance(value, dict):
12932
+ return None
12933
+ payload = _copy_present_keys(value, _REFERENCE_QUESTION_SAVE_KEYS)
12934
+ que_id = _coerce_any_int(value.get("queId"))
12935
+ if que_id is not None:
12936
+ payload["queId"] = que_id
12937
+ if "ordinal" not in payload:
12938
+ payload["ordinal"] = _coerce_nonnegative_int(value.get("ordinal"))
12939
+ if payload.get("ordinal") is None:
12940
+ payload["ordinal"] = ordinal
12941
+ if not any(key in payload for key in ("queId", "queTitle", "queType")):
12942
+ return None
12943
+ return payload
12944
+
12945
+
12946
+ def _normalize_reference_fill_rule_for_save(value: Any) -> dict[str, Any] | None:
12947
+ if not isinstance(value, dict):
12948
+ return None
12949
+ payload = _copy_present_keys(value, _REFERENCE_FILL_RULE_SAVE_KEYS)
12950
+ que_id = _coerce_nonnegative_int(value.get("queId"))
12951
+ related_que_id = _coerce_nonnegative_int(value.get("relatedQueId", value.get("referQueId")))
12952
+ if que_id is not None:
12953
+ payload["queId"] = que_id
12954
+ if related_que_id is not None:
12955
+ payload["relatedQueId"] = related_que_id
12956
+ if "relatedQueTitle" not in payload and value.get("referQueTitle") is not None:
12957
+ payload["relatedQueTitle"] = str(value.get("referQueTitle") or "")
12958
+ if "queId" not in payload or "relatedQueId" not in payload:
12959
+ return None
12960
+ return payload
12961
+
12962
+
12963
+ def _normalize_reference_auth_question_for_save(value: Any) -> dict[str, Any] | None:
12964
+ if not isinstance(value, dict):
12965
+ return None
12966
+ payload = _copy_present_keys(value, _REFERENCE_AUTH_QUESTION_SAVE_KEYS)
12967
+ que_id = _coerce_any_int(value.get("queId"))
12968
+ if que_id is not None:
12969
+ payload["queId"] = que_id
12970
+ que_auth = _coerce_nonnegative_int(value.get("queAuth"))
12971
+ if que_auth is not None:
12972
+ payload["queAuth"] = que_auth
12973
+ sub_ques = [
12974
+ item
12975
+ for item in (
12976
+ _normalize_reference_auth_question_for_save(raw_item)
12977
+ for raw_item in cast(list[Any], value.get("subQues") or [])
12978
+ )
12979
+ if item is not None
12980
+ ]
12981
+ inner_ques = [
12982
+ item
12983
+ for item in (
12984
+ _normalize_reference_auth_question_for_save(raw_item)
12985
+ for raw_item in cast(list[Any], value.get("innerQues") or [])
12986
+ )
12987
+ if item is not None
12988
+ ]
12989
+ if sub_ques or "subQues" in value:
12990
+ payload["subQues"] = sub_ques
12991
+ if inner_ques or "innerQues" in value:
12992
+ payload["innerQues"] = inner_ques
12993
+ if "queId" not in payload or "queAuth" not in payload:
12994
+ return None
12995
+ return payload
12996
+
12997
+
12998
+ def _normalize_reference_config_for_save(
12999
+ reference: Any,
13000
+ *,
13001
+ field: dict[str, Any],
13002
+ ) -> dict[str, Any]:
13003
+ source = reference if isinstance(reference, dict) else {}
13004
+ payload = _copy_present_keys(source, _REFERENCE_CONFIG_SAVE_KEYS)
13005
+ if str(field.get("target_app_key") or "").strip():
13006
+ payload["referAppKey"] = str(field.get("target_app_key") or "").strip()
13007
+ if field.get("target_field_que_id") is not None:
13008
+ payload["referQueId"] = _coerce_nonnegative_int(field.get("target_field_que_id"))
13009
+ if field.get("field_name_show") is not None:
13010
+ payload["fieldNameShow"] = bool(field.get("field_name_show"))
13011
+
13012
+ refer_questions: list[dict[str, Any]] = []
13013
+ for index, raw_item in enumerate(cast(list[Any], source.get("referQuestions") or []), start=1):
13014
+ normalized_item = _normalize_reference_question_for_save(raw_item, ordinal=index)
13015
+ if normalized_item is not None:
13016
+ refer_questions.append(normalized_item)
13017
+ if refer_questions or "referQuestions" in source:
13018
+ payload["referQuestions"] = refer_questions
13019
+
13020
+ refer_fill_rules = [
13021
+ item
13022
+ for item in (
13023
+ _normalize_reference_fill_rule_for_save(raw_item)
13024
+ for raw_item in cast(list[Any], source.get("referFillRules") or [])
13025
+ )
13026
+ if item is not None
13027
+ ]
13028
+ if refer_fill_rules or "referFillRules" in source:
13029
+ payload["referFillRules"] = refer_fill_rules
13030
+
13031
+ refer_auth_ques = [
13032
+ item
13033
+ for item in (
13034
+ _normalize_reference_auth_question_for_save(raw_item)
13035
+ for raw_item in cast(list[Any], source.get("referAuthQues") or [])
13036
+ )
13037
+ if item is not None
13038
+ ]
13039
+ if refer_auth_ques or "referAuthQues" in source:
13040
+ payload["referAuthQues"] = refer_auth_ques
13041
+
13042
+ return payload
13043
+
13044
+
13045
+ def _normalize_relation_question_for_save(question: dict[str, Any], *, field: dict[str, Any]) -> dict[str, Any]:
13046
+ payload = _copy_present_keys(question, _RELATION_QUESTION_SAVE_KEYS)
13047
+ overlay_keys = _field_question_overlay_keys(field)
13048
+ que_id = _coerce_nonnegative_int(question.get("queId"))
13049
+ if que_id is not None:
13050
+ payload["queId"] = que_id
13051
+ que_temp_id = _coerce_nonnegative_int(question.get("queTempId"))
13052
+ if que_temp_id is not None and "queId" not in payload:
13053
+ payload["queTempId"] = que_temp_id
13054
+ payload["queType"] = _coerce_positive_int(question.get("queType")) or 25
13055
+ payload["queTitle"] = str(field.get("name") or question.get("queTitle") or "")
13056
+ if "required" in overlay_keys or "required" in question or field.get("required") is not None:
13057
+ payload["required"] = bool(field.get("required", question.get("required", False)))
13058
+ if "description" in overlay_keys:
13059
+ payload["queHint"] = "" if field.get("description") is None else str(field.get("description"))
13060
+ elif "queHint" in question and question.get("queHint") is not None:
13061
+ payload["queHint"] = str(question.get("queHint") or "")
13062
+ if field.get("default_type") is not None:
13063
+ payload["queDefaultType"] = _coerce_positive_int(field.get("default_type")) or 1
13064
+ if "default_value" in field:
13065
+ payload["queDefaultValue"] = field.get("default_value")
13066
+ payload["referenceConfig"] = _normalize_reference_config_for_save(question.get("referenceConfig"), field=field)
13067
+ return payload
13068
+
13069
+
13070
+ def _build_form_save_base_payload(current_schema: dict[str, Any], title: str) -> dict[str, Any]:
13071
+ payload: dict[str, Any] = {"formTitle": title}
13072
+ for key in _FORM_SAVE_BASE_KEYS:
13073
+ if key in current_schema:
13074
+ payload[key] = deepcopy(current_schema.get(key))
13075
+ payload["editVersionNo"] = int(current_schema.get("editVersionNo") or 1)
13076
+ payload["formQues"] = []
13077
+ payload["questionRelations"] = []
13078
+ return payload
13079
+
13080
+
13081
+ def _normalize_question_relations_for_save(question_relations: list[dict[str, Any]] | None) -> list[dict[str, Any]]:
13082
+ normalized: list[dict[str, Any]] = []
13083
+ for relation in question_relations or []:
13084
+ if not isinstance(relation, dict):
13085
+ continue
13086
+ item: dict[str, Any] = {}
13087
+ for key in _QUESTION_RELATION_SAVE_KEYS:
13088
+ if key not in relation:
13089
+ continue
13090
+ value = relation.get(key)
13091
+ if value is None:
13092
+ continue
13093
+ item[key] = deepcopy(value)
13094
+ if item:
13095
+ normalized.append(item)
13096
+ return normalized
13097
+
13098
+
13099
+ def _field_rename_maps(fields: list[dict[str, Any]]) -> tuple[dict[int, str], dict[str, str]]:
13100
+ by_que_id: dict[int, str] = {}
13101
+ by_title: dict[str, str] = {}
13102
+ for field in fields:
13103
+ if not isinstance(field, dict):
13104
+ continue
13105
+ template = field.get("_question_template")
13106
+ if not isinstance(template, dict):
13107
+ continue
13108
+ old_title = str(template.get("queTitle") or "").strip()
13109
+ new_title = str(field.get("name") or "").strip()
13110
+ if not old_title or not new_title or old_title == new_title:
13111
+ continue
13112
+ que_id = _coerce_nonnegative_int(field.get("que_id"))
13113
+ if que_id is None:
13114
+ que_id = _coerce_nonnegative_int(template.get("queId"))
13115
+ if que_id is not None:
13116
+ by_que_id[que_id] = new_title
13117
+ by_title[old_title] = new_title
13118
+ return by_que_id, by_title
13119
+
13120
+
13121
+ def _sync_question_title_references(value: Any, *, by_que_id: dict[int, str], by_title: dict[str, str]) -> None:
13122
+ if isinstance(value, list):
13123
+ for item in value:
13124
+ _sync_question_title_references(item, by_que_id=by_que_id, by_title=by_title)
13125
+ return
13126
+ if not isinstance(value, dict):
13127
+ return
13128
+
13129
+ title_keys = ("queTitle", "_field_id")
13130
+ que_id = _coerce_nonnegative_int(value.get("queId"))
13131
+ replacement = None
13132
+ if que_id is not None and que_id in by_que_id:
13133
+ replacement = by_que_id[que_id]
13134
+ elif que_id is None:
13135
+ for key in title_keys:
13136
+ current_title = str(value.get(key) or "").strip()
13137
+ if current_title and current_title in by_title:
13138
+ replacement = by_title[current_title]
13139
+ break
13140
+ if replacement is not None:
13141
+ for key in title_keys:
13142
+ current_title = str(value.get(key) or "").strip()
13143
+ 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):
13144
+ value[key] = replacement
13145
+ sup_id = _coerce_nonnegative_int(value.get("supId"))
13146
+ if sup_id is not None and sup_id in by_que_id and "supQueTitle" in value:
13147
+ value["supQueTitle"] = by_que_id[sup_id]
13148
+
13149
+ for child_value in value.values():
13150
+ if isinstance(child_value, (dict, list)):
13151
+ _sync_question_title_references(child_value, by_que_id=by_que_id, by_title=by_title)
13152
+
13153
+
12816
13154
  def _materialize_preserved_question(field: dict[str, Any]) -> dict[str, Any] | None:
12817
13155
  template = deepcopy(field.get("_question_template"))
12818
13156
  if not isinstance(template, dict):
@@ -12825,6 +13163,8 @@ def _materialize_preserved_question(field: dict[str, Any]) -> dict[str, Any] | N
12825
13163
  if "description" in overlay_keys:
12826
13164
  description = field.get("description")
12827
13165
  template["queHint"] = "" if description is None else str(description)
13166
+ if str(field.get("type") or "") == FieldType.relation.value:
13167
+ return _normalize_relation_question_for_save(template, field=field)
12828
13168
  return template
12829
13169
 
12830
13170
 
@@ -12932,6 +13272,7 @@ def _field_to_question(field: dict[str, Any], *, temp_id: int) -> dict[str, Any]
12932
13272
  if field.get("target_field_que_id") is not None:
12933
13273
  reference["referQueId"] = field.get("target_field_que_id")
12934
13274
  question["referenceConfig"] = reference
13275
+ question = _normalize_relation_question_for_save(question, field=field)
12935
13276
  if field.get("type") == FieldType.department.value:
12936
13277
  scope_type, scope_payload = _serialize_department_scope_for_question(field.get("department_scope"))
12937
13278
  question["deptSelectScopeType"] = scope_type
@@ -13072,7 +13413,9 @@ def _build_form_payload_from_fields(
13072
13413
  _apply_row_widths(row)
13073
13414
  payload = default_form_payload(title, form_rows)
13074
13415
  payload["editVersionNo"] = int(current_schema.get("editVersionNo") or 1)
13075
- payload["questionRelations"] = deepcopy(question_relations if question_relations is not None else (current_schema.get("questionRelations") or []))
13416
+ payload["questionRelations"] = _normalize_question_relations_for_save(
13417
+ question_relations if question_relations is not None else (current_schema.get("questionRelations") or [])
13418
+ )
13076
13419
  return payload
13077
13420
 
13078
13421
 
@@ -13175,10 +13518,18 @@ def _build_form_payload_for_edit_fields(
13175
13518
  wrapper["innerQuestions"] = inner_rows
13176
13519
  form_rows.append([wrapper])
13177
13520
 
13178
- payload = deepcopy(current_schema)
13179
- payload["formTitle"] = title
13521
+ rename_by_que_id, rename_by_title = _field_rename_maps(fields)
13522
+ if rename_by_que_id or rename_by_title:
13523
+ _sync_question_title_references(form_rows, by_que_id=rename_by_que_id, by_title=rename_by_title)
13524
+ normalized_relations = _normalize_question_relations_for_save(
13525
+ question_relations if question_relations is not None else (current_schema.get("questionRelations") or [])
13526
+ )
13527
+ if rename_by_que_id or rename_by_title:
13528
+ _sync_question_title_references(normalized_relations, by_que_id=rename_by_que_id, by_title=rename_by_title)
13529
+
13530
+ payload = _build_form_save_base_payload(current_schema, title)
13180
13531
  payload["formQues"] = form_rows
13181
- payload["questionRelations"] = deepcopy(question_relations if question_relations is not None else (current_schema.get("questionRelations") or []))
13532
+ payload["questionRelations"] = normalized_relations
13182
13533
  payload["editVersionNo"] = int(current_schema.get("editVersionNo") or 1)
13183
13534
  return payload
13184
13535
 
@@ -14592,9 +14943,9 @@ def _build_form_payload_from_existing_schema(
14592
14943
  wrapper["queWidth"] = 100
14593
14944
  form_rows.append([wrapper])
14594
14945
 
14595
- payload = deepcopy(current_schema)
14946
+ payload = _build_form_save_base_payload(current_schema, str(current_schema.get("formTitle") or "未命名应用"))
14596
14947
  payload["formQues"] = form_rows
14597
- payload["questionRelations"] = deepcopy(current_schema.get("questionRelations") or [])
14948
+ payload["questionRelations"] = _normalize_question_relations_for_save(current_schema.get("questionRelations") or [])
14598
14949
  payload["editVersionNo"] = int(current_schema.get("editVersionNo") or 1)
14599
14950
  payload.setdefault("formTitle", current_schema.get("formTitle") or "未命名应用")
14600
14951
  return payload