@josephyan/qingflow-app-builder-mcp 0.2.0-beta.986 → 0.2.0-beta.988
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 +2 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +2 -0
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/builder_facade/service.py +432 -18
- package/src/qingflow_mcp/cli/commands/workspace.py +11 -0
- package/src/qingflow_mcp/cli/formatters.py +19 -1
- package/src/qingflow_mcp/public_surface.py +2 -0
- package/src/qingflow_mcp/server_app_builder.py +10 -0
- package/src/qingflow_mcp/server_app_user.py +10 -0
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +2 -2
- package/src/qingflow_mcp/solution/executor.py +2 -2
- package/src/qingflow_mcp/tools/auth_tools.py +50 -2
- package/src/qingflow_mcp/tools/workspace_tools.py +94 -0
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @josephyan/qingflow-app-builder-mcp@0.2.0-beta.
|
|
6
|
+
npm install @josephyan/qingflow-app-builder-mcp@0.2.0-beta.988
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.
|
|
12
|
+
npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.988 qingflow-app-builder-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -40,6 +40,7 @@ Default modeling rules:
|
|
|
40
40
|
- Directory: `member_search`, `role_search`, `role_create`
|
|
41
41
|
- Writes: `app_schema_apply`, `app_layout_apply`, `app_flow_apply`, `app_views_apply`, `app_charts_apply`, `portal_apply`, `app_release_edit_lock_if_mine`
|
|
42
42
|
- Verification: `app_publish_verify`
|
|
43
|
+
- Cross-cutting escalation: `feedback_submit` after explicit user confirmation when the public builder surface still cannot satisfy the user's need
|
|
43
44
|
|
|
44
45
|
Treat these as the official surface. Do not default to `package_create`, `package_attach_app`, raw `portal_*` writes, or raw `qingbi_report_*` writes.
|
|
45
46
|
|
|
@@ -102,6 +103,7 @@ Treat these as the official surface. Do not default to `package_create`, `packag
|
|
|
102
103
|
- For workflow assignees, prefer `role_search` over explicit members unless the user explicitly wants named members.
|
|
103
104
|
- Public flow building is still intentionally limited to stable linear workflows. If a requirement sounds like branches/conditions, explain the limitation instead of freehanding unsupported graph shapes.
|
|
104
105
|
- Respect collaborative edit locks. Only use `app_release_edit_lock_if_mine` when the lock owner is the current authenticated user.
|
|
106
|
+
- If the supported builder surface is still awkward or blocked after reasonable use, summarize the gap, ask whether to submit feedback, and call `feedback_submit` only after explicit user confirmation.
|
|
105
107
|
|
|
106
108
|
## Response Interpretation
|
|
107
109
|
|
|
@@ -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":
|
|
10104
|
+
"que_id": que_id,
|
|
10082
10105
|
"name": str(item.get("queTitle") or "").strip() or None,
|
|
10083
10106
|
}
|
|
10084
|
-
|
|
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,379 @@ 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
|
+
relation_config_explicit = bool(field.get("_relation_config_explicit"))
|
|
13458
|
+
normalized_source_questions = [
|
|
13459
|
+
item
|
|
13460
|
+
for item in (
|
|
13461
|
+
_normalize_reference_question_for_save(raw_item, ordinal=index)
|
|
13462
|
+
for index, raw_item in enumerate(cast(list[Any], source.get("referQuestions") or []), start=1)
|
|
13463
|
+
)
|
|
13464
|
+
if item is not None
|
|
13465
|
+
]
|
|
13466
|
+
if not relation_config_explicit:
|
|
13467
|
+
return normalized_source_questions
|
|
13468
|
+
|
|
13469
|
+
display_field = field.get("display_field") if isinstance(field.get("display_field"), dict) else None
|
|
13470
|
+
visible_fields = [item for item in cast(list[Any], field.get("visible_fields") or []) if isinstance(item, dict)]
|
|
13471
|
+
ordered_visible_selectors: list[dict[str, Any]] = []
|
|
13472
|
+
if display_field is not None:
|
|
13473
|
+
ordered_visible_selectors.append(display_field)
|
|
13474
|
+
for item in visible_fields:
|
|
13475
|
+
if any(_relation_target_field_matches(existing, item) for existing in ordered_visible_selectors):
|
|
13476
|
+
continue
|
|
13477
|
+
ordered_visible_selectors.append(item)
|
|
13478
|
+
|
|
13479
|
+
if not ordered_visible_selectors:
|
|
13480
|
+
return normalized_source_questions
|
|
13481
|
+
|
|
13482
|
+
canonical_questions: list[dict[str, Any]] = []
|
|
13483
|
+
used_source_indexes: set[int] = set()
|
|
13484
|
+
|
|
13485
|
+
for ordinal, selector in enumerate(ordered_visible_selectors, start=1):
|
|
13486
|
+
matched_index: int | None = None
|
|
13487
|
+
matched_item: dict[str, Any] | None = None
|
|
13488
|
+
for index, item in enumerate(normalized_source_questions):
|
|
13489
|
+
if index in used_source_indexes:
|
|
13490
|
+
continue
|
|
13491
|
+
if _reference_question_matches_visible_selector(item, selector):
|
|
13492
|
+
matched_index = index
|
|
13493
|
+
matched_item = deepcopy(item)
|
|
13494
|
+
break
|
|
13495
|
+
if matched_item is None:
|
|
13496
|
+
matched_item = _build_reference_question_from_visible_selector(selector, ordinal=ordinal)
|
|
13497
|
+
if matched_item is None:
|
|
13498
|
+
continue
|
|
13499
|
+
matched_item["ordinal"] = ordinal
|
|
13500
|
+
canonical_questions.append(matched_item)
|
|
13501
|
+
if matched_index is not None:
|
|
13502
|
+
used_source_indexes.add(matched_index)
|
|
13503
|
+
|
|
13504
|
+
source_target_app_key = str(source.get("referAppKey") or "").strip()
|
|
13505
|
+
target_app_key = str(field.get("target_app_key") or "").strip()
|
|
13506
|
+
preserve_remaining_source_questions = not source_target_app_key or source_target_app_key == target_app_key
|
|
13507
|
+
|
|
13508
|
+
if preserve_remaining_source_questions:
|
|
13509
|
+
next_ordinal = len(canonical_questions) + 1
|
|
13510
|
+
for index, item in enumerate(normalized_source_questions):
|
|
13511
|
+
if index in used_source_indexes:
|
|
13512
|
+
continue
|
|
13513
|
+
remaining_item = deepcopy(item)
|
|
13514
|
+
remaining_item["ordinal"] = next_ordinal
|
|
13515
|
+
next_ordinal += 1
|
|
13516
|
+
canonical_questions.append(remaining_item)
|
|
13517
|
+
|
|
13518
|
+
return canonical_questions
|
|
13519
|
+
|
|
13520
|
+
|
|
13521
|
+
def _canonicalize_reference_auth_questions_for_save(
|
|
13522
|
+
*,
|
|
13523
|
+
source: dict[str, Any],
|
|
13524
|
+
refer_questions: list[dict[str, Any]],
|
|
13525
|
+
relation_config_explicit: bool,
|
|
13526
|
+
) -> list[dict[str, Any]]:
|
|
13527
|
+
source_auth_questions = [
|
|
13528
|
+
item
|
|
13529
|
+
for item in (
|
|
13530
|
+
_normalize_reference_auth_question_for_save(raw_item)
|
|
13531
|
+
for raw_item in cast(list[Any], source.get("referAuthQues") or [])
|
|
13532
|
+
)
|
|
13533
|
+
if item is not None
|
|
13534
|
+
]
|
|
13535
|
+
source_auth_by_que_id: dict[int, dict[str, Any]] = {}
|
|
13536
|
+
for item in source_auth_questions:
|
|
13537
|
+
que_id = _coerce_any_int(item.get("queId"))
|
|
13538
|
+
if que_id is None or que_id in source_auth_by_que_id:
|
|
13539
|
+
continue
|
|
13540
|
+
source_auth_by_que_id[que_id] = item
|
|
13541
|
+
|
|
13542
|
+
if not relation_config_explicit:
|
|
13543
|
+
auth_questions: list[dict[str, Any]] = []
|
|
13544
|
+
seen_que_ids: set[int] = set()
|
|
13545
|
+
refer_question_auth_by_que_id: dict[int, int] = {}
|
|
13546
|
+
for item in refer_questions:
|
|
13547
|
+
que_id = _coerce_any_int(item.get("queId"))
|
|
13548
|
+
que_auth = _coerce_nonnegative_int(item.get("queAuth"))
|
|
13549
|
+
if que_id is None or que_auth is None or que_id in refer_question_auth_by_que_id:
|
|
13550
|
+
continue
|
|
13551
|
+
refer_question_auth_by_que_id[que_id] = que_auth
|
|
13552
|
+
|
|
13553
|
+
for item in source_auth_questions:
|
|
13554
|
+
que_id = _coerce_any_int(item.get("queId"))
|
|
13555
|
+
if que_id is None or que_id in seen_que_ids:
|
|
13556
|
+
continue
|
|
13557
|
+
payload = deepcopy(item)
|
|
13558
|
+
if que_id in refer_question_auth_by_que_id:
|
|
13559
|
+
payload["queAuth"] = refer_question_auth_by_que_id[que_id]
|
|
13560
|
+
auth_questions.append(payload)
|
|
13561
|
+
seen_que_ids.add(que_id)
|
|
13562
|
+
|
|
13563
|
+
for item in refer_questions:
|
|
13564
|
+
que_id = _coerce_any_int(item.get("queId"))
|
|
13565
|
+
que_auth = _coerce_nonnegative_int(item.get("queAuth"))
|
|
13566
|
+
if que_id is None or que_auth is None or que_id in seen_que_ids:
|
|
13567
|
+
continue
|
|
13568
|
+
payload = deepcopy(source_auth_by_que_id.get(que_id) or {"queId": que_id})
|
|
13569
|
+
payload["queId"] = que_id
|
|
13570
|
+
payload["queAuth"] = que_auth
|
|
13571
|
+
auth_questions.append(payload)
|
|
13572
|
+
seen_que_ids.add(que_id)
|
|
13573
|
+
|
|
13574
|
+
return _dedupe_reference_auth_questions(auth_questions)
|
|
13575
|
+
|
|
13576
|
+
auth_questions: list[dict[str, Any]] = []
|
|
13577
|
+
for item in refer_questions:
|
|
13578
|
+
que_id = _coerce_any_int(item.get("queId"))
|
|
13579
|
+
que_auth = _coerce_nonnegative_int(item.get("queAuth"))
|
|
13580
|
+
if que_id is None or que_auth is None:
|
|
13581
|
+
continue
|
|
13582
|
+
payload = deepcopy(source_auth_by_que_id.get(que_id) or {"queId": que_id})
|
|
13583
|
+
payload["queId"] = que_id
|
|
13584
|
+
payload["queAuth"] = que_auth
|
|
13585
|
+
auth_questions.append(payload)
|
|
13586
|
+
return _dedupe_reference_auth_questions(auth_questions)
|
|
13587
|
+
|
|
13588
|
+
|
|
13589
|
+
def _enforce_reference_config_consistency_for_save(
|
|
13590
|
+
payload: dict[str, Any],
|
|
13591
|
+
*,
|
|
13592
|
+
field: dict[str, Any],
|
|
13593
|
+
) -> dict[str, Any]:
|
|
13594
|
+
relation_config_explicit = bool(field.get("_relation_config_explicit"))
|
|
13595
|
+
refer_questions = [
|
|
13596
|
+
item
|
|
13597
|
+
for item in (
|
|
13598
|
+
_normalize_reference_question_for_save(raw_item, ordinal=index)
|
|
13599
|
+
for index, raw_item in enumerate(cast(list[Any], payload.get("referQuestions") or []), start=1)
|
|
13600
|
+
)
|
|
13601
|
+
if item is not None
|
|
13602
|
+
]
|
|
13603
|
+
if not refer_questions:
|
|
13604
|
+
return payload
|
|
13605
|
+
|
|
13606
|
+
refer_auth_ques = _dedupe_reference_auth_questions(
|
|
13607
|
+
[
|
|
13608
|
+
item
|
|
13609
|
+
for item in (
|
|
13610
|
+
_normalize_reference_auth_question_for_save(raw_item)
|
|
13611
|
+
for raw_item in cast(list[Any], payload.get("referAuthQues") or [])
|
|
13612
|
+
)
|
|
13613
|
+
if item is not None
|
|
13614
|
+
]
|
|
13615
|
+
)
|
|
13616
|
+
refer_auth_by_que_id: dict[int, int] = {}
|
|
13617
|
+
for item in refer_auth_ques:
|
|
13618
|
+
que_id = _coerce_any_int(item.get("queId"))
|
|
13619
|
+
que_auth = _coerce_nonnegative_int(item.get("queAuth"))
|
|
13620
|
+
if que_id is None or que_auth is None or que_id in refer_auth_by_que_id:
|
|
13621
|
+
continue
|
|
13622
|
+
refer_auth_by_que_id[que_id] = que_auth
|
|
13623
|
+
|
|
13624
|
+
display_field_que_id = _coerce_any_int(payload.get("referQueId"))
|
|
13625
|
+
if display_field_que_id is None:
|
|
13626
|
+
display_field_que_id = _coerce_any_int(field.get("target_field_que_id"))
|
|
13627
|
+
if display_field_que_id is not None:
|
|
13628
|
+
payload["referQueId"] = display_field_que_id
|
|
13629
|
+
|
|
13630
|
+
if relation_config_explicit:
|
|
13631
|
+
if display_field_que_id is not None and not any(
|
|
13632
|
+
_coerce_any_int(item.get("queId")) == display_field_que_id for item in refer_questions
|
|
13633
|
+
):
|
|
13634
|
+
display_selector = field.get("display_field") if isinstance(field.get("display_field"), dict) else None
|
|
13635
|
+
display_question = (
|
|
13636
|
+
_build_reference_question_from_visible_selector(display_selector, ordinal=1)
|
|
13637
|
+
if display_selector is not None
|
|
13638
|
+
else None
|
|
13639
|
+
)
|
|
13640
|
+
if display_question is not None:
|
|
13641
|
+
display_question["queId"] = display_field_que_id
|
|
13642
|
+
display_question["queAuth"] = _REFERENCE_FIELD_VISIBLE_AUTH
|
|
13643
|
+
refer_questions = [display_question, *refer_questions]
|
|
13644
|
+
|
|
13645
|
+
if display_field_que_id is not None:
|
|
13646
|
+
display_questions = [
|
|
13647
|
+
item for item in refer_questions if _coerce_any_int(item.get("queId")) == display_field_que_id
|
|
13648
|
+
]
|
|
13649
|
+
trailing_questions = [
|
|
13650
|
+
item for item in refer_questions if _coerce_any_int(item.get("queId")) != display_field_que_id
|
|
13651
|
+
]
|
|
13652
|
+
refer_questions = [*display_questions, *trailing_questions]
|
|
13653
|
+
|
|
13654
|
+
for ordinal, item in enumerate(refer_questions, start=1):
|
|
13655
|
+
que_id = _coerce_any_int(item.get("queId"))
|
|
13656
|
+
if que_id is None:
|
|
13657
|
+
continue
|
|
13658
|
+
item["ordinal"] = ordinal
|
|
13659
|
+
item["queAuth"] = refer_auth_by_que_id.get(
|
|
13660
|
+
que_id,
|
|
13661
|
+
_coerce_nonnegative_int(item.get("queAuth")) or _REFERENCE_FIELD_VISIBLE_AUTH,
|
|
13662
|
+
)
|
|
13663
|
+
if display_field_que_id is not None and que_id == display_field_que_id:
|
|
13664
|
+
item["queAuth"] = _REFERENCE_FIELD_VISIBLE_AUTH
|
|
13665
|
+
|
|
13666
|
+
payload["referQuestions"] = refer_questions
|
|
13667
|
+
payload["referAuthQues"] = _canonicalize_reference_auth_questions_for_save(
|
|
13668
|
+
source={"referAuthQues": refer_auth_ques},
|
|
13669
|
+
refer_questions=refer_questions,
|
|
13670
|
+
relation_config_explicit=relation_config_explicit,
|
|
13671
|
+
)
|
|
13672
|
+
return payload
|
|
13673
|
+
|
|
13674
|
+
|
|
13278
13675
|
def _normalize_reference_config_for_save(
|
|
13279
13676
|
reference: Any,
|
|
13280
13677
|
*,
|
|
@@ -13289,11 +13686,13 @@ def _normalize_reference_config_for_save(
|
|
|
13289
13686
|
if field.get("field_name_show") is not None:
|
|
13290
13687
|
payload["fieldNameShow"] = bool(field.get("field_name_show"))
|
|
13291
13688
|
|
|
13292
|
-
|
|
13293
|
-
|
|
13294
|
-
|
|
13295
|
-
|
|
13296
|
-
|
|
13689
|
+
refer_question_auth_overrides = _reference_question_auth_overrides_for_save(source=source, field=field)
|
|
13690
|
+
refer_questions = _canonicalize_reference_questions_for_save(source=source, field=field)
|
|
13691
|
+
for index, normalized_item in enumerate(refer_questions, start=1):
|
|
13692
|
+
que_id = _coerce_any_int(normalized_item.get("queId"))
|
|
13693
|
+
if que_id is not None and que_id in refer_question_auth_overrides:
|
|
13694
|
+
normalized_item["queAuth"] = refer_question_auth_overrides[que_id]
|
|
13695
|
+
normalized_item["ordinal"] = index
|
|
13297
13696
|
if refer_questions or "referQuestions" in source:
|
|
13298
13697
|
payload["referQuestions"] = refer_questions
|
|
13299
13698
|
|
|
@@ -13308,18 +13707,17 @@ def _normalize_reference_config_for_save(
|
|
|
13308
13707
|
if refer_fill_rules or "referFillRules" in source:
|
|
13309
13708
|
payload["referFillRules"] = refer_fill_rules
|
|
13310
13709
|
|
|
13311
|
-
refer_auth_ques =
|
|
13312
|
-
|
|
13313
|
-
|
|
13314
|
-
|
|
13315
|
-
|
|
13316
|
-
|
|
13317
|
-
|
|
13318
|
-
]
|
|
13710
|
+
refer_auth_ques = _canonicalize_reference_auth_questions_for_save(
|
|
13711
|
+
source=source,
|
|
13712
|
+
refer_questions=refer_questions,
|
|
13713
|
+
relation_config_explicit=bool(field.get("_relation_config_explicit")),
|
|
13714
|
+
)
|
|
13715
|
+
if not refer_auth_ques:
|
|
13716
|
+
refer_auth_ques = _synthesize_reference_auth_questions_for_save(source=source, field=field)
|
|
13319
13717
|
if refer_auth_ques or "referAuthQues" in source:
|
|
13320
13718
|
payload["referAuthQues"] = refer_auth_ques
|
|
13321
13719
|
|
|
13322
|
-
return payload
|
|
13720
|
+
return _enforce_reference_config_consistency_for_save(payload, field=field)
|
|
13323
13721
|
|
|
13324
13722
|
|
|
13325
13723
|
def _normalize_relation_question_for_save(question: dict[str, Any], *, field: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -13550,9 +13948,16 @@ def _field_to_question(field: dict[str, Any], *, temp_id: int) -> dict[str, Any]
|
|
|
13550
13948
|
preserved_reference["referAppKey"] = field.get("target_app_key")
|
|
13551
13949
|
question["referenceConfig"] = preserved_reference
|
|
13552
13950
|
else:
|
|
13951
|
+
existing_reference = (
|
|
13952
|
+
deepcopy(relation_question_template.get("referenceConfig"))
|
|
13953
|
+
if relation_question_template is not None and isinstance(relation_question_template.get("referenceConfig"), dict)
|
|
13954
|
+
else deepcopy(question.get("referenceConfig"))
|
|
13955
|
+
if isinstance(question.get("referenceConfig"), dict)
|
|
13956
|
+
else {}
|
|
13957
|
+
)
|
|
13553
13958
|
reference = (
|
|
13554
|
-
|
|
13555
|
-
if relation_config_explicit
|
|
13959
|
+
existing_reference
|
|
13960
|
+
if relation_config_explicit
|
|
13556
13961
|
else deepcopy(question.get("referenceConfig"))
|
|
13557
13962
|
if isinstance(question.get("referenceConfig"), dict)
|
|
13558
13963
|
else {}
|
|
@@ -13562,6 +13967,13 @@ def _field_to_question(field: dict[str, Any], *, temp_id: int) -> dict[str, Any]
|
|
|
13562
13967
|
if isinstance(built_question.get("referenceConfig"), dict)
|
|
13563
13968
|
else {}
|
|
13564
13969
|
)
|
|
13970
|
+
original_target_app_key = str(existing_reference.get("referAppKey") or "").strip()
|
|
13971
|
+
next_target_app_key = str(field.get("target_app_key") or "").strip()
|
|
13972
|
+
preserve_existing_reference_questions = (
|
|
13973
|
+
relation_config_explicit
|
|
13974
|
+
and bool(original_target_app_key)
|
|
13975
|
+
and original_target_app_key == next_target_app_key
|
|
13976
|
+
)
|
|
13565
13977
|
if relation_config_explicit:
|
|
13566
13978
|
for stale_key in ("customButtonText", "customAdvancedSetting", "configShowForm", "dataShowForm"):
|
|
13567
13979
|
reference.pop(stale_key, None)
|
|
@@ -13573,6 +13985,8 @@ def _field_to_question(field: dict[str, Any], *, temp_id: int) -> dict[str, Any]
|
|
|
13573
13985
|
"fieldNameShow",
|
|
13574
13986
|
"_targetFieldId",
|
|
13575
13987
|
):
|
|
13988
|
+
if preserve_existing_reference_questions and key in {"referQuestions", "referAuthQues"}:
|
|
13989
|
+
continue
|
|
13576
13990
|
if key in built_reference:
|
|
13577
13991
|
reference[key] = deepcopy(built_reference[key])
|
|
13578
13992
|
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),
|
|
@@ -93,6 +93,16 @@ def build_builder_server() -> FastMCP:
|
|
|
93
93
|
include_external=include_external,
|
|
94
94
|
)
|
|
95
95
|
|
|
96
|
+
@server.tool()
|
|
97
|
+
def workspace_get(
|
|
98
|
+
profile: str = DEFAULT_PROFILE,
|
|
99
|
+
ws_id: int | None = None,
|
|
100
|
+
) -> dict:
|
|
101
|
+
return workspace.workspace_get(
|
|
102
|
+
profile=profile,
|
|
103
|
+
ws_id=ws_id,
|
|
104
|
+
)
|
|
105
|
+
|
|
96
106
|
@server.tool()
|
|
97
107
|
def file_upload_local(
|
|
98
108
|
profile: str = DEFAULT_PROFILE,
|
|
@@ -228,6 +228,16 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
228
228
|
include_external=include_external,
|
|
229
229
|
)
|
|
230
230
|
|
|
231
|
+
@server.tool()
|
|
232
|
+
def workspace_get(
|
|
233
|
+
profile: str = DEFAULT_PROFILE,
|
|
234
|
+
ws_id: int | None = None,
|
|
235
|
+
) -> dict:
|
|
236
|
+
return workspace.workspace_get(
|
|
237
|
+
profile=profile,
|
|
238
|
+
ws_id=ws_id,
|
|
239
|
+
)
|
|
240
|
+
|
|
231
241
|
@server.tool()
|
|
232
242
|
def app_list(profile: str = DEFAULT_PROFILE) -> dict:
|
|
233
243
|
return apps.app_list(profile=profile)
|
|
@@ -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":
|
|
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":
|
|
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",
|
|
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":
|
|
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":
|
|
228
|
-
"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
|