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

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.93
6
+ npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.95
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.93 qingflow-app-user-mcp
12
+ npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.95 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.93",
3
+ "version": "0.2.0-beta.95",
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.0b93"
7
+ version = "0.2.0b95"
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.0b93"
8
+ _FALLBACK_VERSION = "0.2.0b95"
9
9
 
10
10
 
11
11
  def _resolve_local_pyproject_version() -> str | None:
@@ -1,12 +1,16 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import base64
3
4
  from copy import deepcopy
4
5
  from dataclasses import dataclass, field
5
6
  import json
6
7
  import os
8
+ import random
7
9
  import re
10
+ import string
8
11
  import tempfile
9
12
  from typing import Any, cast
13
+ from urllib.parse import quote_plus, unquote_plus
10
14
  from uuid import uuid4
11
15
 
12
16
  from ..backend_client import BackendRequestContext
@@ -143,6 +147,7 @@ JUDGE_EQUAL_ANY = 9
143
147
  JUDGE_FUZZY_MATCH = 19
144
148
  JUDGE_INCLUDE_ANY = 20
145
149
  DEFAULT_TYPE_RELATION = 2
150
+ DEFAULT_TYPE_FORMULA = 3
146
151
  RELATION_TYPE_Q_LINKER = 2
147
152
  RELATION_TYPE_CODE_BLOCK = 3
148
153
 
@@ -12839,6 +12844,275 @@ _QUESTION_RELATION_SAVE_KEYS = (
12839
12844
  "sortConfig",
12840
12845
  )
12841
12846
 
