@josephyan/qingflow-cli 0.2.0-beta.76 → 0.2.0-beta.78

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-cli@0.2.0-beta.76
6
+ npm install @josephyan/qingflow-cli@0.2.0-beta.78
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-cli@0.2.0-beta.76 qingflow
12
+ npx -y -p @josephyan/qingflow-cli@0.2.0-beta.78 qingflow
13
13
  ```
14
14
 
15
15
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-cli",
3
- "version": "0.2.0-beta.76",
3
+ "version": "0.2.0-beta.78",
4
4
  "description": "Human-friendly Qingflow command line interface for auth, record operations, import, tasks, and stable builder flows.",
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.0b76"
7
+ version = "0.2.0b78"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -2,4 +2,4 @@ from __future__ import annotations
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.2.0b48"
5
+ __version__ = "0.2.0b78"
@@ -291,7 +291,7 @@ class FieldSelector(StrictModel):
291
291
 
292
292
  @model_validator(mode="after")
293
293
  def validate_selector(self) -> "FieldSelector":
294
- if not any((self.field_id, self.que_id, self.name)):
294
+ if self.field_id is None and self.que_id is None and self.name is None:
295
295
  raise ValueError("selector must include field_id, que_id, or name")
296
296
  return self
297
297
 
@@ -3530,6 +3530,12 @@ class AiBuilderFacade:
3530
3530
  app_name=str(resolved["app_name"]),
3531
3531
  tag_ids=_coerce_int_list(resolved.get("tag_ids")),
3532
3532
  )
3533
+ requested_app_name = str(app_name or "").strip() or None
3534
+ requested_rename = requested_app_name if app_key else None
3535
+ requested_icon = str(icon or "").strip() or None
3536
+ requested_color = str(color or "").strip() or None
3537
+ requested_app_base_update = bool(requested_rename or requested_icon or requested_color)
3538
+ effective_app_name = target.app_name
3533
3539
  if not bool(resolved.get("created")):
3534
3540
  permission_outcome = self._guard_app_permission(
3535
3541
  profile=profile,
@@ -3540,21 +3546,35 @@ class AiBuilderFacade:
3540
3546
  if permission_outcome.block is not None:
3541
3547
  return permission_outcome.block
3542
3548
  permission_outcomes.append(permission_outcome)
3543
- visual_result = self._ensure_app_base_visuals(
3544
- profile=profile,
3545
- app_key=target.app_key,
3546
- fallback_title=target.app_name,
3547
- icon=icon,
3548
- color=color,
3549
- normalized_args=normalized_args,
3550
- )
3551
- if visual_result.get("status") == "failed":
3552
- return finalize(visual_result)
3549
+ if requested_app_base_update:
3550
+ visual_result = self._ensure_app_base_visuals(
3551
+ profile=profile,
3552
+ app_key=target.app_key,
3553
+ fallback_title=target.app_name,
3554
+ app_name=requested_rename,
3555
+ icon=requested_icon,
3556
+ color=requested_color,
3557
+ normalized_args=normalized_args,
3558
+ )
3559
+ if visual_result.get("status") == "failed":
3560
+ return finalize(visual_result)
3561
+ effective_app_name = str(visual_result.get("app_name_after") or target.app_name).strip() or target.app_name
3562
+ else:
3563
+ visual_result = {
3564
+ "status": "success",
3565
+ "updated": False,
3566
+ "app_icon": None,
3567
+ "app_name_before": target.app_name,
3568
+ "app_name_after": target.app_name,
3569
+ "request_id": None,
3570
+ }
3553
3571
  else:
3554
3572
  visual_result = {
3555
3573
  "status": "success",
3556
3574
  "updated": False,
3557
3575
  "app_icon": str(resolved.get("app_icon") or "").strip() or None,
3576
+ "app_name_before": target.app_name,
3577
+ "app_name_after": target.app_name,
3558
3578
  "request_id": None,
3559
3579
  }
3560
3580
  if bool(resolved.get("created")) and not requested_field_changes:
@@ -3576,10 +3596,14 @@ class AiBuilderFacade:
3576
3596
  "package_attached": None if package_tag_id is None else package_tag_id in target.tag_ids,
3577
3597
  "relation_field_limit_verified": True,
3578
3598
  "app_visuals_verified": True,
3599
+ "app_base_verified": True,
3579
3600
  "publish_skipped": True,
3580
3601
  },
3581
3602
  "app_key": target.app_key,
3582
3603
  "app_icon": str(resolved.get("app_icon") or visual_result.get("app_icon") or "").strip() or None,
3604
+ "app_name_before": str(visual_result.get("app_name_before") or target.app_name),
3605
+ "app_name_after": str(visual_result.get("app_name_after") or target.app_name),
3606
+ "app_base_updated": bool(visual_result.get("updated")),
3583
3607
  "created": True,
3584
3608
  "field_diff": {"added": [], "updated": [], "removed": []},
3585
3609
  "verified": True,
@@ -3602,7 +3626,7 @@ class AiBuilderFacade:
3602
3626
  details=_with_state_read_blocked_details({"app_key": target.app_key}, resource="schema", error=api_error),
3603
3627
  suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": target.app_key}},
3604
3628
  ))
3605
- schema_result = _empty_schema_result(target.app_name)
3629
+ schema_result = _empty_schema_result(effective_app_name)
3606
3630
  _schema_source = "synthetic_new_app"
3607
3631
  schema_readback_delayed = True
3608
3632
  parsed = _parse_schema(schema_result)
@@ -3759,13 +3783,19 @@ class AiBuilderFacade:
3759
3783
  )
3760
3784
 
3761
3785
  if not added and not updated and not removed and not normalized_code_block_fields and not bool(resolved.get("created")):
3762
- tag_ids_after = _coerce_int_list((self.apps.app_get_base(profile=profile, app_key=target.app_key, include_raw=True).get("result") or {}).get("tagIds"))
3786
+ base_info = self.apps.app_get_base(profile=profile, app_key=target.app_key, include_raw=True).get("result") or {}
3787
+ tag_ids_after = _coerce_int_list(base_info.get("tagIds"))
3763
3788
  package_attached = None if package_tag_id is None else package_tag_id in tag_ids_after
3789
+ actual_app_name = str(base_info.get("formTitle") or effective_app_name).strip() or effective_app_name
3790
+ actual_app_icon = str(base_info.get("appIcon") or visual_result.get("app_icon") or "").strip() or None
3791
+ expected_app_icon = str(visual_result.get("app_icon") or "").strip() or None
3792
+ app_base_verified = actual_app_name == effective_app_name and actual_app_icon == expected_app_icon
3793
+ verified = app_base_verified and relation_target_metadata_verified
3764
3794
  response = {
3765
- "status": "success",
3766
- "error_code": None,
3767
- "recoverable": False,
3768
- "message": "updated app visuals; schema already matches requested state" if bool(visual_result.get("updated")) else "schema already matches requested state",
3795
+ "status": "success" if verified else "partial_success",
3796
+ "error_code": None if verified else "APP_BASE_READBACK_PENDING",
3797
+ "recoverable": not verified,
3798
+ "message": "updated app base metadata; schema already matches requested state" if bool(visual_result.get("updated")) else "schema already matches requested state",
3769
3799
  "normalized_args": normalized_args,
3770
3800
  "missing_fields": [],
3771
3801
  "allowed_values": {"field_types": [item.value for item in PublicFieldType]},
@@ -3777,14 +3807,18 @@ class AiBuilderFacade:
3777
3807
  "verification": {
3778
3808
  "fields_verified": True,
3779
3809
  "relation_field_limit_verified": relation_limit_verified,
3780
- "app_visuals_verified": True,
3810
+ "app_visuals_verified": app_base_verified,
3811
+ "app_base_verified": app_base_verified,
3781
3812
  "relation_target_metadata_verified": relation_target_metadata_verified,
3782
3813
  },
3783
3814
  "app_key": target.app_key,
3784
3815
  "app_icon": str(visual_result.get("app_icon") or "").strip() or None,
3816
+ "app_name_before": str(visual_result.get("app_name_before") or target.app_name),
3817
+ "app_name_after": effective_app_name,
3818
+ "app_base_updated": bool(visual_result.get("updated")),
3785
3819
  "created": False,
3786
3820
  "field_diff": {"added": [], "updated": [], "removed": []},
3787
- "verified": True,
3821
+ "verified": verified,
3788
3822
  "tag_ids_after": tag_ids_after,
3789
3823
  "package_attached": package_attached,
3790
3824
  }
@@ -3796,7 +3830,7 @@ class AiBuilderFacade:
3796
3830
  return finalize(self._append_publish_result(profile=profile, app_key=target.app_key, publish=publish, response=response))
3797
3831
 
3798
3832
  payload = _build_form_payload_from_fields(
3799
- title=schema_result.get("formTitle") or target.app_name,
3833
+ title=effective_app_name,
3800
3834
  current_schema=schema_result,
3801
3835
  fields=current_fields,
3802
3836
  layout=layout,
@@ -3904,7 +3938,7 @@ class AiBuilderFacade:
3904
3938
  suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
3905
3939
  )
3906
3940
  rebound_payload = _build_form_payload_from_fields(
3907
- title=rebound_schema.get("formTitle") or target.app_name,
3941
+ title=effective_app_name,
3908
3942
  current_schema=rebound_schema,
3909
3943
  fields=rebound_fields,
3910
3944
  layout=rebound_layout,
@@ -3948,11 +3982,15 @@ class AiBuilderFacade:
3948
3982
  "fields_verified": False,
3949
3983
  "package_attached": None,
3950
3984
  "app_visuals_verified": True,
3985
+ "app_base_verified": True,
3951
3986
  "relation_field_limit_verified": relation_limit_verified,
3952
3987
  "relation_target_metadata_verified": relation_target_metadata_verified,
3953
3988
  },
3954
3989
  "app_key": target.app_key,
3955
3990
  "app_icon": str(visual_result.get("app_icon") or resolved.get("app_icon") or "").strip() or None,
3991
+ "app_name_before": str(visual_result.get("app_name_before") or target.app_name),
3992
+ "app_name_after": effective_app_name,
3993
+ "app_base_updated": bool(visual_result.get("updated")),
3956
3994
  "created": bool(resolved.get("created")),
3957
3995
  "field_diff": {
3958
3996
  "added": added,
@@ -3999,16 +4037,23 @@ class AiBuilderFacade:
3999
4037
  base_info = self.apps.app_get_base(profile=profile, app_key=target.app_key, include_raw=True).get("result") or {}
4000
4038
  tag_ids_after = _coerce_int_list(base_info.get("tagIds"))
4001
4039
  package_attached = None if package_tag_id is None else package_tag_id in tag_ids_after
4040
+ actual_app_name = str(base_info.get("formTitle") or effective_app_name).strip() or effective_app_name
4041
+ actual_app_icon = str(base_info.get("appIcon") or visual_result.get("app_icon") or "").strip() or None
4042
+ expected_app_icon = str(visual_result.get("app_icon") or "").strip() or None
4043
+ app_base_verified = actual_app_name == effective_app_name and actual_app_icon == expected_app_icon
4002
4044
  except (QingflowApiError, RuntimeError) as error:
4003
4045
  base_error = _coerce_api_error(error)
4004
4046
  if verification_error is None:
4005
4047
  verification_error = base_error
4006
4048
  tag_ids_after = []
4007
4049
  package_attached = None if package_tag_id is None else False
4050
+ app_base_verified = False
4008
4051
  response["verification"]["fields_verified"] = verification_ok
4009
4052
  response["verification"]["package_attached"] = package_attached
4053
+ response["verification"]["app_visuals_verified"] = app_base_verified
4054
+ response["verification"]["app_base_verified"] = app_base_verified
4010
4055
  response["verification"]["relation_field_limit_verified"] = relation_limit_verified
4011
- response["verified"] = verification_ok
4056
+ response["verified"] = verification_ok and app_base_verified
4012
4057
  response["tag_ids_after"] = tag_ids_after
4013
4058
  response["package_attached"] = package_attached
4014
4059
  if package_attached is False:
@@ -4018,14 +4063,18 @@ class AiBuilderFacade:
4018
4063
  "profile": profile,
4019
4064
  "tag_id": package_tag_id,
4020
4065
  "app_key": target.app_key,
4021
- "app_title": app_name or target.app_name,
4066
+ "app_title": effective_app_name,
4022
4067
  },
4023
4068
  }
4024
4069
  publish_failed = bool(response.get("publish_requested")) and not bool(response.get("published"))
4025
- if verification_ok and package_attached is not False and not publish_failed:
4070
+ if verification_ok and app_base_verified and package_attached is not False and not publish_failed:
4026
4071
  response["status"] = "success"
4027
4072
  else:
4028
4073
  response["status"] = "partial_success"
4074
+ if not app_base_verified and verification_error is None:
4075
+ response["recoverable"] = True
4076
+ response["error_code"] = response.get("error_code") or "APP_BASE_READBACK_PENDING"
4077
+ response["message"] = f"{response.get('message') or 'apply succeeded'}; app base readback pending"
4029
4078
  if verification_error is not None:
4030
4079
  response["recoverable"] = True
4031
4080
  response["error_code"] = response.get("error_code") or (
@@ -6535,17 +6584,11 @@ class AiBuilderFacade:
6535
6584
  profile: str,
6536
6585
  app_key: str,
6537
6586
  fallback_title: str,
6587
+ app_name: str | None,
6538
6588
  icon: str | None,
6539
6589
  color: str | None,
6540
6590
  normalized_args: dict[str, Any],
6541
6591
  ) -> JSONObject:
6542
- if not icon and not color:
6543
- return {
6544
- "status": "success",
6545
- "updated": False,
6546
- "app_icon": None,
6547
- "request_id": None,
6548
- }
6549
6592
  try:
6550
6593
  base_result = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True)
6551
6594
  except (QingflowApiError, RuntimeError) as error:
@@ -6559,22 +6602,25 @@ class AiBuilderFacade:
6559
6602
  )
6560
6603
  raw_base = base_result.get("result") if isinstance(base_result.get("result"), dict) else {}
6561
6604
  effective_title = str(raw_base.get("formTitle") or fallback_title or "未命名应用").strip() or "未命名应用"
6605
+ desired_title = str(app_name or effective_title).strip() or effective_title
6562
6606
  existing_icon = str(raw_base.get("appIcon") or "").strip() or None
6563
6607
  desired_icon = encode_workspace_icon_with_defaults(
6564
6608
  icon=icon,
6565
6609
  color=color,
6566
- title=effective_title,
6610
+ title=desired_title,
6567
6611
  fallback_icon_name="template",
6568
6612
  existing_icon=existing_icon,
6569
6613
  )
6570
- if desired_icon == existing_icon:
6614
+ if desired_icon == existing_icon and desired_title == effective_title:
6571
6615
  return {
6572
6616
  "status": "success",
6573
6617
  "updated": False,
6574
- "app_icon": desired_icon,
6618
+ "app_icon": desired_icon or existing_icon,
6619
+ "app_name_before": effective_title,
6620
+ "app_name_after": desired_title,
6575
6621
  "request_id": None,
6576
6622
  }
6577
- payload = self._build_app_base_update_payload(raw_base=raw_base, form_title=effective_title, app_icon=desired_icon)
6623
+ payload = self._build_app_base_update_payload(raw_base=raw_base, form_title=desired_title, app_icon=desired_icon)
6578
6624
  if payload is None:
6579
6625
  return _failed(
6580
6626
  "APP_VISUAL_UPDATE_UNSUPPORTED",
@@ -6598,6 +6644,8 @@ class AiBuilderFacade:
6598
6644
  "status": "success",
6599
6645
  "updated": True,
6600
6646
  "app_icon": desired_icon,
6647
+ "app_name_before": effective_title,
6648
+ "app_name_after": desired_title,
6601
6649
  "request_id": update_result.get("request_id") if isinstance(update_result, dict) else None,
6602
6650
  }
6603
6651
 
@@ -8455,6 +8503,8 @@ def _hydrate_relation_field_configs(
8455
8503
  for field in resolved_fields:
8456
8504
  if not isinstance(field, dict) or field.get("type") != FieldType.relation.value:
8457
8505
  continue
8506
+ if not bool(field.get("_relation_config_explicit")) and isinstance(field.get("_reference_config_template"), dict):
8507
+ continue
8458
8508
  target_app_key = str(field.get("target_app_key") or "").strip()
8459
8509
  if not target_app_key:
8460
8510
  continue
@@ -8613,13 +8663,13 @@ def _parse_field(question: dict[str, Any], *, field_id_hint: str | None = None)
8613
8663
  field["relation_mode"] = _relation_mode_from_optional_data_num(reference.get("optionalDataNum"))
8614
8664
  refer_questions = reference.get("referQuestions") if isinstance(reference.get("referQuestions"), list) else []
8615
8665
  visible_fields: list[dict[str, Any]] = []
8616
- display_field_que_id = _coerce_positive_int(reference.get("referQueId"))
8666
+ display_field_que_id = _coerce_nonnegative_int(reference.get("referQueId"))
8617
8667
  display_field_name: str | None = None
8618
8668
  for item in refer_questions:
8619
8669
  if not isinstance(item, dict):
8620
8670
  continue
8621
8671
  selector = {
8622
- "que_id": _coerce_positive_int(item.get("queId")),
8672
+ "que_id": _coerce_nonnegative_int(item.get("queId")),
8623
8673
  "name": str(item.get("queTitle") or "").strip() or None,
8624
8674
  }
8625
8675
  visible_fields.append(selector)
@@ -8634,6 +8684,8 @@ def _parse_field(question: dict[str, Any], *, field_id_hint: str | None = None)
8634
8684
  }
8635
8685
  field["visible_fields"] = visible_fields
8636
8686
  field["field_name_show"] = bool(reference.get("fieldNameShow", True))
8687
+ field["_reference_config_template"] = deepcopy(reference)
8688
+ field["_relation_config_explicit"] = False
8637
8689
  if field_type == FieldType.department:
8638
8690
  department_scope = _normalize_department_scope_from_question(question)
8639
8691
  if department_scope is not None:
@@ -9293,7 +9345,7 @@ def _build_selector_map(fields: list[dict[str, Any]]) -> dict[str, int]:
9293
9345
  for index, field in enumerate(fields):
9294
9346
  field_id = str(field.get("field_id") or "")
9295
9347
  field_name = str(field.get("name") or "")
9296
- que_id = _coerce_positive_int(field.get("que_id"))
9348
+ que_id = _coerce_nonnegative_int(field.get("que_id"))
9297
9349
  if field_id:
9298
9350
  mapping[f"field_id:{field_id}"] = index
9299
9351
  if field_name:
@@ -9308,7 +9360,7 @@ def _resolve_selector(selector_map: dict[str, int], selector: FieldSelector) ->
9308
9360
  value = selector_map.get(f"field_id:{selector.field_id}")
9309
9361
  if value is not None:
9310
9362
  return value
9311
- if selector.que_id:
9363
+ if selector.que_id is not None:
9312
9364
  value = selector_map.get(f"que_id:{selector.que_id}")
9313
9365
  if value is not None:
9314
9366
  return value
@@ -9356,6 +9408,7 @@ def _field_patch_to_internal(patch: FieldPatch) -> dict[str, Any]:
9356
9408
  "que_id": None,
9357
9409
  "default_type": 1,
9358
9410
  "default_value": None,
9411
+ "_relation_config_explicit": patch.type == PublicFieldType.relation,
9359
9412
  }
9360
9413
 
9361
9414
 
@@ -9426,7 +9479,7 @@ def _field_selector_payload_equal(left: Any, right: Any) -> bool:
9426
9479
  return False
9427
9480
  return (
9428
9481
  str(left.get("field_id") or "") == str(right.get("field_id") or "")
9429
- and _coerce_positive_int(left.get("que_id")) == _coerce_positive_int(right.get("que_id"))
9482
+ and _coerce_nonnegative_int(left.get("que_id")) == _coerce_nonnegative_int(right.get("que_id"))
9430
9483
  and str(left.get("name") or "") == str(right.get("name") or "")
9431
9484
  )
9432
9485
 
@@ -9581,6 +9634,10 @@ def _code_block_binding_equal(left: Any, right: Any) -> bool:
9581
9634
 
9582
9635
  def _apply_field_mutation(field: dict[str, Any], mutation: Any) -> None:
9583
9636
  payload = mutation.model_dump(mode="json", exclude_none=True)
9637
+ relation_config_explicit = (
9638
+ payload.get("type") == FieldType.relation.value
9639
+ or any(key in payload for key in ("target_app_key", "display_field", "visible_fields", "relation_mode"))
9640
+ )
9584
9641
  if "name" in payload:
9585
9642
  field["name"] = payload["name"]
9586
9643
  if "type" in payload:
@@ -9623,6 +9680,11 @@ def _apply_field_mutation(field: dict[str, Any], mutation: Any) -> None:
9623
9680
  field["custom_button_text"] = payload["custom_button_text"]
9624
9681
  if "subfields" in payload:
9625
9682
  field["subfields"] = [_field_patch_to_internal(item) for item in payload["subfields"]]
9683
+ if relation_config_explicit:
9684
+ field["_relation_config_explicit"] = True
9685
+ elif payload.get("type") and payload.get("type") != FieldType.relation.value:
9686
+ field.pop("_relation_config_explicit", None)
9687
+ field.pop("_reference_config_template", None)
9626
9688
 
9627
9689
 
9628
9690
  def _resolve_field_selector_with_uniqueness(
@@ -9634,8 +9696,8 @@ def _resolve_field_selector_with_uniqueness(
9634
9696
  selector = FieldSelector.model_validate(selector_payload)
9635
9697
  if selector.field_id:
9636
9698
  matched = [field for field in fields if str(field.get("field_id") or "") == str(selector.field_id)]
9637
- elif selector.que_id:
9638
- matched = [field for field in fields if _coerce_positive_int(field.get("que_id")) == _coerce_positive_int(selector.que_id)]
9699
+ elif selector.que_id is not None:
9700
+ matched = [field for field in fields if _coerce_nonnegative_int(field.get("que_id")) == _coerce_nonnegative_int(selector.que_id)]
9639
9701
  elif selector.name:
9640
9702
  name = str(selector.name or "").strip()
9641
9703
  matched = [field for field in fields if str(field.get("name") or "").strip() == name]
@@ -11094,7 +11156,7 @@ def _field_to_question(field: dict[str, Any], *, temp_id: int) -> dict[str, Any]
11094
11156
  },
11095
11157
  temp_id,
11096
11158
  )
11097
- if field.get("que_id"):
11159
+ if _coerce_nonnegative_int(field.get("que_id")) is not None:
11098
11160
  question["queId"] = field["que_id"]
11099
11161
  else:
11100
11162
  question["queTempId"] = temp_id
@@ -11104,13 +11166,23 @@ def _field_to_question(field: dict[str, Any], *, temp_id: int) -> dict[str, Any]
11104
11166
  if "default_value" in field:
11105
11167
  question["queDefaultValue"] = field.get("default_value")
11106
11168
  if field.get("type") == FieldType.relation.value:
11107
- reference = question.get("referenceConfig") if isinstance(question.get("referenceConfig"), dict) else {}
11108
- reference["referAppKey"] = field.get("target_app_key")
11109
- reference["_targetEntityId"] = field.get("target_app_key")
11110
- if field.get("target_field_que_id"):
11111
- reference["referQueId"] = field.get("target_field_que_id")
11112
- reference["optionalDataNum"] = _relation_mode_to_optional_data_num(field.get("relation_mode"))
11113
- question["referenceConfig"] = reference
11169
+ preserved_reference = (
11170
+ deepcopy(field.get("_reference_config_template"))
11171
+ if not bool(field.get("_relation_config_explicit")) and isinstance(field.get("_reference_config_template"), dict)
11172
+ else None
11173
+ )
11174
+ if preserved_reference is not None:
11175
+ preserved_reference["referAppKey"] = field.get("target_app_key")
11176
+ preserved_reference["_targetEntityId"] = field.get("target_app_key")
11177
+ question["referenceConfig"] = preserved_reference
11178
+ else:
11179
+ reference = question.get("referenceConfig") if isinstance(question.get("referenceConfig"), dict) else {}
11180
+ reference["referAppKey"] = field.get("target_app_key")
11181
+ reference["_targetEntityId"] = field.get("target_app_key")
11182
+ if field.get("target_field_que_id") is not None:
11183
+ reference["referQueId"] = field.get("target_field_que_id")
11184
+ reference["optionalDataNum"] = _relation_mode_to_optional_data_num(field.get("relation_mode"))
11185
+ question["referenceConfig"] = reference
11114
11186
  if field.get("type") == FieldType.department.value:
11115
11187
  scope_type, scope_payload = _serialize_department_scope_for_question(field.get("department_scope"))
11116
11188
  question["deptSelectScopeType"] = scope_type
@@ -458,13 +458,13 @@ def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
458
458
  has_app_title = bool((args.app_title or "").strip())
459
459
  has_package_tag_id = args.package_tag_id is not None
460
460
  if has_app_key:
461
- if args.create_if_missing or has_app_name or has_package_tag_id or has_app_title:
461
+ if args.create_if_missing or has_package_tag_id:
462
462
  raise_config_error(
463
- "schema apply edit mode only accepts --app-key as the resource selector.",
464
- fix_hint="For existing apps, pass `--app-key` only. For create mode, use `--package-tag-id --app-name --create-if-missing`.",
463
+ "schema apply edit mode accepts --app-key and optional --app-name only.",
464
+ fix_hint="For existing apps, use `--app-key` and optionally `--app-name`. For create mode, use `--package-tag-id --app-name --create-if-missing`.",
465
465
  )
466
466
  else:
467
- if not args.create_if_missing or not has_package_tag_id or not has_app_name:
467
+ if not args.create_if_missing or not has_package_tag_id or not (has_app_name or has_app_title):
468
468
  raise_config_error(
469
469
  "schema apply create mode requires --package-tag-id, --app-name, and --create-if-missing.",
470
470
  fix_hint="Use `--app-key` for existing apps, or pass `--package-tag-id --app-name --create-if-missing` to create a new app.",
@@ -73,8 +73,10 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
73
73
 
74
74
  update = record_subparsers.add_parser("update", help="更新记录")
75
75
  update.add_argument("--app-key", required=True)
76
- update.add_argument("--record-id", required=True, type=int)
77
- update.add_argument("--fields-file", required=True)
76
+ update.add_argument("--record-id", type=int)
77
+ update.add_argument("--fields-file")
78
+ update.add_argument("--items-file")
79
+ update.add_argument("--dry-run", action=argparse.BooleanOptionalAction, default=False)
78
80
  update.add_argument("--verify-write", action=argparse.BooleanOptionalAction, default=True)
79
81
  update.set_defaults(handler=_handle_update, format_hint="")
80
82
 
@@ -242,6 +244,31 @@ def _handle_insert(args: argparse.Namespace, context: CliContext) -> dict:
242
244
 
243
245
 
244
246
  def _handle_update(args: argparse.Namespace, context: CliContext) -> dict:
247
+ if args.items_file:
248
+ if args.record_id is not None or args.fields_file:
249
+ raise_config_error(
250
+ "record update batch mode does not accept --record-id or --fields-file.",
251
+ fix_hint="Use `record update --app-key APP_KEY --items-file ITEMS.json [--dry-run]` for batch updates.",
252
+ )
253
+ return context.record.record_update_public(
254
+ profile=args.profile,
255
+ app_key=args.app_key,
256
+ record_id=None,
257
+ fields=None,
258
+ items=require_list_arg(args.items_file, option_name="--items-file"),
259
+ dry_run=bool(args.dry_run),
260
+ verify_write=bool(args.verify_write),
261
+ )
262
+ if args.dry_run:
263
+ raise_config_error(
264
+ "record update --dry-run currently requires --items-file.",
265
+ fix_hint="Use `record update --app-key APP_KEY --items-file ITEMS.json --dry-run` for batch preflight.",
266
+ )
267
+ if args.record_id is None or not args.fields_file:
268
+ raise_config_error(
269
+ "record update single mode requires --record-id and --fields-file.",
270
+ fix_hint="Use `record update --app-key APP_KEY --record-id RECORD_ID --fields-file FIELDS.json`.",
271
+ )
245
272
  return context.record.record_update_public(
246
273
  profile=args.profile,
247
274
  app_key=args.app_key,
@@ -6,6 +6,7 @@ import sys
6
6
  from typing import Any, Callable, TextIO
7
7
 
8
8
  from ..errors import QingflowApiError
9
+ from ..public_surface import cli_public_tool_spec_from_namespace
9
10
  from ..response_trim import resolve_cli_tool_name, trim_error_response, trim_public_response
10
11
  from .context import CliContext, build_cli_context
11
12
  from .formatters import emit_json_result, emit_text_result
@@ -49,6 +50,8 @@ def run(
49
50
  return 2
50
51
  context = context_factory()
51
52
  try:
53
+ if not bool(args.json):
54
+ _emit_cli_effective_context_notice(args, context, stream=err)
52
55
  result = handler(args, context)
53
56
  except RuntimeError as exc:
54
57
  payload = trim_error_response(_parse_error_payload(exc))
@@ -145,5 +148,31 @@ def _result_exit_code(result: dict[str, Any]) -> int:
145
148
  return 0
146
149
 
147
150
 
151
+ def _emit_cli_effective_context_notice(args: argparse.Namespace, context: CliContext, *, stream: TextIO) -> None:
152
+ spec = cli_public_tool_spec_from_namespace(args)
153
+ if spec is None or not spec.cli_show_effective_context:
154
+ return
155
+ sessions = getattr(context, "sessions", None)
156
+ if sessions is None or not hasattr(sessions, "get_profile"):
157
+ return
158
+ profile_name = str(getattr(args, "profile", "default") or "default")
159
+ try:
160
+ session_profile = sessions.get_profile(profile_name)
161
+ except Exception:
162
+ session_profile = None
163
+ workspace_id = getattr(session_profile, "selected_ws_id", None) if session_profile is not None else None
164
+ workspace_name = getattr(session_profile, "selected_ws_name", None) if session_profile is not None else None
165
+ if workspace_id is None:
166
+ workspace_label = "(not selected)"
167
+ elif workspace_name:
168
+ workspace_label = f"{workspace_name} ({workspace_id})"
169
+ else:
170
+ workspace_label = str(workspace_id)
171
+ lines = [f"Context: profile={profile_name} workspace={workspace_label}"]
172
+ if spec.cli_context_write and profile_name == "default":
173
+ lines.append("Warning: using default profile for a workspace-sensitive write command")
174
+ stream.write("\n".join(lines) + "\n")
175
+
176
+
148
177
  if __name__ == "__main__":
149
178
  main()