@qingflow-tech/qingflow-cli 1.1.41 → 1.1.43-beta.1

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.
Files changed (51) hide show
  1. package/README.md +4 -4
  2. package/package.json +2 -1
  3. package/pyproject.toml +3 -2
  4. package/skills/qingflow-cli/reference/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md +1 -1
  5. package/skills/qingflow-cli/reference/task/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md +1 -1
  6. package/src/qingflow_mcp/app_form_compat.py +115 -0
  7. package/src/qingflow_mcp/app_form_validator.py +199 -0
  8. package/src/qingflow_mcp/builder_facade/app_form.py +2836 -0
  9. package/src/qingflow_mcp/builder_facade/app_form_schema.py +269 -0
  10. package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +37 -0
  11. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +1766 -0
  12. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +76 -0
  13. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +76 -0
  14. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +76 -0
  15. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +223 -0
  16. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +538 -0
  17. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +76 -0
  18. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +76 -0
  19. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +149 -0
  20. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +76 -0
  21. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +76 -0
  22. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +76 -0
  23. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +76 -0
  24. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +92 -0
  25. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +76 -0
  26. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +76 -0
  27. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +338 -0
  28. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +137 -0
  29. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +92 -0
  30. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +92 -0
  31. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +810 -0
  32. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +76 -0
  33. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +713 -0
  34. package/src/qingflow_mcp/builder_facade/service.py +5 -2
  35. package/src/qingflow_mcp/cli/commands/builder.py +91 -28
  36. package/src/qingflow_mcp/cli/json_io.py +21 -2
  37. package/src/qingflow_mcp/cli/main.py +3 -39
  38. package/src/qingflow_mcp/cloud_auth.py +11 -0
  39. package/src/qingflow_mcp/cloud_context.py +5 -0
  40. package/src/qingflow_mcp/cloud_oauth_proxy.py +453 -0
  41. package/src/qingflow_mcp/cloud_redis.py +25 -0
  42. package/src/qingflow_mcp/config.py +44 -0
  43. package/src/qingflow_mcp/public_surface.py +5 -2
  44. package/src/qingflow_mcp/response_trim.py +8 -0
  45. package/src/qingflow_mcp/server_app_builder.py +38 -86
  46. package/src/qingflow_mcp/server_app_user.py +2 -1
  47. package/src/qingflow_mcp/server_cloud.py +245 -22
  48. package/src/qingflow_mcp/tools/ai_builder_tools.py +93 -100
  49. package/src/qingflow_mcp/tools/app_tools.py +46 -65
  50. package/src/qingflow_mcp/tools/base.py +2 -1
  51. package/src/qingflow_mcp/tools/task_context_tools.py +6 -0
@@ -9191,6 +9191,7 @@ class AiBuilderFacade:
9191
9191
  remove_fields: list[FieldRemovePatch],
9192
9192
  inline_form_layout_sections: list[LayoutSectionPatch] | None = None,
9193
9193
  ) -> JSONObject:
9194
+ # TODO(deprecation): Remove after all internal callers use the AppForm reconciler.
9194
9195
  apply_started_at = time.perf_counter()
9195
9196
  schema_write_ms: int | None = None
9196
9197
  inline_layout_ms: int | None = None
@@ -10182,6 +10183,7 @@ class AiBuilderFacade:
10182
10183
  sections: list[LayoutSectionPatch],
10183
10184
  publish: bool = True,
10184
10185
  ) -> JSONObject:
10186
+ # TODO(deprecation): Remove after all internal callers use the AppForm reconciler.
10185
10187
  requested_sections = [section.model_dump(mode="json", exclude_none=True) for section in sections]