12847
+ _RELATION_QUESTION_SAVE_KEYS = (
12848
+ "queId",
12849
+ "queTempId",
12850
+ "queType",
12851
+ "queOriginType",
12852
+ "queTitle",
12853
+ "queWidth",
12854
+ "scanType",
12855
+ "status",
12856
+ "required",
12857
+ "queHint",
12858
+ "linkedQuestions",
12859
+ "logicalShow",
12860
+ "queDefaultType",
12861
+ "queDefaultValue",
12862
+ "queDefaultValues",
12863
+ "subQueWidth",
12864
+ "innerQuestions",
12865
+ "minOpts",
12866
+ "maxOpts",
12867
+ "beingHide",
12868
+ "beingDesensitized",
12869
+ "relationDisplayMode",
12870
+ "customRenderConfig",
12871
+ )
12872
+
12873
+ _REFERENCE_CONFIG_SAVE_KEYS = (
12874
+ "referAppKey",
12875
+ "referQueId",
12876
+ "customButtonText",
12877
+ "beingTableSource",
12878
+ "referMatchRules",
12879
+ "canAddData",
12880
+ "dataAdditionButtonText",
12881
+ "canViewProcessLog",
12882
+ "optionalDataNum",
12883
+ "beingDataLogVisible",
12884
+ "beingDefaultFormulaAutoFillEnabled",
12885
+ "defaultValueMatchRules",
12886
+ "configShowForm",
12887
+ "configSortFieldId",
12888
+ "configAsc",
12889
+ "dataShowForm",
12890
+ "defaultRow",
12891
+ "fieldNameShow",
12892
+ "dataSortFieldId",
12893
+ "dataSortAsc",
12894
+ )
12895
+
12896
+ _REFERENCE_QUESTION_SAVE_KEYS = (
12897
+ "queId",
12898
+ "queTitle",
12899
+ "queType",
12900
+ "queAuth",
12901
+ "ordinal",
12902
+ )
12903
+
12904
+ _REFERENCE_FILL_RULE_SAVE_KEYS = (
12905
+ "queId",
12906
+ "relatedQueId",
12907
+ "queTitle",
12908
+ "relatedQueTitle",
12909
+ )
12910
+
12911
+ _REFERENCE_AUTH_QUESTION_SAVE_KEYS = (
12912
+ "queId",
12913
+ "queAuth",
12914
+ )
12915
+
12916
+
12917
+ def _copy_present_keys(
12918
+ source: dict[str, Any],
12919
+ keys: tuple[str, ...],
12920
+ *,
12921
+ keep_none_keys: tuple[str, ...] = (),
12922
+ ) -> dict[str, Any]:
12923
+ payload: dict[str, Any] = {}
12924
+ keep_none = set(keep_none_keys)
12925
+ for key in keys:
12926
+ if key not in source:
12927
+ continue
12928
+ value = source.get(key)
12929
+ if value is None and key not in keep_none:
12930
+ continue
12931
+ payload[key] = deepcopy(value)
12932
+ return payload
12933
+
12934
+
12935
+ def _looks_like_backend_encoded_formula(value: str) -> bool:
12936
+ if len(value) <= 32:
12937
+ return False
12938
+ encoded = value[16:-16]
12939
+ if not encoded:
12940
+ return False
12941
+ try:
12942
+ decoded = base64.b64decode(encoded, validate=True).decode("utf-8")
12943
+ unquote_plus(decoded)
12944
+ except Exception:
12945
+ return False
12946
+ return True
12947
+
12948
+
12949
+ def _encode_formula_for_backend_save(value: Any) -> Any:
12950
+ if not isinstance(value, str) or not value:
12951
+ return value
12952
+ if _looks_like_backend_encoded_formula(value):
12953
+ return value
12954
+ encoded = quote_plus(value, encoding="utf-8")
12955
+ b64_value = base64.b64encode(encoded.encode("utf-8")).decode("ascii")
12956
+ alphabet = string.ascii_letters + string.digits
12957
+ prefix = "".join(random.choice(alphabet) for _ in range(16))
12958
+ suffix = "".join(random.choice(alphabet) for _ in range(16))
12959
+ return f"{prefix}{b64_value}{suffix}"
12960
+
12961
+
12962
+ def _normalize_formula_defaults_for_save(value: Any) -> None:
12963
+ if isinstance(value, list):
12964
+ for item in value:
12965
+ _normalize_formula_defaults_for_save(item)
12966
+ return
12967
+ if not isinstance(value, dict):
12968
+ return
12969
+ if _coerce_any_int(value.get("queDefaultType")) == DEFAULT_TYPE_FORMULA and value.get("queDefaultValue"):
12970
+ value["queDefaultValue"] = _encode_formula_for_backend_save(value.get("queDefaultValue"))
12971
+ for key in ("subQuestions", "innerQuestions"):
12972
+ nested = value.get(key)
12973
+ if isinstance(nested, (list, dict)):
12974
+ _normalize_formula_defaults_for_save(nested)
12975
+
12976
+
12977
+ def _normalize_reference_question_for_save(value: Any, *, ordinal: int) -> dict[str, Any] | None:
12978
+ if not isinstance(value, dict):
12979
+ return None
12980
+ payload = _copy_present_keys(value, _REFERENCE_QUESTION_SAVE_KEYS)
12981
+ que_id = _coerce_any_int(value.get("queId"))
12982
+ if que_id is not None:
12983
+ payload["queId"] = que_id
12984
+ if "ordinal" not in payload:
12985
+ payload["ordinal"] = _coerce_nonnegative_int(value.get("ordinal"))
12986
+ if payload.get("ordinal") is None:
12987
+ payload["ordinal"] = ordinal
12988
+ if not any(key in payload for key in ("queId", "queTitle", "queType")):
12989
+ return None
12990
+ return payload
12991
+
12992
+
12993
+ def _normalize_reference_fill_rule_for_save(value: Any) -> dict[str, Any] | None:
12994
+ if not isinstance(value, dict):
12995
+ return None
12996
+ payload = _copy_present_keys(value, _REFERENCE_FILL_RULE_SAVE_KEYS)
12997
+ que_id = _coerce_nonnegative_int(value.get("queId"))
12998
+ related_que_id = _coerce_nonnegative_int(value.get("relatedQueId", value.get("referQueId")))
12999
+ if que_id is not None:
13000
+ payload["queId"] = que_id
13001
+ if related_que_id is not None:
13002
+ payload["relatedQueId"] = related_que_id
13003
+ if "relatedQueTitle" not in payload and value.get("referQueTitle") is not None:
13004
+ payload["relatedQueTitle"] = str(value.get("referQueTitle") or "")
13005
+ if "queId" not in payload or "relatedQueId" not in payload:
13006
+ return None
13007
+ return payload
13008
+
13009
+
13010
+ def _normalize_reference_auth_question_for_save(value: Any) -> dict[str, Any] | None:
13011
+ if not isinstance(value, dict):
13012
+ return None
13013
+ payload = _copy_present_keys(value, _REFERENCE_AUTH_QUESTION_SAVE_KEYS)
13014
+ que_id = _coerce_any_int(value.get("queId"))
13015
+ if que_id is not None:
13016
+ payload["queId"] = que_id
13017
+ que_auth = _coerce_nonnegative_int(value.get("queAuth"))
13018
+ if que_auth is not None:
13019
+ payload["queAuth"] = que_auth
13020
+ sub_ques = [
13021
+ item
13022
+ for item in (
13023
+ _normalize_reference_auth_question_for_save(raw_item)
13024
+ for raw_item in cast(list[Any], value.get("subQues") or [])
13025
+ )
13026
+ if item is not None
13027
+ ]
13028
+ inner_ques = [
13029
+ item
13030
+ for item in (
13031
+ _normalize_reference_auth_question_for_save(raw_item)
13032
+ for raw_item in cast(list[Any], value.get("innerQues") or [])
13033
+ )
13034
+ if item is not None
13035
+ ]
13036
+ if sub_ques or "subQues" in value:
13037
+ payload["subQues"] = sub_ques
13038
+ if inner_ques or "innerQues" in value:
13039
+ payload["innerQues"] = inner_ques
13040
+ if "queId" not in payload or "queAuth" not in payload:
13041
+ return None
13042
+ return payload
13043
+
13044
+
13045
+ def _normalize_reference_config_for_save(
13046
+ reference: Any,
13047
+ *,
13048
+ field: dict[str, Any],
13049
+ ) -> dict[str, Any]:
13050
+ source = reference if isinstance(reference, dict) else {}
13051
+ payload = _copy_present_keys(source, _REFERENCE_CONFIG_SAVE_KEYS)
13052
+ if str(field.get("target_app_key") or "").strip():
13053
+ payload["referAppKey"] = str(field.get("target_app_key") or "").strip()
13054
+ if field.get("target_field_que_id") is not None:
13055
+ payload["referQueId"] = _coerce_nonnegative_int(field.get("target_field_que_id"))
13056
+ if field.get("field_name_show") is not None:
13057
+ payload["fieldNameShow"] = bool(field.get("field_name_show"))
13058
+
13059
+ refer_questions: list[dict[str, Any]] = []
13060
+ for index, raw_item in enumerate(cast(list[Any], source.get("referQuestions") or []), start=1):
13061
+ normalized_item = _normalize_reference_question_for_save(raw_item, ordinal=index)
13062
+ if normalized_item is not None:
13063
+ refer_questions.append(normalized_item)
13064
+ if refer_questions or "referQuestions" in source:
13065
+ payload["referQuestions"] = refer_questions
13066
+
13067
+ refer_fill_rules = [
13068
+ item
13069
+ for item in (
13070
+ _normalize_reference_fill_rule_for_save(raw_item)
13071
+ for raw_item in cast(list[Any], source.get("referFillRules") or [])
13072
+ )
13073
+ if item is not None
13074
+ ]
13075
+ if refer_fill_rules or "referFillRules" in source:
13076
+ payload["referFillRules"] = refer_fill_rules
13077
+
13078
+ refer_auth_ques = [
13079
+ item
13080
+ for item in (
13081
+ _normalize_reference_auth_question_for_save(raw_item)
13082
+ for raw_item in cast(list[Any], source.get("referAuthQues") or [])
13083
+ )
13084
+ if item is not None
13085
+ ]
13086
+ if refer_auth_ques or "referAuthQues" in source:
13087
+ payload["referAuthQues"] = refer_auth_ques
13088
+
13089
+ return payload
13090
+
13091
+
13092
+ def _normalize_relation_question_for_save(question: dict[str, Any], *, field: dict[str, Any]) -> dict[str, Any]:
13093
+ payload = _copy_present_keys(question, _RELATION_QUESTION_SAVE_KEYS)
13094
+ overlay_keys = _field_question_overlay_keys(field)
13095
+ que_id = _coerce_nonnegative_int(question.get("queId"))
13096
+ if que_id is not None:
13097
+ payload["queId"] = que_id
13098
+ que_temp_id = _coerce_nonnegative_int(question.get("queTempId"))
13099
+ if que_temp_id is not None and "queId" not in payload:
13100
+ payload["queTempId"] = que_temp_id
13101
+ payload["queType"] = _coerce_positive_int(question.get("queType")) or 25
13102
+ payload["queTitle"] = str(field.get("name") or question.get("queTitle") or "")
13103
+ if "required" in overlay_keys or "required" in question or field.get("required") is not None:
13104
+ payload["required"] = bool(field.get("required", question.get("required", False)))
13105
+ if "description" in overlay_keys:
13106
+ payload["queHint"] = "" if field.get("description") is None else str(field.get("description"))
13107
+ elif "queHint" in question and question.get("queHint") is not None:
13108
+ payload["queHint"] = str(question.get("queHint") or "")
13109
+ if field.get("default_type") is not None:
13110
+ payload["queDefaultType"] = _coerce_positive_int(field.get("default_type")) or 1
13111
+ if "default_value" in field:
13112
+ payload["queDefaultValue"] = field.get("default_value")
13113
+ payload["referenceConfig"] = _normalize_reference_config_for_save(question.get("referenceConfig"), field=field)
13114
+ return payload
13115
+
12842
13116
 
