@josephyan/qingflow-app-user-mcp 0.2.0-beta.986 → 0.2.0-beta.987

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.986
6
+ npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.987
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.986 qingflow-app-user-mcp
12
+ npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.987 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.986",
3
+ "version": "0.2.0-beta.987",
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.0b986"
7
+ version = "0.2.0b987"
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.0b986"
8
+ _FALLBACK_VERSION = "0.2.0b987"
9
9
 
10
10
 
11
11
  def _resolve_local_pyproject_version() -> str | None:
@@ -9811,6 +9811,15 @@ def _apply_relation_target_selection(
9811
9811
  config["refer_field_types"] = [item.get("type") for item in normalized_visible]
9812
9812
  config["auth_field_ids"] = [item.get("field_id") or item.get("name") for item in normalized_visible]
9813
9813
  config["auth_field_que_ids"] = [_coerce_positive_int(item.get("que_id")) or 0 for item in normalized_visible]
9814
+ config["refer_auth_ques"] = [
9815
+ {
9816
+ "queId": _coerce_positive_int(item.get("que_id")) or 0,
9817
+ "queAuth": _REFERENCE_FIELD_VISIBLE_AUTH,
9818
+ "_field_id": item.get("field_id") or item.get("name"),
9819
+ }
9820
+ for item in normalized_visible
9821
+ if (_coerce_positive_int(item.get("que_id")) or 0) > 0
9822
+ ]
9814
9823
  config["field_name_show"] = bool(field.get("field_name_show", True))
9815
9824
  field["target_field_id"] = display_field.get("field_id") or display_field.get("name")
9816
9825
  field["target_field_que_id"] = _coerce_positive_int(display_field.get("que_id")) or 0
@@ -10071,17 +10080,32 @@ def _parse_field(question: dict[str, Any], *, field_id_hint: str | None = None)
10071
10080
  field["target_app_key"] = reference.get("referAppKey")
10072
10081
  field["relation_mode"] = _relation_mode_from_optional_data_num(reference.get("optionalDataNum"))
10073
10082
  refer_questions = reference.get("referQuestions") if isinstance(reference.get("referQuestions"), list) else []
10083
+ refer_auth_questions = reference.get("referAuthQues") if isinstance(reference.get("referAuthQues"), list) else []
10084
+ refer_auth_by_que_id: dict[int, int] = {}
10085
+ for raw_item in refer_auth_questions:
10086
+ if not isinstance(raw_item, dict):
10087
+ continue
10088
+ que_id = _coerce_nonnegative_int(raw_item.get("queId"))
10089
+ que_auth = _coerce_nonnegative_int(raw_item.get("queAuth"))
10090
+ if que_id is None or que_auth is None or que_id in refer_auth_by_que_id:
10091
+ continue
10092
+ refer_auth_by_que_id[que_id] = que_auth
10074
10093
  visible_fields: list[dict[str, Any]] = []
10075
10094
  display_field_que_id = _coerce_nonnegative_int(reference.get("referQueId"))
10076
10095
  display_field_name: str | None = None
10077
10096
  for item in refer_questions:
10078
10097
  if not isinstance(item, dict):
10079
10098
  continue
10099
+ que_id = _coerce_nonnegative_int(item.get("queId"))
10100
+ que_auth = _coerce_nonnegative_int(item.get("queAuth"))
10101
+ if que_auth is None and que_id is not None:
10102
+ que_auth = refer_auth_by_que_id.get(que_id)
10080
10103
  selector = {
10081
- "que_id": _coerce_nonnegative_int(item.get("queId")),
10104
+ "que_id": que_id,
10082
10105
  "name": str(item.get("queTitle") or "").strip() or None,
10083
10106
  }
10084
- visible_fields.append(selector)
10107
+ if que_auth != _REFERENCE_FIELD_HIDDEN_AUTH:
10108
+ visible_fields.append(selector)
10085
10109
  if display_field_que_id is not None and selector["que_id"] == display_field_que_id:
10086
10110
  display_field_name = selector["name"]
10087
10111
  if display_field_name is None and visible_fields:
@@ -13275,6 +13299,333 @@ def _normalize_reference_auth_question_for_save(value: Any) -> dict[str, Any] |
13275
13299
  return payload
13276
13300
 
13277
13301
 
13302
+ def _dedupe_reference_auth_questions(auth_questions: list[dict[str, Any]]) -> list[dict[str, Any]]:
13303
+ deduped: list[dict[str, Any]] = []
13304
+ seen_que_ids: set[int] = set()
13305
+ for item in auth_questions:
13306
+ normalized_item = _normalize_reference_auth_question_for_save(item)
13307
+ if normalized_item is None:
13308
+ continue
13309
+ que_id = _coerce_any_int(normalized_item.get("queId"))
13310
+ if que_id is None or que_id in seen_que_ids:
13311
+ continue
13312
+ seen_que_ids.add(que_id)
13313
+ deduped.append(normalized_item)
13314
+ return deduped
13315
+
13316
+
13317
+ _REFERENCE_FIELD_HIDDEN_AUTH = 2
13318
+ _REFERENCE_FIELD_VISIBLE_AUTH = 3
13319
+
13320
+
13321
+ def _synthesize_reference_auth_questions_for_save(
13322
+ *,
13323
+ source: dict[str, Any],
13324
+ field: dict[str, Any],
13325
+ ) -> list[dict[str, Any]]:
13326
+ config = field.get("config") if isinstance(field.get("config"), dict) else {}
13327
+ synthesized: list[dict[str, Any]] = []
13328
+
13329
+ if isinstance(config.get("refer_auth_ques"), list):
13330
+ synthesized.extend(cast(list[dict[str, Any]], config.get("refer_auth_ques") or []))
13331
+ if synthesized:
13332
+ return _dedupe_reference_auth_questions(synthesized)
13333
+
13334
+ refer_question_ids_by_name: dict[str, int] = {}
13335
+ for raw_item in cast(list[Any], source.get("referQuestions") or []):
13336
+ if not isinstance(raw_item, dict):
13337
+ continue
13338
+ que_id = _coerce_any_int(raw_item.get("queId"))
13339
+ name = str(raw_item.get("queTitle") or "").strip()
13340
+ if que_id is None or not name or name in refer_question_ids_by_name:
13341
+ continue
13342
+ refer_question_ids_by_name[name] = que_id
13343
+
13344
+ visible_fields = cast(list[dict[str, Any]], field.get("visible_fields") or [])
13345
+ for item in visible_fields:
13346
+ if not isinstance(item, dict):
13347
+ continue
13348
+ que_id = _coerce_any_int(item.get("que_id"))
13349
+ if que_id is None:
13350
+ name = str(item.get("name") or "").strip()
13351
+ que_id = refer_question_ids_by_name.get(name)
13352
+ if que_id is None:
13353
+ continue
13354
+ synthesized.append({"queId": que_id, "queAuth": _REFERENCE_FIELD_VISIBLE_AUTH})
13355
+ if synthesized:
13356
+ return _dedupe_reference_auth_questions(synthesized)
13357
+
13358
+ auth_field_que_ids = cast(list[Any], config.get("auth_field_que_ids") or [])
13359
+ for raw_que_id in auth_field_que_ids:
13360
+ que_id = _coerce_any_int(raw_que_id)
13361
+ if que_id is None:
13362
+ continue
13363
+ synthesized.append({"queId": que_id, "queAuth": _REFERENCE_FIELD_VISIBLE_AUTH})
13364
+ if synthesized:
13365
+ return _dedupe_reference_auth_questions(synthesized)
13366
+
13367
+ for raw_item in cast(list[Any], source.get("referQuestions") or []):
13368
+ if not isinstance(raw_item, dict):
13369
+ continue
13370
+ que_id = _coerce_any_int(raw_item.get("queId"))
13371
+ if que_id is None:
13372
+ continue
13373
+ synthesized.append(
13374
+ {
13375
+ "queId": que_id,
13376
+ "queAuth": _REFERENCE_FIELD_VISIBLE_AUTH,
13377
+ }
13378
+ )
13379
+ if synthesized:
13380
+ return _dedupe_reference_auth_questions(synthesized)
13381
+
13382
+ fallback_que_id = _coerce_any_int(field.get("target_field_que_id"))
13383
+ if fallback_que_id is not None:
13384
+ synthesized.append({"queId": fallback_que_id, "queAuth": _REFERENCE_FIELD_VISIBLE_AUTH})
13385
+ return _dedupe_reference_auth_questions(synthesized)
13386
+
13387
+
13388
+ def _reference_question_auth_overrides_for_save(
13389
+ *,
13390
+ source: dict[str, Any],
13391
+ field: dict[str, Any],
13392
+ ) -> dict[int, int]:
13393
+ overrides: dict[int, int] = {}
13394
+ visible_que_ids: set[int] = set()
13395
+
13396
+ for item in cast(list[Any], field.get("visible_fields") or []):
13397
+ if not isinstance(item, dict):
13398
+ continue
13399
+ que_id = _coerce_any_int(item.get("que_id"))
13400
+ if que_id is not None:
13401
+ visible_que_ids.add(que_id)
13402
+
13403
+ if not visible_que_ids:
13404
+ refer_auth_ques = _synthesize_reference_auth_questions_for_save(source=source, field=field)
13405
+ for item in refer_auth_ques:
13406
+ que_id = _coerce_any_int(item.get("queId"))
13407
+ que_auth = _coerce_nonnegative_int(item.get("queAuth"))
13408
+ if que_id is None or que_auth is None:
13409
+ continue
13410
+ overrides[que_id] = que_auth
13411
+ if overrides:
13412
+ return overrides
13413
+
13414
+ for raw_item in cast(list[Any], source.get("referQuestions") or []):
13415
+ if not isinstance(raw_item, dict):
13416
+ continue
13417
+ que_id = _coerce_any_int(raw_item.get("queId"))
13418
+ if que_id is None:
13419
+ continue
13420
+ overrides[que_id] = (
13421
+ _REFERENCE_FIELD_VISIBLE_AUTH if que_id in visible_que_ids else _REFERENCE_FIELD_HIDDEN_AUTH
13422
+ )
13423
+ return overrides
13424
+
13425
+
13426
+ def _reference_question_matches_visible_selector(question: dict[str, Any], selector: dict[str, Any]) -> bool:
13427
+ question_que_id = _coerce_any_int(question.get("queId"))
13428
+ selector_que_id = _coerce_any_int(selector.get("que_id"))
13429
+ if question_que_id is not None and selector_que_id is not None and question_que_id == selector_que_id:
13430
+ return True
13431
+ question_name = str(question.get("queTitle") or "").strip()
13432
+ selector_name = str(selector.get("name") or "").strip()
13433
+ return bool(question_name and selector_name and question_name == selector_name)
13434
+
13435
+
13436
+ def _build_reference_question_from_visible_selector(
13437
+ selector: dict[str, Any],
13438
+ *,
13439
+ ordinal: int,
13440
+ ) -> dict[str, Any] | None:
13441
+ return _normalize_reference_question_for_save(
13442
+ {
13443
+ "queId": _coerce_any_int(selector.get("que_id")),
13444
+ "queTitle": str(selector.get("name") or "").strip() or None,
13445
+ "queType": str(selector.get("type") or "2"),
13446
+ "ordinal": ordinal,
13447
+ },
13448
+ ordinal=ordinal,
13449
+ )
13450
+
13451
+
13452
+ def _canonicalize_reference_questions_for_save(
13453
+ *,
13454
+ source: dict[str, Any],
13455
+ field: dict[str, Any],
13456
+ ) -> list[dict[str, Any]]:
13457
+ normalized_source_questions = [
13458
+ item
13459
+ for item in (
13460
+ _normalize_reference_question_for_save(raw_item, ordinal=index)
13461
+ for index, raw_item in enumerate(cast(list[Any], source.get("referQuestions") or []), start=1)
13462
+ )
13463
+ if item is not None
13464
+ ]
13465
+
13466
+ display_field = field.get("display_field") if isinstance(field.get("display_field"), dict) else None
13467
+ visible_fields = [item for item in cast(list[Any], field.get("visible_fields") or []) if isinstance(item, dict)]
13468
+ ordered_visible_selectors: list[dict[str, Any]] = []
13469
+ if display_field is not None:
13470
+ ordered_visible_selectors.append(display_field)
13471
+ for item in visible_fields:
13472
+ if any(_relation_target_field_matches(existing, item) for existing in ordered_visible_selectors):
13473
+ continue
13474
+ ordered_visible_selectors.append(item)
13475
+
13476
+ if not ordered_visible_selectors:
13477
+ return normalized_source_questions
13478
+
13479
+ canonical_questions: list[dict[str, Any]] = []
13480
+ used_source_indexes: set[int] = set()
13481
+
13482
+ for ordinal, selector in enumerate(ordered_visible_selectors, start=1):
13483
+ matched_index: int | None = None
13484
+ matched_item: dict[str, Any] | None = None
13485
+ for index, item in enumerate(normalized_source_questions):
13486
+ if index in used_source_indexes:
13487
+ continue
13488
+ if _reference_question_matches_visible_selector(item, selector):
13489
+ matched_index = index
13490
+ matched_item = deepcopy(item)
13491
+ break
13492
+ if matched_item is None:
13493
+ matched_item = _build_reference_question_from_visible_selector(selector, ordinal=ordinal)
13494
+ if matched_item is None:
13495
+ continue
13496
+ matched_item["ordinal"] = ordinal
13497
+ canonical_questions.append(matched_item)
13498
+ if matched_index is not None:
13499
+ used_source_indexes.add(matched_index)
13500
+
13501
+ next_ordinal = len(canonical_questions) + 1
13502
+ for index, item in enumerate(normalized_source_questions):
13503
+ if index in used_source_indexes:
13504
+ continue
13505
+ remaining_item = deepcopy(item)
13506
+ remaining_item["ordinal"] = next_ordinal
13507
+ next_ordinal += 1
13508
+ canonical_questions.append(remaining_item)
13509
+
13510
+ return canonical_questions
13511
+
13512
+
13513
+ def _canonicalize_reference_auth_questions_for_save(
13514
+ *,
13515
+ source: dict[str, Any],
13516
+ refer_questions: list[dict[str, Any]],
13517
+ ) -> list[dict[str, Any]]:
13518
+ source_auth_questions = [
13519
+ item
13520
+ for item in (
13521
+ _normalize_reference_auth_question_for_save(raw_item)
13522
+ for raw_item in cast(list[Any], source.get("referAuthQues") or [])
13523
+ )
13524
+ if item is not None
13525
+ ]
13526
+ source_auth_by_que_id: dict[int, dict[str, Any]] = {}
13527
+ for item in source_auth_questions:
13528
+ que_id = _coerce_any_int(item.get("queId"))
13529
+ if que_id is None or que_id in source_auth_by_que_id:
13530
+ continue
13531
+ source_auth_by_que_id[que_id] = item
13532
+
13533
+ auth_questions: list[dict[str, Any]] = []
13534
+ for item in refer_questions:
13535
+ que_id = _coerce_any_int(item.get("queId"))
13536
+ que_auth = _coerce_nonnegative_int(item.get("queAuth"))
13537
+ if que_id is None or que_auth is None:
13538
+ continue
13539
+ payload = deepcopy(source_auth_by_que_id.get(que_id) or {"queId": que_id})
13540
+ payload["queId"] = que_id
13541
+ payload["queAuth"] = que_auth
13542
+ auth_questions.append(payload)
13543
+ return _dedupe_reference_auth_questions(auth_questions)
13544
+
13545
+
13546
+ def _enforce_reference_config_consistency_for_save(
13547
+ payload: dict[str, Any],
13548
+ *,
13549
+ field: dict[str, Any],
13550
+ ) -> dict[str, Any]:
13551
+ refer_questions = [
13552
+ item
13553
+ for item in (
13554
+ _normalize_reference_question_for_save(raw_item, ordinal=index)
13555
+ for index, raw_item in enumerate(cast(list[Any], payload.get("referQuestions") or []), start=1)
13556
+ )
13557
+ if item is not None
13558
+ ]
13559
+ if not refer_questions:
13560
+ return payload
13561
+
13562
+ refer_auth_ques = _dedupe_reference_auth_questions(
13563
+ [
13564
+ item
13565
+ for item in (
13566
+ _normalize_reference_auth_question_for_save(raw_item)
13567
+ for raw_item in cast(list[Any], payload.get("referAuthQues") or [])
13568
+ )
13569
+ if item is not None
13570
+ ]
13571
+ )
13572
+ refer_auth_by_que_id: dict[int, int] = {}
13573
+ for item in refer_auth_ques:
13574
+ que_id = _coerce_any_int(item.get("queId"))
13575
+ que_auth = _coerce_nonnegative_int(item.get("queAuth"))
13576
+ if que_id is None or que_auth is None or que_id in refer_auth_by_que_id:
13577
+ continue
13578
+ refer_auth_by_que_id[que_id] = que_auth
13579
+
13580
+ display_field_que_id = _coerce_any_int(payload.get("referQueId"))
13581
+ if display_field_que_id is None:
13582
+ display_field_que_id = _coerce_any_int(field.get("target_field_que_id"))
13583
+ if display_field_que_id is not None:
13584
+ payload["referQueId"] = display_field_que_id
13585
+
13586
+ if display_field_que_id is not None and not any(
13587
+ _coerce_any_int(item.get("queId")) == display_field_que_id for item in refer_questions
13588
+ ):
13589
+ display_selector = field.get("display_field") if isinstance(field.get("display_field"), dict) else None
13590
+ display_question = (
13591
+ _build_reference_question_from_visible_selector(display_selector, ordinal=1)
13592
+ if display_selector is not None
13593
+ else None
13594
+ )
13595
+ if display_question is not None:
13596
+ display_question["queId"] = display_field_que_id
13597
+ display_question["queAuth"] = _REFERENCE_FIELD_VISIBLE_AUTH
13598
+ refer_questions = [display_question, *refer_questions]
13599
+
13600
+ if display_field_que_id is not None:
13601
+ display_questions = [
13602
+ item for item in refer_questions if _coerce_any_int(item.get("queId")) == display_field_que_id
13603
+ ]
13604
+ trailing_questions = [
13605
+ item for item in refer_questions if _coerce_any_int(item.get("queId")) != display_field_que_id
13606
+ ]
13607
+ refer_questions = [*display_questions, *trailing_questions]
13608
+
13609
+ for ordinal, item in enumerate(refer_questions, start=1):
13610
+ que_id = _coerce_any_int(item.get("queId"))
13611
+ if que_id is None:
13612
+ continue
13613
+ item["ordinal"] = ordinal
13614
+ item["queAuth"] = refer_auth_by_que_id.get(
13615
+ que_id,
13616
+ _coerce_nonnegative_int(item.get("queAuth")) or _REFERENCE_FIELD_VISIBLE_AUTH,
13617
+ )
13618
+ if display_field_que_id is not None and que_id == display_field_que_id:
13619
+ item["queAuth"] = _REFERENCE_FIELD_VISIBLE_AUTH
13620
+
13621
+ payload["referQuestions"] = refer_questions
13622
+ payload["referAuthQues"] = _canonicalize_reference_auth_questions_for_save(
13623
+ source={"referAuthQues": refer_auth_ques},
13624
+ refer_questions=refer_questions,
13625
+ )
13626
+ return payload
13627
+
13628
+
13278
13629
  def _normalize_reference_config_for_save(
13279
13630
  reference: Any,
13280
13631
  *,
@@ -13289,11 +13640,13 @@ def _normalize_reference_config_for_save(
13289
13640
  if field.get("field_name_show") is not None:
13290
13641
  payload["fieldNameShow"] = bool(field.get("field_name_show"))
13291
13642
 
13292
- refer_questions: list[dict[str, Any]] = []
13293
- for index, raw_item in enumerate(cast(list[Any], source.get("referQuestions") or []), start=1):
13294
- normalized_item = _normalize_reference_question_for_save(raw_item, ordinal=index)
13295
- if normalized_item is not None:
13296
- refer_questions.append(normalized_item)
13643
+ refer_question_auth_overrides = _reference_question_auth_overrides_for_save(source=source, field=field)
13644
+ refer_questions = _canonicalize_reference_questions_for_save(source=source, field=field)
13645
+ for index, normalized_item in enumerate(refer_questions, start=1):
13646
+ que_id = _coerce_any_int(normalized_item.get("queId"))
13647
+ if que_id is not None and que_id in refer_question_auth_overrides:
13648
+ normalized_item["queAuth"] = refer_question_auth_overrides[que_id]
13649
+ normalized_item["ordinal"] = index
13297
13650
  if refer_questions or "referQuestions" in source:
13298
13651
  payload["referQuestions"] = refer_questions
13299
13652
 
@@ -13308,18 +13661,13 @@ def _normalize_reference_config_for_save(
13308
13661
  if refer_fill_rules or "referFillRules" in source:
13309
13662
  payload["referFillRules"] = refer_fill_rules
13310
13663
 
13311
- refer_auth_ques = [
13312
- item
13313
- for item in (
13314
- _normalize_reference_auth_question_for_save(raw_item)
13315
- for raw_item in cast(list[Any], source.get("referAuthQues") or [])
13316
- )
13317
- if item is not None
13318
- ]
13664
+ refer_auth_ques = _canonicalize_reference_auth_questions_for_save(source=source, refer_questions=refer_questions)
13665
+ if not refer_auth_ques:
13666
+ refer_auth_ques = _synthesize_reference_auth_questions_for_save(source=source, field=field)
13319
13667
  if refer_auth_ques or "referAuthQues" in source:
13320
13668
  payload["referAuthQues"] = refer_auth_ques
13321
13669
 
13322
- return payload
13670
+ return _enforce_reference_config_consistency_for_save(payload, field=field)
13323
13671
 
13324
13672
 
13325
13673
  def _normalize_relation_question_for_save(question: dict[str, Any], *, field: dict[str, Any]) -> dict[str, Any]:
@@ -13550,9 +13898,16 @@ def _field_to_question(field: dict[str, Any], *, temp_id: int) -> dict[str, Any]
13550
13898
  preserved_reference["referAppKey"] = field.get("target_app_key")
13551
13899
  question["referenceConfig"] = preserved_reference
13552
13900
  else:
13901
+ existing_reference = (
13902
+ deepcopy(relation_question_template.get("referenceConfig"))
13903
+ if relation_question_template is not None and isinstance(relation_question_template.get("referenceConfig"), dict)
13904
+ else deepcopy(question.get("referenceConfig"))
13905
+ if isinstance(question.get("referenceConfig"), dict)
13906
+ else {}
13907
+ )
13553
13908
  reference = (
13554
- deepcopy(built_question.get("referenceConfig"))
13555
- if relation_config_explicit and isinstance(built_question.get("referenceConfig"), dict)
13909
+ existing_reference
13910
+ if relation_config_explicit
13556
13911
  else deepcopy(question.get("referenceConfig"))
13557
13912
  if isinstance(question.get("referenceConfig"), dict)
13558
13913
  else {}
@@ -13573,6 +13928,8 @@ def _field_to_question(field: dict[str, Any], *, temp_id: int) -> dict[str, Any]
13573
13928
  "fieldNameShow",
13574
13929
  "_targetFieldId",
13575
13930
  ):
13931
+ if relation_config_explicit and key in {"referQuestions", "referAuthQues"}:
13932
+ continue
13576
13933
  if key in built_reference:
13577
13934
  reference[key] = deepcopy(built_reference[key])
13578
13935
  reference["referAppKey"] = field.get("target_app_key")
@@ -15,6 +15,10 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
15
15
  list_parser.add_argument("--include-external", action="store_true")
16
16
  list_parser.set_defaults(handler=_handle_list, format_hint="workspace_list")
17
17
 
18
+ get_parser = workspace_subparsers.add_parser("get", help="读取工作区详情")
19
+ get_parser.add_argument("--ws-id", type=int, default=0)
20
+ get_parser.set_defaults(handler=_handle_get, format_hint="workspace_get")
21
+
18
22
 
19
23
  def _handle_list(args: argparse.Namespace, context: CliContext) -> dict:
20
24
  return context.workspace.workspace_list(
@@ -23,3 +27,10 @@ def _handle_list(args: argparse.Namespace, context: CliContext) -> dict:
23
27
  page_size=args.page_size,
24
28
  include_external=bool(args.include_external),
25
29
  )
30
+
31
+
32
+ def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
33
+ return context.workspace.workspace_get(
34
+ profile=args.profile,
35
+ ws_id=args.ws_id if int(args.ws_id or 0) > 0 else None,
36
+ )
@@ -38,8 +38,12 @@ def _format_whoami(result: dict[str, Any]) -> str:
38
38
  f"User: {result.get('nick_name') or '-'} ({result.get('email') or '-'})",
39
39
  f"UID: {result.get('uid')}",
40
40
  f"Workspace: {result.get('selected_ws_name') or '-'} ({result.get('selected_ws_id') or '-'})",
41
- f"QF Version: {result.get('qf_version') or '-'}",
41
+ f"Workspace QF Version: {result.get('qf_version') or '-'}",
42
42
  ]
43
+ request_route = result.get("request_route") if isinstance(result.get("request_route"), dict) else {}
44
+ route_qf_version = request_route.get("qf_version")
45
+ if route_qf_version and route_qf_version != result.get("qf_version"):
46
+ lines.append(f"Request Route QF Version: {route_qf_version}")
43
47
  lines.append(f"Permission Level: {result.get('permission_level') or '-'}")
44
48
  departments = result.get("departments") if isinstance(result.get("departments"), list) else []
45
49
  roles = result.get("roles") if isinstance(result.get("roles"), list) else []
@@ -82,6 +86,19 @@ def _format_workspace_list(result: dict[str, Any]) -> str:
82
86
  return _render_titled_table("Workspaces", ["ws_id", "name", "remark"], rows)
83
87
 
84
88
 
89
+ def _format_workspace_get(result: dict[str, Any]) -> str:
90
+ workspace = result.get("workspace") if isinstance(result.get("workspace"), dict) else {}
91
+ lines = [
92
+ f"Workspace: {workspace.get('workspaceName') or workspace.get('wsName') or '-'} ({workspace.get('wsId') or result.get('ws_id') or '-'})",
93
+ f"QF Version: {result.get('qf_version') or workspace.get('systemVersion') or '-'}",
94
+ f"Identity: {workspace.get('identity') or '-'}",
95
+ f"Auth: {workspace.get('auth') if workspace.get('auth') is not None else '-'}",
96
+ f"State: {workspace.get('state') if workspace.get('state') is not None else '-'}",
97
+ ]
98
+ _append_warnings(lines, result.get("warnings"))
99
+ return "\n".join(lines) + "\n"
100
+
101
+
85
102
  def _format_app_items(result: dict[str, Any]) -> str:
86
103
  items = result.get("items")
87
104
  if not isinstance(items, list):
@@ -344,6 +361,7 @@ def _first_present(payload: dict[str, Any], *keys: str) -> Any:
344
361
  _FORMATTERS = {
345
362
  "auth_whoami": _format_whoami,
346
363
  "workspace_list": _format_workspace_list,
364
+ "workspace_get": _format_workspace_get,
347
365
  "app_list": _format_app_items,
348
366
  "app_search": _format_app_items,
349
367
  "app_get": _format_app_get,
@@ -34,6 +34,7 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
34
34
  PublicToolSpec(USER_DOMAIN, "auth_whoami", ("auth_whoami",), ("auth", "whoami")),
35
35
  PublicToolSpec(USER_DOMAIN, "auth_logout", ("auth_logout",), ("auth", "logout")),
36
36
  PublicToolSpec(USER_DOMAIN, "workspace_list", ("workspace_list",), ("workspace", "list")),
37
+ PublicToolSpec(USER_DOMAIN, "workspace_get", ("workspace_get",), ("workspace", "get")),
37
38
  PublicToolSpec(USER_DOMAIN, "app_list", ("app_list",), ("app", "list"), cli_show_effective_context=True),
38
39
  PublicToolSpec(USER_DOMAIN, "app_search", ("app_search",), ("app", "search"), cli_show_effective_context=True),
39
40
  PublicToolSpec(USER_DOMAIN, "app_get", ("app_get",), ("app", "get"), cli_show_effective_context=True),
@@ -109,6 +110,7 @@ BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
109
110
  PublicToolSpec(BUILDER_DOMAIN, "auth_whoami", ("auth_whoami",), ("builder", "auth", "whoami"), cli_public=False),
110
111
  PublicToolSpec(BUILDER_DOMAIN, "auth_logout", ("auth_logout",), ("builder", "auth", "logout"), cli_public=False),
111
112
  PublicToolSpec(BUILDER_DOMAIN, "workspace_list", ("workspace_list",), ("builder", "workspace", "list"), cli_public=False),
113
+ PublicToolSpec(BUILDER_DOMAIN, "workspace_get", ("workspace_get",), ("builder", "workspace", "get"), cli_public=False),
112
114
  PublicToolSpec(BUILDER_DOMAIN, "file_upload_local", ("file_upload_local",), ("builder", "file", "upload-local"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
113
115
  PublicToolSpec(BUILDER_DOMAIN, "feedback_submit", ("feedback_submit",), ("builder", "feedback", "submit"), has_contract=True),
114
116
  PublicToolSpec(BUILDER_DOMAIN, "builder_tool_contract", ("builder_tool_contract",), ("builder", "contract"), has_contract=False),
@@ -307,7 +307,7 @@ def build_reference_config(field: dict[str, Any], temp_id: int) -> dict[str, Any
307
307
  "queId": que_id,
308
308
  "queTitle": label,
309
309
  "queType": _normalize_reference_que_type(raw_type) or "2",
310
- "queAuth": 1,
310
+ "queAuth": 3,
311
311
  "ordinal": ordinal,
312
312
  "quoteId": temp_id,
313
313
  "_field_id": field_id,
@@ -320,7 +320,7 @@ def build_reference_config(field: dict[str, Any], temp_id: int) -> dict[str, Any
320
320
  auth_ques = []
321
321
  for ordinal, field_id in enumerate(auth_field_ids, start=1):
322
322
  que_id = auth_field_que_ids[ordinal - 1] if ordinal - 1 < len(auth_field_que_ids) else 0
323
- auth_ques.append({"queId": que_id, "queAuth": 1, "_field_id": field_id})
323
+ auth_ques.append({"queId": que_id, "queAuth": 3, "_field_id": field_id})
324
324
  return {
325
325
  "referAppKey": "__TARGET_APP_KEY__",
326
326
  "referQueId": display_field_que_id,
@@ -857,12 +857,12 @@ class SolutionExecutor:
857
857
  if refer_que_id is None:
858
858
  continue
859
859
  resolved["queId"] = refer_que_id
860
- resolved["queAuth"] = int(resolved.get("queAuth", 1))
860
+ resolved["queAuth"] = int(resolved.get("queAuth", 3))
861
861
  auth_ques.append(resolved)
862
862
  if not auth_ques:
863
863
  fallback_que_id = target_meta.get("by_field_id", {}).get(target_field_id)
864
864
  if fallback_que_id is not None:
865
- auth_ques.append({"queId": fallback_que_id, "queAuth": 1})
865
+ auth_ques.append({"queId": fallback_que_id, "queAuth": 3})
866
866
  reference_config["referAuthQues"] = auth_ques
867
867
  reference_config["fieldNameShow"] = bool(reference_config.get("fieldNameShow", True))
868
868
  fill_rules = []
@@ -212,6 +212,36 @@ class AuthTools(ToolBase):
212
212
  backend_session, # type: ignore[no-untyped-def]
213
213
  context: BackendRequestContext,
214
214
  ) -> dict[str, Any]:
215
+ workspace, workspace_qf_version = self._selected_workspace_snapshot(
216
+ session_profile=session_profile,
217
+ backend_session=backend_session,
218
+ )
219
+ resolved_qf_version = workspace_qf_version or session_profile.qf_version
220
+ resolved_qf_version_source = (
221
+ "workspace_system_version"
222
+ if workspace_qf_version is not None
223
+ else session_profile.qf_version_source
224
+ )
225
+ if (
226
+ workspace_qf_version is not None
227
+ and (
228
+ workspace_qf_version != session_profile.qf_version
229
+ or session_profile.qf_version_source != "workspace_system_version"
230
+ )
231
+ ):
232
+ session_profile = self.sessions.update_route(
233
+ profile,
234
+ qf_version=workspace_qf_version,
235
+ qf_version_source="workspace_system_version",
236
+ )
237
+ backend_session = self.sessions.get_backend_session(profile) or backend_session
238
+ context = BackendRequestContext(
239
+ base_url=backend_session.base_url,
240
+ token=backend_session.token,
241
+ ws_id=session_profile.selected_ws_id,
242
+ qf_version=backend_session.qf_version,
243
+ qf_version_source=backend_session.qf_version_source,
244
+ )
215
245
  if self._should_refresh_identity_metadata(session_profile):
216
246
  refreshed_profile = self._refresh_identity_metadata(
217
247
  profile=profile,
@@ -224,8 +254,8 @@ class AuthTools(ToolBase):
224
254
  response = {
225
255
  "profile": session_profile.profile,
226
256
  "base_url": session_profile.base_url,
227
- "qf_version": session_profile.qf_version,
228
- "qf_version_source": session_profile.qf_version_source,
257
+ "qf_version": resolved_qf_version,
258
+ "qf_version_source": resolved_qf_version_source,
229
259
  "uid": session_profile.uid,
230
260
  "email": session_profile.email,
231
261
  "nick_name": session_profile.nick_name,
@@ -512,6 +542,24 @@ class AuthTools(ToolBase):
512
542
  raise_tool_error(QingflowApiError(category="workspace", message=f"Workspace {ws_id} is not accessible"))
513
543
  return workspace
514
544
 
545
+ def _selected_workspace_snapshot(
546
+ self,
547
+ *,
548
+ session_profile, # type: ignore[no-untyped-def]
549
+ backend_session, # type: ignore[no-untyped-def]
550
+ ) -> tuple[dict[str, Any] | None, str | None]:
551
+ ws_id = session_profile.selected_ws_id
552
+ if ws_id is None:
553
+ return None, None
554
+ workspace = self._fetch_workspace_with_name_fallback(
555
+ session_profile.base_url,
556
+ backend_session.token,
557
+ ws_id,
558
+ qf_version=session_profile.qf_version,
559
+ qf_version_source=session_profile.qf_version_source,
560
+ )
561
+ return workspace, self._workspace_system_version(workspace)
562
+
515
563
  def _request_route_payload(self, context: BackendRequestContext) -> dict[str, Any]:
516
564
  """执行内部辅助逻辑。"""
517
565
  describe_route = getattr(self.backend, "describe_route", None)
@@ -35,6 +35,16 @@ class WorkspaceTools(ToolBase):
35
35
  include_external=include_external,
36
36
  )
37
37
 
38
+ @mcp.tool()
39
+ def workspace_get(
40
+ profile: str = DEFAULT_PROFILE,
41
+ ws_id: int = 0,
42
+ ) -> dict[str, Any]:
43
+ return self.workspace_get(
44
+ profile=profile,
45
+ ws_id=ws_id if ws_id > 0 else None,
46
+ )
47
+
38
48
  @mcp.tool()
39
49
  def workspace_set_plugin_status(
40
50
  profile: str = DEFAULT_PROFILE,
@@ -93,6 +103,31 @@ class WorkspaceTools(ToolBase):
93
103
 
94
104
  return self._run(profile, runner, require_workspace=False)
95
105
 
106
+ @tool_cn_name("工作区详情")
107
+ def workspace_get(
108
+ self,
109
+ *,
110
+ profile: str = DEFAULT_PROFILE,
111
+ ws_id: int | None = None,
112
+ ) -> dict[str, Any]:
113
+ """读取单个工作区详情,并尽量补齐真实 systemVersion。"""
114
+
115
+ def runner(session_profile, context):
116
+ target_ws_id = ws_id or (session_profile.selected_ws_id if session_profile is not None else None)
117
+ if target_ws_id is None or target_ws_id <= 0:
118
+ raise_tool_error(QingflowApiError.workspace_not_selected(profile))
119
+ workspace = self._fetch_workspace_with_fallback(context, ws_id=target_ws_id)
120
+ system_version = self._workspace_system_version(workspace)
121
+ return {
122
+ "profile": profile,
123
+ "ws_id": target_ws_id,
124
+ "qf_version": system_version,
125
+ "qf_version_source": "workspace_system_version" if system_version else "unverified",
126
+ "workspace": workspace,
127
+ }
128
+
129
+ return self._run(profile, runner, require_workspace=False)
130
+
96
131
  @tool_cn_name("设置工作区插件状态")
97
132
  def workspace_set_plugin_status(
98
133
  self,
@@ -123,3 +158,62 @@ class WorkspaceTools(ToolBase):
123
158
  }
124
159
 
125
160
  return self._run(profile, runner)
161
+
162
+ def _fetch_workspace_with_fallback(
163
+ self,
164
+ context: BackendRequestContext,
165
+ *,
166
+ ws_id: int,
167
+ ) -> dict[str, Any]:
168
+ workspace = self.backend.request("GET", context, f"/user/workspace/{ws_id}")
169
+ if not isinstance(workspace, dict):
170
+ raise_tool_error(QingflowApiError(category="workspace", message=f"Workspace {ws_id} is not accessible"))
171
+ if self._workspace_needs_list_fallback(workspace):
172
+ fallback = self._fetch_workspace_from_list(context, ws_id=ws_id)
173
+ if isinstance(fallback, dict):
174
+ merged = dict(workspace)
175
+ for key, value in fallback.items():
176
+ if merged.get(key) in (None, "") and value not in (None, ""):
177
+ merged[key] = value
178
+ workspace = merged
179
+ return workspace
180
+
181
+ def _fetch_workspace_from_list(self, context: BackendRequestContext, *, ws_id: int) -> dict[str, Any] | None:
182
+ payload = self.backend.request(
183
+ "POST",
184
+ BackendRequestContext(
185
+ base_url=context.base_url,
186
+ token=context.token,
187
+ ws_id=None,
188
+ qf_version=context.qf_version,
189
+ qf_version_source=context.qf_version_source,
190
+ ),
191
+ "/user/workspaceList/pageQuery",
192
+ json_body={"pageNum": 1, "pageSize": 100, "authList": [0, 1, 2, 3]},
193
+ )
194
+ workspaces = payload.get("list") if isinstance(payload, dict) else []
195
+ if not isinstance(workspaces, list):
196
+ return None
197
+ found = next(
198
+ (
199
+ item
200
+ for item in workspaces
201
+ if isinstance(item, dict) and item.get("wsId") == ws_id
202
+ ),
203
+ None,
204
+ )
205
+ return found if isinstance(found, dict) else None
206
+
207
+ def _workspace_needs_list_fallback(self, workspace: dict[str, Any]) -> bool:
208
+ workspace_name = str(workspace.get("workspaceName") or workspace.get("wsName") or "").strip()
209
+ system_version = self._workspace_system_version(workspace)
210
+ return not workspace_name or system_version is None
211
+
212
+ def _workspace_system_version(self, workspace: Any) -> str | None:
213
+ if not isinstance(workspace, dict):
214
+ return None
215
+ value = workspace.get("systemVersion")
216
+ if value is None:
217
+ return None
218
+ normalized = str(value).strip()
219
+ return normalized or None