fix(qwen): align hooks with Qwen Code 0.20
This commit is contained in:
1 parent
ad315c3655
commit
231cda604e
13 files changed
+2088
-916
No files matched your search
+129
-77
@@ -1,4 +1,4 @@
|
||||
"""Qwen Code settings parsing and source-preserving rendering."""
|
||||
"""Qwen Code JSONC parsing and source-preserving hook edits."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -6,12 +6,13 @@ from dataclasses import dataclass
|
||||
from typing import Dict, Optional
|
||||
|
||||
from ._jsonc import JsoncEditor
|
||||
from ._managed_files import FileSnapshot
|
||||
from .qwen_contract import (
|
||||
TOOL_EVENTS,
|
||||
MANAGED_EVENTS,
|
||||
JsonObject,
|
||||
QwenHookSettings,
|
||||
managed_removals,
|
||||
read_hook_settings,
|
||||
QwenHooks,
|
||||
managed_qwen_removals,
|
||||
read_qwen_hooks,
|
||||
)
|
||||
|
||||
|
||||
@@ -20,83 +21,121 @@ OWNED_FILE_MARKER = "// Managed by Elydora"
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class QwenDocument:
|
||||
kind: str
|
||||
file_path: str
|
||||
exists: bool
|
||||
raw: str
|
||||
snapshot: Optional[FileSnapshot]
|
||||
root: JsonObject
|
||||
hooks: QwenHookSettings
|
||||
hooks: QwenHooks
|
||||
has_hooks_container: bool
|
||||
hooks_disabled: bool
|
||||
disable_all_hooks: Optional[bool]
|
||||
folder_trust_enabled: Optional[bool]
|
||||
owned_file: bool
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RenderedDocument:
|
||||
class RenderedQwenDocument:
|
||||
document: QwenDocument
|
||||
changed: bool
|
||||
next_source: Optional[str]
|
||||
|
||||
|
||||
def _label(file_path: str) -> str:
|
||||
return f"Qwen Code settings at {file_path}"
|
||||
def qwen_source_label(kind: str) -> str:
|
||||
labels = {
|
||||
"system-defaults": "Qwen Code system defaults",
|
||||
"user": "Qwen Code user settings",
|
||||
"workspace": "Qwen Code workspace settings",
|
||||
"system": "Qwen Code system override settings",
|
||||
}
|
||||
try:
|
||||
return labels[kind]
|
||||
except KeyError as error:
|
||||
raise ValueError(f"Unsupported Qwen Code document kind: {kind}") from error
|
||||
|
||||
|
||||
def parse_document(
|
||||
def qwen_document_label(document: QwenDocument) -> str:
|
||||
return qwen_source_label(document.kind)
|
||||
|
||||
|
||||
def parse_qwen_jsonc_object(raw: str, label: str) -> JsonObject:
|
||||
editor = JsoncEditor(raw, label, allow_trailing_commas=False)
|
||||
if not isinstance(editor.value, dict):
|
||||
raise ValueError(f"{label} must contain a JSON object")
|
||||
return editor.value
|
||||
|
||||
|
||||
def _folder_trust_enabled(
|
||||
root: JsonObject, label: str
|
||||
) -> Optional[bool]:
|
||||
if "security" not in root:
|
||||
return None
|
||||
security = root["security"]
|
||||
if not isinstance(security, dict):
|
||||
raise ValueError(f'{label} field "security" must be an object')
|
||||
if "folderTrust" not in security:
|
||||
return None
|
||||
folder_trust = security["folderTrust"]
|
||||
if not isinstance(folder_trust, dict):
|
||||
raise ValueError(
|
||||
f'{label} field "security.folderTrust" must be an object'
|
||||
)
|
||||
enabled = folder_trust.get("enabled")
|
||||
if enabled is not None and not isinstance(enabled, bool):
|
||||
raise ValueError(
|
||||
f'{label} field "security.folderTrust.enabled" must be a boolean'
|
||||
)
|
||||
return enabled
|
||||
|
||||
|
||||
def parse_qwen_document(
|
||||
*,
|
||||
kind: str,
|
||||
exists: bool,
|
||||
file_path: str,
|
||||
raw: str,
|
||||
snapshot: Optional[FileSnapshot] = None,
|
||||
) -> QwenDocument:
|
||||
label = _label(file_path)
|
||||
editor = JsoncEditor(
|
||||
raw,
|
||||
label,
|
||||
allow_trailing_commas=False,
|
||||
)
|
||||
if not isinstance(editor.value, dict):
|
||||
raise ValueError(f"{label} must contain a JSON object")
|
||||
root = editor.value
|
||||
label = f"{qwen_source_label(kind)} at {file_path}"
|
||||
root = parse_qwen_jsonc_object(raw, label)
|
||||
disabled = root.get("disableAllHooks")
|
||||
if "disableAllHooks" in root and not isinstance(disabled, bool):
|
||||
raise ValueError(f'{label} field "disableAllHooks" must be a boolean')
|
||||
has_container = "hooks" in root
|
||||
hooks = (
|
||||
read_hook_settings(root["hooks"], f'{label} field "hooks"')
|
||||
if has_container
|
||||
else {}
|
||||
)
|
||||
has_hooks = "hooks" in root
|
||||
return QwenDocument(
|
||||
file_path,
|
||||
exists,
|
||||
raw,
|
||||
root,
|
||||
hooks,
|
||||
has_container,
|
||||
disabled is True,
|
||||
raw.startswith(OWNED_FILE_MARKER),
|
||||
kind=kind,
|
||||
file_path=file_path,
|
||||
exists=exists,
|
||||
raw=raw,
|
||||
snapshot=snapshot,
|
||||
root=root,
|
||||
hooks=read_qwen_hooks(root["hooks"]) if has_hooks else {},
|
||||
has_hooks_container=has_hooks,
|
||||
disable_all_hooks=disabled if isinstance(disabled, bool) else None,
|
||||
folder_trust_enabled=_folder_trust_enabled(root, label),
|
||||
owned_file=kind == "user" and raw.startswith(OWNED_FILE_MARKER),
|
||||
)
|
||||
|
||||
|
||||
def create_owned_document(file_path: str) -> QwenDocument:
|
||||
return parse_document(
|
||||
def create_qwen_document(kind: str, file_path: str) -> QwenDocument:
|
||||
raw = f"{OWNED_FILE_MARKER}\n{{}}\n" if kind == "user" else "{}\n"
|
||||
return parse_qwen_document(
|
||||
kind=kind,
|
||||
exists=False,
|
||||
file_path=file_path,
|
||||
raw=f"{OWNED_FILE_MARKER}\n{{}}\n",
|
||||
raw=raw,
|
||||
)
|
||||
|
||||
|
||||
def _settings_from_editor(
|
||||
editor: JsoncEditor,
|
||||
document: QwenDocument,
|
||||
) -> QwenHookSettings:
|
||||
root = editor.value
|
||||
if not isinstance(root, dict):
|
||||
raise ValueError(f"{_label(document.file_path)} must contain a JSON object")
|
||||
if "hooks" not in root:
|
||||
return {}
|
||||
return read_hook_settings(
|
||||
root["hooks"],
|
||||
f'{_label(document.file_path)} field "hooks"',
|
||||
def _current_document(
|
||||
document: QwenDocument, editor: JsoncEditor
|
||||
) -> QwenDocument:
|
||||
return parse_qwen_document(
|
||||
kind=document.kind,
|
||||
exists=document.exists,
|
||||
file_path=document.file_path,
|
||||
raw=editor.raw,
|
||||
snapshot=document.snapshot,
|
||||
)
|
||||
|
||||
|
||||
@@ -105,8 +144,8 @@ def _remove_managed(
|
||||
document: QwenDocument,
|
||||
agent_id: Optional[str],
|
||||
) -> None:
|
||||
removals = managed_removals(document.hooks, agent_id)
|
||||
for event in TOOL_EVENTS:
|
||||
removals = managed_qwen_removals(document.hooks, agent_id)
|
||||
for event in MANAGED_EVENTS:
|
||||
event_removals = sorted(
|
||||
(item for item in removals if item.event == event),
|
||||
key=lambda item: item.group_index,
|
||||
@@ -120,14 +159,12 @@ def _remove_managed(
|
||||
for handler_index in sorted(removal.handler_indexes, reverse=True):
|
||||
editor.delete((*group_path, "hooks", handler_index))
|
||||
if event_removals:
|
||||
current = _settings_from_editor(editor, document)
|
||||
if event in current and not current[event]:
|
||||
current = _current_document(document, editor)
|
||||
if event in current.hooks and not current.hooks[event]:
|
||||
editor.delete(("hooks", event))
|
||||
root = editor.value
|
||||
if isinstance(root, dict) and "hooks" in root:
|
||||
current = _settings_from_editor(editor, document)
|
||||
if not current:
|
||||
editor.delete(("hooks",))
|
||||
current = _current_document(document, editor)
|
||||
if current.has_hooks_container and not current.hooks:
|
||||
editor.delete(("hooks",))
|
||||
|
||||
|
||||
def _append_group(
|
||||
@@ -136,42 +173,57 @@ def _append_group(
|
||||
event: str,
|
||||
group: JsonObject,
|
||||
) -> None:
|
||||
root = editor.value
|
||||
if not isinstance(root, dict):
|
||||
raise ValueError(f"{_label(document.file_path)} must contain a JSON object")
|
||||
if "hooks" not in root:
|
||||
current = _current_document(document, editor)
|
||||
if not current.has_hooks_container:
|
||||
editor.add_property((), "hooks", {event: [group]})
|
||||
return
|
||||
current = _settings_from_editor(editor, document)
|
||||
if event in current:
|
||||
elif event in current.hooks:
|
||||
editor.append(("hooks", event), group)
|
||||
else:
|
||||
editor.add_property(("hooks",), event, [group])
|
||||
|
||||
|
||||
def render_document(
|
||||
def _already_installed(
|
||||
document: QwenDocument, additions: Dict[str, JsonObject]
|
||||
) -> bool:
|
||||
if set(additions) != set(MANAGED_EVENTS):
|
||||
return False
|
||||
removals = managed_qwen_removals(document.hooks)
|
||||
for event in MANAGED_EVENTS:
|
||||
expected = additions[event]
|
||||
event_removals = [item for item in removals if item.event == event]
|
||||
exact_groups = [
|
||||
group for group in document.hooks.get(event, []) if group == expected
|
||||
]
|
||||
if (
|
||||
len(event_removals) != 1
|
||||
or not event_removals[0].remove_group
|
||||
or len(exact_groups) != 1
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def render_qwen_document(
|
||||
document: QwenDocument,
|
||||
agent_id: Optional[str],
|
||||
additions: Dict[str, JsonObject],
|
||||
) -> RenderedDocument:
|
||||
) -> RenderedQwenDocument:
|
||||
if agent_id is None and _already_installed(document, additions):
|
||||
return RenderedQwenDocument(document, False, document.raw)
|
||||
editor = JsoncEditor(
|
||||
document.raw,
|
||||
_label(document.file_path),
|
||||
f"{qwen_document_label(document)} at {document.file_path}",
|
||||
allow_trailing_commas=False,
|
||||
)
|
||||
_remove_managed(editor, document, agent_id)
|
||||
for event in TOOL_EVENTS:
|
||||
for event in MANAGED_EVENTS:
|
||||
group = additions.get(event)
|
||||
if group is not None:
|
||||
_append_group(editor, document, event, group)
|
||||
next_document = parse_document(
|
||||
exists=document.exists,
|
||||
file_path=document.file_path,
|
||||
raw=editor.raw,
|
||||
)
|
||||
if not additions and document.owned_file and not next_document.root:
|
||||
return RenderedDocument(document, True, None)
|
||||
return RenderedDocument(
|
||||
current = _current_document(document, editor)
|
||||
if not additions and document.owned_file and not current.root:
|
||||
return RenderedQwenDocument(document, True, None)
|
||||
return RenderedQwenDocument(
|
||||
document,
|
||||
editor.raw != document.raw,
|
||||
editor.raw,
|
||||
|
||||
Reference in new issue
Block a user