12843
13117
  def _build_form_save_base_payload(current_schema: dict[str, Any], title: str) -> dict[str, Any]:
12844
13118
  payload: dict[str, Any] = {"formTitle": title}
@@ -12863,6 +13137,8 @@ def _normalize_question_relations_for_save(question_relations: list[dict[str, An
12863
13137
  value = relation.get(key)
12864
13138
  if value is None:
12865
13139
  continue
13140
+ if key == "matchRuleFormula":
13141
+ value = _encode_formula_for_backend_save(value)
12866
13142
  item[key] = deepcopy(value)
12867
13143
  if item:
12868
13144
  normalized.append(item)
@@ -12899,7 +13175,7 @@ def _sync_question_title_references(value: Any, *, by_que_id: dict[int, str], by
12899
13175
  if not isinstance(value, dict):
12900
13176
  return
12901
13177
 
12902
- title_keys = ("queTitle", "supQueTitle", "_field_id")
13178
+ title_keys = ("queTitle", "_field_id")
12903
13179
  que_id = _coerce_nonnegative_int(value.get("queId"))
12904
13180
  replacement = None
12905
13181
  if que_id is not None and que_id in by_que_id:
@@ -12936,6 +13212,8 @@ def _materialize_preserved_question(field: dict[str, Any]) -> dict[str, Any] | N
12936
13212
  if "description" in overlay_keys:
12937
13213
  description = field.get("description")
12938
13214
  template["queHint"] = "" if description is None else str(description)
13215
+ if str(field.get("type") or "") == FieldType.relation.value:
13216
+ return _normalize_relation_question_for_save(template, field=field)
12939
13217
  return template
12940
13218
 
12941
13219
 
@@ -13043,6 +13321,7 @@ def _field_to_question(field: dict[str, Any], *, temp_id: int) -> dict[str, Any]
13043
13321
  if field.get("target_field_que_id") is not None:
13044
13322
  reference["referQueId"] = field.get("target_field_que_id")
13045
13323
  question["referenceConfig"] = reference
13324
+ question = _normalize_relation_question_for_save(question, field=field)
13046
13325
  if field.get("type") == FieldType.department.value:
13047
13326
  scope_type, scope_payload = _serialize_department_scope_for_question(field.get("department_scope"))
13048
13327
  question["deptSelectScopeType"] = scope_type
@@ -13182,6 +13461,7 @@ def _build_form_payload_from_fields(
13182
13461
  for row in form_rows:
13183
13462
  _apply_row_widths(row)
13184
13463
  payload = default_form_payload(title, form_rows)
13464
+ _normalize_formula_defaults_for_save(payload.get("formQues"))
13185
13465
  payload["editVersionNo"] = int(current_schema.get("editVersionNo") or 1)
13186
13466
  payload["questionRelations"] = _normalize_question_relations_for_save(
13187
13467
  question_relations if question_relations is not None else (current_schema.get("questionRelations") or [])
@@ -13299,6 +13579,7 @@ def _build_form_payload_for_edit_fields(
13299
13579
 
13300
13580
  payload = _build_form_save_base_payload(current_schema, title)
13301
13581
  payload["formQues"] = form_rows
13582
+ _normalize_formula_defaults_for_save(payload.get("formQues"))
13302
13583
  payload["questionRelations"] = normalized_relations
13303
13584
  payload["editVersionNo"] = int(current_schema.get("editVersionNo") or 1)
13304
13585
  return payload
@@ -14715,6 +14996,7 @@ def _build_form_payload_from_existing_schema(
14715
14996
 
14716
14997
  payload = _build_form_save_base_payload(current_schema, str(current_schema.get("formTitle") or "未命名应用"))
14717
14998
  payload["formQues"] = form_rows
14999
+ _normalize_formula_defaults_for_save(payload.get("formQues"))
14718
15000
  payload["questionRelations"] = _normalize_question_relations_for_save(current_schema.get("questionRelations") or [])
14719
15001
  payload["editVersionNo"] = int(current_schema.get("editVersionNo") or 1)
14720
15002
  payload.setdefault("formTitle", current_schema.get("formTitle") or "未命名应用")