10186
10188
  normalized_args = {
10187
10189
  "app_key": app_key,
@@ -22236,7 +22238,7 @@ def _compile_code_block_binding_fields(
22236
22238
  alias_name = str(output_item.get("alias") or "").strip()
22237
22239
  alias_path = str(output_item.get("path") or "").strip()
22238
22240
  existing_alias = existing_alias_by_name.get(alias_name) or {}
22239
- alias_id = _coerce_positive_int(existing_alias.get("alias_id"))
22241
+ alias_id = _coerce_positive_int(output_item.get("alias_id")) or _coerce_positive_int(existing_alias.get("alias_id"))
22240
22242
  compiled_outputs.append(
22241
22243
  {
22242
22244
  "alias_name": alias_name,
@@ -22649,7 +22651,8 @@ def _compile_q_linker_binding_fields(
22649
22651
  {
22650
22652
  "alias_name": alias_name,
22651
22653
  "alias_path": alias_path,
22652
- "alias_id": _coerce_positive_int((existing_alias_by_name.get(alias_name) or {}).get("alias_id")),
22654
+ "alias_id": _coerce_positive_int(output_item.get("alias_id"))
22655
+ or _coerce_positive_int((existing_alias_by_name.get(alias_name) or {}).get("alias_id")),
22653
22656
  "target_field": {
22654
22657
  "field_id": target_field.get("field_id"),
22655
22658
  "que_id": target_ref,
@@ -4,6 +4,7 @@ import argparse
4
4
  from copy import deepcopy
5
5
 
6
6
  from ..context import CliContext
7
+ from ...app_form_validator import AppFormValidationError, validate_declaration
7
8
  from ..json_io import load_json_value
8
9
  from .common import load_list_arg, load_object_arg, parse_bool_text, raise_config_error, require_list_arg
9
10
 
@@ -98,6 +99,29 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
98
99
  package_apply.add_argument("--config-file", required=True)
99
100
  package_apply.set_defaults(handler=_handle_package_apply, format_hint="builder_summary", force_json_output=True)
100
101
 
102
+ app_form = builder_subparsers.add_parser("app-form", help="声明式应用表单")
103
+ app_form_subparsers = app_form.add_subparsers(dest="builder_app_form_command", required=True)
104
+
105
+ app_form_schema = app_form_subparsers.add_parser("schema", help="读取 AppForm Schema")
106
+ app_form_schema.add_argument("--schema-version")
107
+ app_form_schema.add_argument("--field-type")
108
+ app_form_schema.set_defaults(handler=_handle_app_form_schema, format_hint="builder_summary", force_json_output=True)
109
+
110
+ app_form_get = app_form_subparsers.add_parser("get", help="读取 canonical AppForm")
111
+ app_form_get.add_argument("--app-key", required=True)
112
+ app_form_get.add_argument("--schema-version")
113
+ app_form_get.add_argument("--being-draft", action=argparse.BooleanOptionalAction, default=False)
114
+ app_form_get.set_defaults(handler=_handle_app_form_get, format_hint="builder_summary", force_json_output=True)
115
+
116
+ app_form_apply = app_form_subparsers.add_parser("apply", help="应用完整 AppForm 声明")
117
+ app_form_apply.add_argument("--file", required=True, help="AppForm JSON 文件,- 表示 stdin")
118
+ app_form_apply.set_defaults(handler=_handle_app_form_apply, format_hint="builder_summary", force_json_output=True)
119
+
120
+ app_form_validate = app_form_subparsers.add_parser("validate", help="只校验 AppForm 声明,不访问后端")
121
+ app_form_validate.add_argument("--file", required=True, help="AppForm JSON 文件,- 表示 stdin")
122
+ app_form_validate.add_argument("--schema-version", help="固定使用的 AppForm Schema 版本")
123
+ app_form_validate.set_defaults(handler=_handle_app_form_validate, format_hint="builder_summary", force_json_output=True)
124
+
101
125
  app = builder_subparsers.add_parser("app", help="应用")
102
126
  app_subparsers = app.add_subparsers(dest="builder_app_command", required=True)
103
127
 
@@ -113,6 +137,10 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
113
137
  app_release_lock.add_argument("--lock-owner-name", required=True)
114
138
  app_release_lock.set_defaults(handler=_handle_app_release_edit_lock_if_mine, format_hint="builder_summary", force_json_output=True)
115
139
 
140
+ app_delete = app_subparsers.add_parser("delete", help="删除应用")
141
+ app_delete.add_argument("--app-key", required=True)
142
+ app_delete.set_defaults(handler=_handle_app_delete, format_hint="builder_summary", force_json_output=True)
143
+
116
144
  app_get = app_subparsers.add_parser("get", help="读取应用配置(字段请使用: builder app get --app-key APP fields)")
117
145
  app_get.add_argument(
118
146
  "builder_app_get_section",
@@ -202,34 +230,6 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
202
230
  portal_delete.add_argument("--dash-key", required=True)
203
231
  portal_delete.set_defaults(handler=_handle_portal_delete, format_hint="builder_summary", force_json_output=True)
204
232
 
205
- schema_apply = builder_subparsers.add_parser("schema", help="字段搭建")
206
- schema_apply_subparsers = schema_apply.add_subparsers(dest="builder_schema_command", required=True)
207
- schema_apply_apply = schema_apply_subparsers.add_parser("apply", help="执行字段变更")
208
- schema_apply_apply.add_argument("--app-key", default="")
209
- schema_apply_apply.add_argument("--package-id", type=int)
210
- schema_apply_apply.add_argument("--app-name", default="")
211
- schema_apply_apply.add_argument("--app-title", default="")
212
- schema_apply_apply.add_argument("--icon")
213
- schema_apply_apply.add_argument("--color")
214
- schema_apply_apply.add_argument("--visibility-file")
215
- schema_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=None)
216
- schema_apply_apply.add_argument("--apps-file", help="多应用 schema JSON 对象;每项可带 client_key/app_name/form,支持 relation target_app_ref")
217
- schema_apply_apply.add_argument("--form-file", help="单应用表单结构 JSON 数组;form[].rows[][] 同时声明字段和表单分组布局")
218
- schema_apply_apply.add_argument("--add-fields-file", help="字段 JSON 数组;字段可用 as_data_title/as_data_cover 标记数据标题/封面")
219
- schema_apply_apply.add_argument("--update-fields-file", help="字段更新 JSON 数组;set 内可用 as_data_title/as_data_cover 标记数据标题/封面")
220
- schema_apply_apply.add_argument("--remove-fields-file")
221
- schema_apply_apply.set_defaults(handler=_handle_schema_apply, format_hint="builder_summary", force_json_output=True)
222
-
223
- layout_apply = builder_subparsers.add_parser("layout", help="布局")
224
- layout_apply_subparsers = layout_apply.add_subparsers(dest="builder_layout_command", required=True)
225
- layout_apply_apply = layout_apply_subparsers.add_parser("apply", help="执行布局变更")
226
- layout_apply_apply.add_argument("--app-key", default="")
227
- layout_apply_apply.add_argument("--apps-file", help="多应用布局 JSON 数组;每项包含 app_key/mode?/publish?/sections")
228
- layout_apply_apply.add_argument("--mode", choices=["merge", "replace"], default="merge")
229
- layout_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
230
- layout_apply_apply.add_argument("--sections-file")
231
- layout_apply_apply.set_defaults(handler=_handle_layout_apply, format_hint="builder_summary", force_json_output=True)
232
-
233
233
  views_apply = builder_subparsers.add_parser("views", help="视图")
234
234
  views_apply_subparsers = views_apply.add_subparsers(dest="builder_views_command", required=True)
235
235
  views_apply_apply = views_apply_subparsers.add_parser("apply", help="执行视图变更")
@@ -331,6 +331,67 @@ def _handle_contract(args: argparse.Namespace, context: CliContext) -> dict:
331
331
  return context.builder.builder_tool_contract(tool_name=args.tool_name)
332
332
 
333
333
 
334
+ def _handle_app_form_schema(args: argparse.Namespace, context: CliContext) -> dict:
335
+ return context.builder.app_form_schema(
336
+ schema_version=args.schema_version,
337
+ field_type=args.field_type,
338
+ )
339
+
340
+
341
+ def _handle_app_form_get(args: argparse.Namespace, context: CliContext) -> dict:
342
+ return context.builder.app_form_get(
343
+ profile=args.profile,
344
+ app_key=args.app_key,
345
+ schema_version=args.schema_version,
346
+ being_draft=bool(args.being_draft),
347
+ )
348
+
349
+
350
+ def _handle_app_form_apply(args: argparse.Namespace, context: CliContext) -> dict:
351
+ payload = load_json_value(
352
+ args.file,
353
+ option_name="--file",
354
+ max_bytes=2 * 1024 * 1024,
355
+ max_depth=64,
356
+ )
357
+ if not isinstance(payload, dict):
358
+ raise_config_error("builder app-form apply --file must contain a JSON object")
359
+ return context.builder.app_form_apply(profile=args.profile, spec=payload)
360
+
361
+
362
+ def _handle_app_form_validate(args: argparse.Namespace, _context: CliContext) -> dict:
363
+ payload = load_json_value(
364
+ args.file,
365
+ option_name="--file",
366
+ max_bytes=2 * 1024 * 1024,
367
+ max_depth=64,
368
+ )
369
+ if not isinstance(payload, dict):
370
+ raise_config_error("builder app-form validate --file must contain a JSON object")
371
+ try:
372
+ result = validate_declaration(payload, schema_version=args.schema_version)
373
+ except AppFormValidationError as error:
374
+ return {
375
+ "category": "validation",
376
+ "status": "failed",
377
+ "ok": False,
378
+ "error_code": "INVALID_SPEC",
379
+ "message": "AppForm declaration failed validation",
380
+ "details": {
381
+ "schemaVersion": error.schema_version,
382
+ "apiVersion": error.api_version,
383
+ "issues": error.issues,
384
+ },
385
+ }
386
+ except (ValueError, RuntimeError) as error:
387
+ raise_config_error(str(error), error_code="VALIDATION_ERROR")
388
+ return {"category": "validation", "status": "success", "ok": True, **result, "message": "AppForm declaration is valid"}
389
+
390
+
391
+ def _handle_app_delete(args: argparse.Namespace, context: CliContext) -> dict:
392
+ return context.builder.app_delete(profile=args.profile, app_key=args.app_key)
393
+
394
+
334
395
  def _handle_icon_catalog(args: argparse.Namespace, context: CliContext) -> dict:
335
396
  return context.builder.workspace_icon_catalog_get(profile=args.profile)
336
397
 
@@ -554,6 +615,8 @@ def _handle_chart_get(args: argparse.Namespace, context: CliContext) -> dict:
554
615
  return context.builder.chart_get(profile=args.profile, chart_id=args.chart_id)
555
616
 
556
617
 
618
+ # TODO(deprecation): The legacy schema/layout CLI routes are removed. Keep the implementation below only
619
+ # while internal migration and focused regression coverage still depend on the imperative adapters.
557
620
  def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
558
621
  apps_payload = _load_apps_file_arg(args.apps_file)
559
622
  apps = apps_payload["apps"]
@@ -8,7 +8,13 @@ from typing import Any
8
8
  from ..errors import QingflowApiError
9
9
 
10
10
 
11
- def load_json_value(path: str, *, option_name: str) -> Any:
11
+ def load_json_value(
12
+ path: str,
13
+ *,
14
+ option_name: str,
15
+ max_bytes: int | None = None,
16
+ max_depth: int | None = None,
17
+ ) -> Any:
12
18
  if not path:
13
19
  raise QingflowApiError.config_error(f"{option_name} is required")
14
20
  if path == "-":
@@ -18,10 +24,23 @@ def load_json_value(path: str, *, option_name: str) -> Any:
18
24
  raw = Path(path).expanduser().read_text(encoding="utf-8")
19
25
  except OSError as exc:
20
26
  raise QingflowApiError.config_error(f"failed to read {option_name} from '{path}': {exc}") from exc
27
+ if max_bytes is not None and len(raw.encode("utf-8")) > max_bytes:
28
+ raise QingflowApiError.config_error(f"{option_name} exceeds the {max_bytes}-byte limit")
21
29
  try:
22
- return json.loads(raw)
30
+ value = json.loads(raw)
23
31
  except json.JSONDecodeError as exc:
24
32
  raise QingflowApiError.config_error(f"{option_name} must contain valid JSON: {exc}") from exc
33
+ if max_depth is not None:
34
+ stack = [(value, 1)]
35
+ while stack:
36
+ item, depth = stack.pop()
37
+ if depth > max_depth:
38
+ raise QingflowApiError.config_error(f"{option_name} exceeds the maximum nesting depth {max_depth}")
39
+ if isinstance(item, dict):
40
+ stack.extend((child, depth + 1) for child in item.values())
41
+ elif isinstance(item, list):
42
+ stack.extend((child, depth + 1) for child in item)
43
+ return value
25
44
 
26
45
 
27
46
  def load_json_object(path: str, *, option_name: str) -> dict[str, Any]:
@@ -79,23 +79,6 @@ def run(
79
79
  return 2
80
80
  except SystemExit as exc:
81
81
  return int(exc.code or 0)
82
- try:
83
- _validate_conditional_args(args)
84
- except _CliArgumentError as exc:
85
- if _should_force_json_output_argv(normalized_argv):
86
- payload = {
87
- "category": "config",
88
- "status": "failed",
89
- "error_code": "ARGUMENT_ERROR",
90
- "message": exc.message,
91
- "details": {"usage": exc.usage.strip(), "prog": exc.prog},
92
- }
93
- payload = _maybe_attach_builder_apply_error_envelope_from_args(args, payload)
94
- emit_json_result(payload, stream=out)
95
- return 2
96
- err.write(exc.usage)
97
- err.write(f"{exc.prog}: error: {exc.message}\n")
98
- return 2
99
82
  setattr(args, "_stdin", sys.stdin)
100
83
  setattr(args, "_stdout_stream", out)
101
84
  setattr(args, "_stderr_stream", err)
@@ -161,21 +144,6 @@ def _emit_version(*, json_mode: bool, stdout: TextIO) -> int:
161
144
  return 0
162
145
 
163
146
 
164
- def _validate_conditional_args(args: argparse.Namespace) -> None:
165
- if (
166
- getattr(args, "command", "") in {"builder", "build"}
167
- and getattr(args, "builder_command", "") == "layout"
168
- and getattr(args, "builder_layout_command", "") == "apply"
169
- and not getattr(args, "apps_file", None)
170
- and not getattr(args, "sections_file", None)
171
- ):
172
- raise _CliArgumentError(
173
- prog="qingflow builder layout apply",
174
- message="--sections-file is required",
175
- usage="usage: qingflow builder layout apply --app-key APP_KEY --sections-file SECTIONS.json\n",
176
- )
177
-
178
-
179
147
  def _should_force_json_output_argv_for_version(argv: list[str]) -> bool:
180
148
  return "--json" in argv
181
149
 
@@ -231,8 +199,10 @@ def _should_force_json_output_argv(argv: list[str]) -> bool:
231
199
  return False
232
200
  section = tokens[1]
233
201
  action = tokens[2]
234
- if section in {"package", "button", "associated-resource", "associated-resources", "portal", "schema", "layout", "views", "flow", "charts"}:
202
+ if section in {"package", "button", "associated-resource", "associated-resources", "portal", "views", "flow", "charts"}:
235
203
  return action == "apply"
204
+ if section == "app-form":
205
+ return action in {"schema", "get", "validate", "apply"}
236
206
  if section == "publish":
237
207
  return action == "verify"
238
208
  if section == "app":
@@ -283,10 +253,6 @@ def _builder_apply_operation_from_args(args: argparse.Namespace) -> str | None:
283
253
  return "portal_apply"
284
254
  if section == "portal" and getattr(args, "builder_portal_command", "") == "delete":
285
255
  return "portal_delete"
286
- if section == "schema" and getattr(args, "builder_schema_command", "") == "apply":
287
- return "app_schema_apply"
288
- if section == "layout" and getattr(args, "builder_layout_command", "") == "apply":
289
- return "app_layout_apply"
290
256
  if section == "views" and getattr(args, "builder_views_command", "") == "apply":
291
257
  return "app_views_apply"
292
258
  if section == "flow" and getattr(args, "builder_flow_command", "") == "apply":
@@ -312,8 +278,6 @@ def _builder_apply_operation_from_argv(argv: list[str]) -> str | None:
312
278
  "associated-resource": "app_associated_resources_apply",
313
279
  "associated-resources": "app_associated_resources_apply",
314
280
  "portal": "portal_apply",
315
- "schema": "app_schema_apply",
316
- "layout": "app_layout_apply",
317
281
  "views": "app_views_apply",
318
282
  "flow": "app_flow_apply",
319
283
  "charts": "app_charts_apply",
@@ -25,6 +25,7 @@ from .json_types import JSONObject
25
25
  AUTH_CONTEXT_KEY_PREFIX = "mcp:auth:context"
26
26
  AUTH_CONTEXT_LOCK_KEY_PREFIX = "mcp:auth:context:lock"
27
27
  AUTH_CONTEXT_NEGATIVE_KEY_PREFIX = "mcp:auth:context:negative"
28
+ ACCESS_PLATFORM_KEY_PREFIX = "oauth2:access-platform:"
28
29
 
29
30
 
30
31
  @dataclass(slots=True)
@@ -103,6 +104,16 @@ class CloudAuthBroker:
103
104
  def invalidate(self, credential_hash: str) -> None:
104
105
  self.cache.delete(self._auth_key(credential_hash))
105
106
 
107
+ def resolve_call_platform(self, access_token: str) -> str:
108
+ normalized_token = self._normalize_text(access_token)
109
+ if normalized_token is None:
110
+ return ""
111
+ try:
112
+ value = self.cache.get_text(f"{ACCESS_PLATFORM_KEY_PREFIX}{normalized_token}")
113
+ except QingflowApiError:
114
+ return ""
115
+ return self._normalize_text(value) or ""
116
+
106
117
  @staticmethod
107
118
  def credential_hash(credential: str) -> str:
108
119
  return sha256(credential.encode("utf-8")).hexdigest()
@@ -68,3 +68,8 @@ current_cloud_context: ContextVar[CloudMcpContext | None] = ContextVar(
68
68
  "qingflow_mcp_current_cloud_context",
69
69
  default=None,
70
70
  )
71
+
72
+ current_call_platform: ContextVar[str] = ContextVar(
73
+ "qingflow_mcp_current_call_platform",
74
+ default="",
75
+ )