Complete reversible 1C saved-state prepare workflow
This commit is contained in:
@@ -308,18 +308,20 @@ dimension, or resource, also pass `member_ref` or `member_kind` +
|
||||
`member_name`; the adapter resolves the exact parent/member GUIDs and
|
||||
serialized tree path internally. The method supports `plan`, `apply`,
|
||||
`apply_and_verify`, and `apply_and_rollback`, requires explicit saved-state
|
||||
gates, and never writes active `Config`/`ConfigCAS`. Renaming an object and
|
||||
member, adding/removing collection items, and adding a new synonym locale
|
||||
remain intentionally disabled.
|
||||
gates, and never writes active `Config`/`ConfigCAS`. Renaming an object or
|
||||
member, removing collection items, and adding a new synonym locale remain
|
||||
intentionally disabled.
|
||||
|
||||
`metadata.object.member.add` adds one new object requisite or tabular-section
|
||||
column (`Attribute`) by cloning an existing attribute in the same collection.
|
||||
The caller passes only
|
||||
`template_member_ref`, `new_member_name`, and optionally synonym/comment; the
|
||||
adapter generates the GUID, preserves the template's type/settings, appends to
|
||||
the exact declared collection, and verifies the new identity after apply.
|
||||
Arbitrary type construction and deletion are not supported by this first
|
||||
structural route.
|
||||
the exact declared collection, and verifies the new identity, empty/default
|
||||
comment, container, and preserved non-identity settings after apply. A template
|
||||
is blocked when its GUID or name is referenced outside its declared identity
|
||||
fields. Arbitrary type construction and deletion are not supported by this
|
||||
first structural route.
|
||||
|
||||
Managed form bodies in base `Config` are resolved from the public form GUID to
|
||||
the sibling `<guid>.0` SQL payload. Command-bar buttons expose public command
|
||||
|
||||
@@ -1005,7 +1005,7 @@ METHOD_INPUT_SCHEMAS["metadata.object.member.add"] = {
|
||||
},
|
||||
"new_member_name": {"type": "string", "description": "New 1C Attribute name."},
|
||||
"new_member_synonym": {"type": "string", "description": "Optional ru synonym; defaults to the new name."},
|
||||
"new_member_comment": {"type": "string", "description": "Optional comment; otherwise inherited from the template."},
|
||||
"new_member_comment": {"type": "string", "description": "Optional comment; defaults to an empty string and is never inherited from the template."},
|
||||
"extension": {"type": "string"},
|
||||
"layer": {"type": "string", "enum": ["base_saved_state", "extension_saved_state"]},
|
||||
"expected_sha1": {"type": "string"},
|
||||
@@ -1018,6 +1018,89 @@ METHOD_INPUT_SCHEMAS["metadata.object.member.add"] = {
|
||||
"include_storage": {"type": "boolean"},
|
||||
},
|
||||
}
|
||||
METHOD_INPUT_SCHEMAS["metadata.write"] = {
|
||||
"type": "object",
|
||||
"required": ["base_id"],
|
||||
"additionalProperties": True,
|
||||
"properties": {
|
||||
"base_id": {"type": "string", "description": "Adapter base identifier."},
|
||||
"target": {
|
||||
"type": "object",
|
||||
"additionalProperties": True,
|
||||
"description": "Name-first target. Use kind/area=object|form|module|schedule plus a public ref, kind/name, or canonical_path.",
|
||||
"properties": {
|
||||
"kind": {"type": "string", "enum": ["object", "form", "module", "schedule"]},
|
||||
"area": {"type": "string", "enum": ["object", "form", "module", "schedule"]},
|
||||
"ref": {"type": "string", "description": "Public 1C object/member ref."},
|
||||
"canonical_path": {"type": "string", "description": "Public 1C code or metadata path."},
|
||||
"property": {"type": "string"},
|
||||
"operation": {
|
||||
"type": "string",
|
||||
"description": "Operation such as add_attribute, property_change, replace, or upsert_routine.",
|
||||
},
|
||||
"template_member_ref": {"type": "string"},
|
||||
"new_member_name": {"type": "string"},
|
||||
},
|
||||
},
|
||||
"target_kind": {"type": "string", "enum": ["object", "form", "module", "schedule"]},
|
||||
"ref": {"type": "string", "description": "Optional public 1C object ref."},
|
||||
"kind": {"type": "string", "description": "Optional metadata kind in English or Russian."},
|
||||
"name": {"type": "string", "description": "Optional exact 1C object name."},
|
||||
"object_type": {"type": "string", "description": "MCP alias of kind."},
|
||||
"object_name": {"type": "string", "description": "MCP alias of name."},
|
||||
"extension": {"type": "string"},
|
||||
"property": {"type": "string"},
|
||||
"value": {},
|
||||
"schedule": {"type": "object", "additionalProperties": True},
|
||||
"template_member_ref": {"type": "string"},
|
||||
"new_member_name": {"type": "string"},
|
||||
"new_member_synonym": {"type": "string"},
|
||||
"new_member_comment": {"type": "string"},
|
||||
"execution_mode": {
|
||||
"type": "string",
|
||||
"enum": ["plan", "apply", "apply_and_verify", "apply_and_rollback"],
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["plan", "apply", "apply_and_verify", "apply_and_rollback"],
|
||||
},
|
||||
"allow_saved_state_write": {"type": "boolean"},
|
||||
"allow_sql_saved_state_prepare": {"type": "boolean"},
|
||||
"allow_sql_saved_state_apply": {"type": "boolean"},
|
||||
"allow_sql_saved_state_rollback": {"type": "boolean"},
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"base_id": "upo_test",
|
||||
"target": {
|
||||
"area": "object",
|
||||
"ref": "Catalog.Номенклатура",
|
||||
"property": "synonym",
|
||||
},
|
||||
"value": "Номенклатура",
|
||||
"mode": "plan",
|
||||
},
|
||||
{
|
||||
"base_id": "upo_test",
|
||||
"target": {
|
||||
"area": "object",
|
||||
"operation": "add_attribute",
|
||||
"template_member_ref": "Catalog.Номенклатура.Attribute.Артикул",
|
||||
"new_member_name": "КодПоставщика",
|
||||
},
|
||||
"mode": "plan",
|
||||
},
|
||||
{
|
||||
"base_id": "upo_test",
|
||||
"target": {
|
||||
"kind": "schedule",
|
||||
"ref": "ScheduledJob.ОбменДанными",
|
||||
},
|
||||
"schedule": {"begin_time": "09:00:00"},
|
||||
"mode": "plan",
|
||||
},
|
||||
],
|
||||
}
|
||||
METHOD_INPUT_SCHEMAS["metadata.code_index.build"] = {
|
||||
"type": "object",
|
||||
"required": ["base_id"],
|
||||
@@ -3098,10 +3181,18 @@ METADATA_WRITE_CAPABILITIES = {
|
||||
"form_element_properties": {
|
||||
"status": "partial_saved_state",
|
||||
"targets": ["form_element", "form_command", "form_attribute"],
|
||||
"operations": ["plan_property_write", "apply_property_write"],
|
||||
"operations": [
|
||||
"plan_property_write",
|
||||
"apply_property_write",
|
||||
"move_sibling_target",
|
||||
"upsert_command_button",
|
||||
"upsert_handler_routine",
|
||||
"verify_command_button_links",
|
||||
],
|
||||
"agent_method": "metadata.write",
|
||||
"write_layer": "save",
|
||||
"gaps": ["Complex command handler bindings and inherited form properties are not fully writable yet."],
|
||||
"guards": ["expected_sha1", "backup", "semantic_readback", "cache_invalidation"],
|
||||
"gaps": ["Cross-parent structural moves, arbitrary element deletion, complex command bindings, and inherited form properties are not fully writable yet."],
|
||||
},
|
||||
"object_metadata": {
|
||||
"status": "partial_saved_state",
|
||||
@@ -3109,8 +3200,8 @@ METADATA_WRITE_CAPABILITIES = {
|
||||
"operations": ["write_synonym", "write_comment", "write_member_synonym", "write_member_comment", "add_attribute_from_template"],
|
||||
"agent_method": "metadata.write",
|
||||
"write_layer": "save",
|
||||
"guards": ["expected_old", "expected_sha1", "backup", "semantic_readback"],
|
||||
"gaps": ["Attribute add currently requires an existing Attribute template in the same collection. Object/member rename, arbitrary type construction, deletion, and other structural collection writes remain disabled."],
|
||||
"guards": ["expected_old", "expected_sha1", "backup", "semantic_readback", "settings_shape_verification", "cache_invalidation"],
|
||||
"gaps": ["Attribute add currently requires a safe existing Attribute template in the same collection. Object/member rename, arbitrary type construction, deletion, Dimension/Resource/TabularSection creation, and other structural collection writes remain disabled."],
|
||||
},
|
||||
"templates": {
|
||||
"status": "read_only",
|
||||
@@ -17521,6 +17612,7 @@ def apply_saved_state_prepare_copy(
|
||||
file_names: list[str],
|
||||
*,
|
||||
expected_source_rows: int,
|
||||
prepared_rows: list[dict[str, Any]],
|
||||
timeout_seconds: int,
|
||||
) -> dict[str, Any]:
|
||||
method = "metadata.saved_state.prepare"
|
||||
@@ -17573,6 +17665,13 @@ def apply_saved_state_prepare_copy(
|
||||
"counts": {"expected_insert_rows": expected_source_rows, "inserted_rows": inserted},
|
||||
"diagnostics": {"message": "Copied row count did not match current source row count."},
|
||||
}
|
||||
receipt = write_saved_state_prepare_receipt(
|
||||
base_id=base_id,
|
||||
config=config,
|
||||
source_table=source_table,
|
||||
target_table=target_table,
|
||||
rows=prepared_rows,
|
||||
)
|
||||
conn.commit()
|
||||
except Exception as exc:
|
||||
try:
|
||||
@@ -17601,6 +17700,7 @@ def apply_saved_state_prepare_copy(
|
||||
"source": {"kind": "live_sql", "database": config["database"], "table": source_table},
|
||||
"target": {"table": target_table},
|
||||
"counts": {"inserted_rows": inserted, "file_names": len(names)},
|
||||
"prepare_receipt": receipt,
|
||||
"cache_invalidation": invalidate_adapter_caches_after_saved_state_change(
|
||||
base_id,
|
||||
reason="saved_state_prepare",
|
||||
@@ -17609,6 +17709,220 @@ def apply_saved_state_prepare_copy(
|
||||
}
|
||||
|
||||
|
||||
def saved_state_prepare_receipt_dir() -> Path:
|
||||
return storage_apply_backup_dir() / "prepare-receipts"
|
||||
|
||||
|
||||
def write_saved_state_prepare_receipt(
|
||||
*,
|
||||
base_id: str,
|
||||
config: dict[str, str],
|
||||
source_table: str,
|
||||
target_table: str,
|
||||
rows: list[dict[str, Any]],
|
||||
) -> dict[str, Any]:
|
||||
receipt_id = uuid.uuid4().hex
|
||||
created_at = datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")
|
||||
root = saved_state_prepare_receipt_dir()
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
path = root / f"{created_at.replace(':', '').replace('-', '')}-{receipt_id}.json"
|
||||
normalized_rows = [
|
||||
{
|
||||
key: row.get(key)
|
||||
for key in ("file_name", "part_no", "data_size", "binary_bytes", "binary_sha1")
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
evidence = {
|
||||
"schema": "onec_saved_state_prepare_receipt.v1",
|
||||
"receipt_id": receipt_id,
|
||||
"created_at_utc": created_at,
|
||||
"base_id": base_id,
|
||||
"source": {
|
||||
"server": config.get("server"),
|
||||
"database": config.get("database"),
|
||||
"table": source_table,
|
||||
},
|
||||
"target": {"table": target_table},
|
||||
"rows": normalized_rows,
|
||||
"status": "prepared",
|
||||
}
|
||||
path.write_text(json.dumps(evidence, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
return {
|
||||
"receipt_id": receipt_id,
|
||||
"path": str(path),
|
||||
"rows": len(normalized_rows),
|
||||
}
|
||||
|
||||
|
||||
def resolve_saved_state_prepare_receipt(receipt_id: str) -> Path | None:
|
||||
normalized = str(receipt_id or "").strip().lower()
|
||||
if not re.fullmatch(r"[0-9a-f]{32}", normalized):
|
||||
return None
|
||||
root = saved_state_prepare_receipt_dir()
|
||||
matches = sorted(root.glob(f"*{normalized}.json")) if root.is_dir() else []
|
||||
return matches[0] if len(matches) == 1 else None
|
||||
|
||||
|
||||
def rollback_saved_state_prepare_receipt(
|
||||
base_id: str,
|
||||
receipt_id: str,
|
||||
*,
|
||||
timeout_seconds: int,
|
||||
) -> dict[str, Any]:
|
||||
path = resolve_saved_state_prepare_receipt(receipt_id)
|
||||
if path is None:
|
||||
return {
|
||||
"schema": "onec_saved_state_prepare_rollback.v1",
|
||||
"status": "not_found",
|
||||
"applied": False,
|
||||
"receipt_id": receipt_id,
|
||||
}
|
||||
try:
|
||||
evidence = json.loads(path.read_text(encoding="utf-8-sig"))
|
||||
except Exception as exc:
|
||||
return {
|
||||
"schema": "onec_saved_state_prepare_rollback.v1",
|
||||
"status": "invalid_receipt",
|
||||
"applied": False,
|
||||
"receipt_id": receipt_id,
|
||||
"diagnostics": {"message": str(exc)},
|
||||
}
|
||||
if str(evidence.get("base_id") or "") != str(base_id):
|
||||
return {
|
||||
"schema": "onec_saved_state_prepare_rollback.v1",
|
||||
"status": "blocked",
|
||||
"applied": False,
|
||||
"error": "receipt_base_mismatch",
|
||||
"receipt_id": receipt_id,
|
||||
}
|
||||
if evidence.get("status") == "rolled_back":
|
||||
return {
|
||||
"schema": "onec_saved_state_prepare_rollback.v1",
|
||||
"status": "already_rolled_back",
|
||||
"applied": True,
|
||||
"receipt_id": receipt_id,
|
||||
}
|
||||
target = evidence.get("target") if isinstance(evidence.get("target"), dict) else {}
|
||||
target_table = str(target.get("table") or "")
|
||||
if target_table not in SAVED_STATE_SOURCE_BY_TARGET:
|
||||
return {
|
||||
"schema": "onec_saved_state_prepare_rollback.v1",
|
||||
"status": "invalid_receipt",
|
||||
"applied": False,
|
||||
"error": "receipt_target_invalid",
|
||||
"receipt_id": receipt_id,
|
||||
}
|
||||
expected_rows = [
|
||||
row for row in (evidence.get("rows") or [])
|
||||
if isinstance(row, dict) and row.get("file_name") and Path(str(row["file_name"])).name == str(row["file_name"])
|
||||
]
|
||||
file_names = sorted({str(row["file_name"]) for row in expected_rows})
|
||||
if not expected_rows or not file_names:
|
||||
return {
|
||||
"schema": "onec_saved_state_prepare_rollback.v1",
|
||||
"status": "invalid_receipt",
|
||||
"applied": False,
|
||||
"error": "receipt_rows_missing",
|
||||
"receipt_id": receipt_id,
|
||||
}
|
||||
|
||||
conn, config, error = connect_live_sql(base_id, "metadata.saved_state.prepare.rollback", timeout_seconds=timeout_seconds)
|
||||
if error:
|
||||
return error
|
||||
started = time.time()
|
||||
try:
|
||||
cursor = conn.cursor(as_dict=True)
|
||||
placeholders = ",".join(["%s"] * len(file_names))
|
||||
cursor.execute(
|
||||
f"""
|
||||
SELECT FileName, PartNo, DataSize, DATALENGTH(BinaryData) AS BinaryBytes,
|
||||
CONVERT(varchar(40), HASHBYTES('SHA1', BinaryData), 2) AS BinarySHA1
|
||||
FROM dbo.[{target_table}] WITH (UPDLOCK, HOLDLOCK)
|
||||
WHERE FileName IN ({placeholders})
|
||||
ORDER BY FileName, PartNo
|
||||
""",
|
||||
tuple(file_names),
|
||||
)
|
||||
current_rows = [
|
||||
saved_state_row_public({key: jsonable(value) for key, value in row.items()})
|
||||
for row in cursor.fetchall()
|
||||
]
|
||||
comparable_fields = ("file_name", "part_no", "data_size", "binary_bytes", "binary_sha1")
|
||||
expected_comparable = sorted(
|
||||
tuple(row.get(field) for field in comparable_fields)
|
||||
for row in expected_rows
|
||||
)
|
||||
current_comparable = sorted(
|
||||
tuple(row.get(field) for field in comparable_fields)
|
||||
for row in current_rows
|
||||
)
|
||||
if current_comparable != expected_comparable:
|
||||
conn.rollback()
|
||||
return {
|
||||
"schema": "onec_saved_state_prepare_rollback.v1",
|
||||
"status": "precondition_failed",
|
||||
"applied": False,
|
||||
"error": "prepared_rows_changed",
|
||||
"receipt_id": receipt_id,
|
||||
"counts": {
|
||||
"expected_rows": len(expected_comparable),
|
||||
"current_rows": len(current_comparable),
|
||||
},
|
||||
}
|
||||
cursor.execute(
|
||||
f"DELETE FROM dbo.[{target_table}] WHERE FileName IN ({placeholders})",
|
||||
tuple(file_names),
|
||||
)
|
||||
deleted = int(cursor.rowcount or 0)
|
||||
if deleted != len(expected_rows):
|
||||
conn.rollback()
|
||||
return {
|
||||
"schema": "onec_saved_state_prepare_rollback.v1",
|
||||
"status": "precondition_failed",
|
||||
"applied": False,
|
||||
"error": "prepared_row_delete_count_mismatch",
|
||||
"receipt_id": receipt_id,
|
||||
"counts": {"expected_rows": len(expected_rows), "deleted_rows": deleted},
|
||||
}
|
||||
conn.commit()
|
||||
except Exception as exc:
|
||||
try:
|
||||
conn.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
return {
|
||||
"schema": "onec_saved_state_prepare_rollback.v1",
|
||||
"status": "error",
|
||||
"applied": False,
|
||||
"receipt_id": receipt_id,
|
||||
"diagnostics": {"message": str(exc)},
|
||||
}
|
||||
finally:
|
||||
try:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
evidence["status"] = "rolled_back"
|
||||
evidence["rolled_back_at_utc"] = datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")
|
||||
path.write_text(json.dumps(evidence, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
return {
|
||||
"schema": "onec_saved_state_prepare_rollback.v1",
|
||||
"status": "rolled_back",
|
||||
"applied": True,
|
||||
"base_id": base_id,
|
||||
"receipt_id": receipt_id,
|
||||
"target": {"table": target_table},
|
||||
"counts": {"deleted_rows": deleted, "file_names": len(file_names)},
|
||||
"cache_invalidation": invalidate_adapter_caches_after_saved_state_change(
|
||||
base_id,
|
||||
reason="saved_state_prepare_rollback",
|
||||
),
|
||||
"duration_ms": int((time.time() - started) * 1000),
|
||||
}
|
||||
|
||||
|
||||
def public_saved_state_prepare_object(object_card: dict[str, Any] | None) -> dict[str, Any] | None:
|
||||
if not isinstance(object_card, dict):
|
||||
return None
|
||||
@@ -17880,6 +18194,7 @@ def metadata_saved_state_prepare(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
target_table,
|
||||
file_names,
|
||||
expected_source_rows=len(source_rows),
|
||||
prepared_rows=public_source_rows,
|
||||
timeout_seconds=timeout_seconds,
|
||||
)
|
||||
result["apply_result"] = apply_result
|
||||
@@ -42579,6 +42894,9 @@ def metadata_object_property_write(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
"diagnostics": prepare_result.get("diagnostics") or {"message": "Could not resolve a writable saved-state copy for the named object."},
|
||||
}
|
||||
return result if include_storage else sanitize_object_property_write_result(result)
|
||||
prepare_apply_result = prepare_result.get("apply_result") if isinstance(prepare_result.get("apply_result"), dict) else {}
|
||||
prepare_receipt = prepare_apply_result.get("prepare_receipt") if isinstance(prepare_apply_result.get("prepare_receipt"), dict) else {}
|
||||
prepare_receipt_id = str(prepare_receipt.get("receipt_id") or "")
|
||||
|
||||
object_card = prepare_result.get("object") if isinstance(prepare_result.get("object"), dict) else {}
|
||||
object_guid = str(object_card.get("guid") or payload.get("guid") or payload.get("object_guid") or "").strip().lower()
|
||||
@@ -42787,7 +43105,22 @@ def metadata_object_property_write(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
}
|
||||
)
|
||||
result["rollback_result"] = rollback_result
|
||||
result["rolled_back"] = bool(rollback_result.get("applied"))
|
||||
payload_rolled_back = bool(rollback_result.get("applied"))
|
||||
prepare_rollback_result = None
|
||||
if payload_rolled_back and prepare_receipt_id:
|
||||
prepare_rollback_result = rollback_saved_state_prepare_receipt(
|
||||
base_id,
|
||||
prepare_receipt_id,
|
||||
timeout_seconds=timeout_seconds,
|
||||
)
|
||||
result["prepare_rollback_result"] = prepare_rollback_result
|
||||
result["rolled_back"] = bool(
|
||||
payload_rolled_back
|
||||
and (
|
||||
not prepare_receipt_id
|
||||
or (prepare_rollback_result or {}).get("applied")
|
||||
)
|
||||
)
|
||||
result["status"] = (
|
||||
"verified_and_rolled_back"
|
||||
if result["applied"] and verified and result["rolled_back"]
|
||||
@@ -42903,6 +43236,9 @@ def metadata_object_member_add(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
"prepare_result": prepare_result,
|
||||
}
|
||||
return result if include_storage else sanitize_object_property_write_result(result)
|
||||
prepare_apply_result = prepare_result.get("apply_result") if isinstance(prepare_result.get("apply_result"), dict) else {}
|
||||
prepare_receipt = prepare_apply_result.get("prepare_receipt") if isinstance(prepare_apply_result.get("prepare_receipt"), dict) else {}
|
||||
prepare_receipt_id = str(prepare_receipt.get("receipt_id") or "")
|
||||
|
||||
object_card = prepare_result.get("object") if isinstance(prepare_result.get("object"), dict) else {}
|
||||
object_guid = str(object_card.get("guid") or payload.get("guid") or payload.get("object_guid") or "").strip().lower()
|
||||
@@ -43148,7 +43484,22 @@ def metadata_object_member_add(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
}
|
||||
)
|
||||
result["rollback_result"] = rollback_result
|
||||
result["rolled_back"] = bool(rollback_result.get("applied"))
|
||||
payload_rolled_back = bool(rollback_result.get("applied"))
|
||||
prepare_rollback_result = None
|
||||
if payload_rolled_back and prepare_receipt_id:
|
||||
prepare_rollback_result = rollback_saved_state_prepare_receipt(
|
||||
base_id,
|
||||
prepare_receipt_id,
|
||||
timeout_seconds=timeout_seconds,
|
||||
)
|
||||
result["prepare_rollback_result"] = prepare_rollback_result
|
||||
result["rolled_back"] = bool(
|
||||
payload_rolled_back
|
||||
and (
|
||||
not prepare_receipt_id
|
||||
or (prepare_rollback_result or {}).get("applied")
|
||||
)
|
||||
)
|
||||
result["status"] = "verified_and_rolled_back" if verified and result["rolled_back"] else "applied_rollback_failed"
|
||||
return result if include_storage else sanitize_object_property_write_result(result)
|
||||
|
||||
@@ -52211,14 +52562,14 @@ def validate_metadata_write_payload(payload: dict[str, Any]) -> dict[str, Any] |
|
||||
target_dict = target if isinstance(target, dict) else {}
|
||||
route_kind = payload.get("target_kind") or target_dict.get("target_kind") or target_dict.get("area") or payload.get("area")
|
||||
if not route_kind and str(target_dict.get("kind") or "").strip().casefold() in {
|
||||
"form", "форма", "module", "модуль", "bsl", "object", "объект", "metadata", "метаданные"
|
||||
"form", "форма", "module", "модуль", "bsl", "object", "объект", "metadata", "метаданные", "schedule", "расписание"
|
||||
}:
|
||||
route_kind = target_dict.get("kind")
|
||||
if not route_kind and str(payload.get("kind") or "").strip().casefold() in {"form", "форма", "module", "модуль", "bsl"}:
|
||||
route_kind = payload.get("kind")
|
||||
target_kind = str(route_kind or "form").strip().casefold()
|
||||
if target_kind not in {"form", "форма", "module", "модуль", "bsl", "object", "объект", "metadata", "метаданные"}:
|
||||
return invalid_argument(method, "target.kind", "Only form, module, and object-property saved-state writes are currently routed.", allowed_values=["form", "module", "object"])
|
||||
if target_kind not in {"form", "форма", "module", "модуль", "bsl", "object", "объект", "metadata", "метаданные", "schedule", "расписание"}:
|
||||
return invalid_argument(method, "target.kind", "Only form, module, object, and scheduled-job schedule saved-state writes are currently routed.", allowed_values=["form", "module", "object", "schedule"])
|
||||
mode = str(payload.get("execution_mode") or payload.get("mode") or "plan").strip().lower()
|
||||
if mode not in FORM_ELEMENT_WRITE_APPLY_MODES:
|
||||
return invalid_argument(method, "execution_mode", "Unsupported execution mode.", allowed_values=sorted(FORM_ELEMENT_WRITE_APPLY_MODES))
|
||||
|
||||
@@ -394,6 +394,32 @@ TOOLS = [
|
||||
"mode": "plan",
|
||||
},
|
||||
},
|
||||
{
|
||||
"method": "metadata.write",
|
||||
"payload": {
|
||||
"base_id": "<base_id-from-project-context>",
|
||||
"target": {
|
||||
"area": "object",
|
||||
"ref": "Справочник.<object-name>",
|
||||
"property": "synonym",
|
||||
},
|
||||
"value": "<new-synonym>",
|
||||
"mode": "plan",
|
||||
},
|
||||
},
|
||||
{
|
||||
"method": "metadata.write",
|
||||
"payload": {
|
||||
"base_id": "<base_id-from-project-context>",
|
||||
"target": {
|
||||
"area": "object",
|
||||
"operation": "add_attribute",
|
||||
"template_member_ref": "Справочник.<object-name>.Реквизит.<template-attribute>",
|
||||
"new_member_name": "<new-attribute-name>",
|
||||
},
|
||||
"mode": "plan",
|
||||
},
|
||||
},
|
||||
{
|
||||
"method": "metadata.form.command_button.write",
|
||||
"payload": {
|
||||
|
||||
Reference in New Issue
Block a user