Ctrl K
ComunidadGuías…

butler/gateway/queue_settings.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Per-session gateway inbound queue mode (OpenClaw-style, zero deps)."""

from __future__ import annotations

import json
import re
import threading
from pathlib import Path
from typing import Any, cast

from butler.configuration.settings import get_butler_home
from butler.configuration.gateway import resolve_gateway_queue_config

_VALID_MODES = frozenset({"followup", "collect", "interrupt", "steer"})
_VALID_DROP = frozenset({"summarize", "old", "new"})
_LOCK = threading.RLock()
_OVERRIDES: dict[str, dict[str, Any]] = {}


def _settings_dir() -> Path:
    path = get_butler_home() / "gateway_queue"
    path.mkdir(parents=True, exist_ok=True)
    return cast(Path, path)


def _override_path(session_key: str) -> Path:
    import hashlib

    digest = hashlib.sha256(str(session_key or "default").encode("utf-8")).hexdigest()[:16]
    return _settings_dir() / f"{digest}.json"


def _load_override_file(session_key: str) -> dict[str, Any]:
    from butler.gateway.queue_settings_ops import load_queue_override_safe

    return cast(dict[str, Any], load_queue_override_safe(_override_path(session_key)))


def _save_override_file(session_key: str, data: dict[str, Any]) -> None:
    path = _override_path(session_key)
    if not data:
        path.unlink(missing_ok=True)
        with _LOCK:
            _OVERRIDES.pop(str(session_key or "default"), None)
        return
    path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
    with _LOCK:
        _OVERRIDES[str(session_key or "default")] = dict(data)


def get_session_override(session_key: str) -> dict[str, Any]:
    key = str(session_key or "default")
    with _LOCK:
        cached = _OVERRIDES.get(key)
    if cached is not None:
        return dict(cached)
    loaded = _load_override_file(key)
    with _LOCK:
        _OVERRIDES[key] = loaded
    return dict(loaded)


def clear_session_override(session_key: str) -> None:
    _save_override_file(session_key, {})


def default_queue_mode() -> str:
    return cast(str, resolve_gateway_queue_config().mode)


def get_queue_mode(session_key: str) -> str:
    override = get_session_override(session_key)
    mode = str(override.get("mode") or "").strip().lower()
    if mode in _VALID_MODES:
        return mode
    return default_queue_mode()


def queue_cap() -> int:
    return cast(int, resolve_gateway_queue_config().cap)


def queue_drop_policy() -> str:
    return cast(str, resolve_gateway_queue_config().drop)


def session_queue_cap(session_key: str) -> int:
    override = get_session_override(session_key)
    if override.get("cap") is not None:
        try:
            return max(1, int(override["cap"]))
        except (TypeError, ValueError):
            pass
    return queue_cap()


def session_drop_policy(session_key: str) -> str:
    override = get_session_override(session_key)
    drop = str(override.get("drop") or "").strip().lower()
    if drop in _VALID_DROP:
        return drop
    return queue_drop_policy()


def collect_debounce_ms(session_key: str) -> int:
    override = get_session_override(session_key)
    if override.get("debounce_ms") is not None:
        try:
            return max(0, int(override["debounce_ms"]))
        except (TypeError, ValueError):
            pass
    return cast(int, resolve_gateway_queue_config().collect_debounce_ms)


def _parse_duration_ms(token: str) -> int | None:
    token = (token or "").strip().lower()
    if not token:
        return None
    m = re.fullmatch(r"(\d+(?:\.\d+)?)(ms|s|m)?", token)
    if not m:
        return None
    value = float(m.group(1))
    unit = m.group(2) or "ms"
    if unit == "s":
        return int(value * 1000)
    if unit == "m":
        return int(value * 60_000)
    return int(value)


def parse_queue_command(arg: str) -> tuple[str | None, dict[str, Any], str | None]:
    """Parse ``/queue steer|followup|collect|interrupt`` and options."""
    stripped = (arg or "").strip()
    if not stripped:
        return None, {}, "用法:/queue followup|collect|interrupt|steer [cap:N] [drop:summarize] [debounce:500ms]"
    if stripped.lower() in ("/queue", "default", "reset"):
        return "reset", {}, None

    parts = stripped.split()
    mode_token = parts[0].lower()
    if mode_token not in _VALID_MODES and mode_token != "reset":
        return None, {}, f"未知模式:{mode_token}(可选 followup、collect、interrupt、steer)"

    opts: dict[str, Any] = {}
    for token in parts[1:]:
        lower = token.lower()
        if lower.startswith("cap:"):
            try:
                opts["cap"] = max(1, int(lower.split(":", 1)[1]))
            except ValueError:
                return None, {}, f"无效 cap:{token}"
        elif lower.startswith("drop:"):
            drop = lower.split(":", 1)[1]
            if drop not in _VALID_DROP:
                return None, {}, f"无效 drop:{drop}"
            opts["drop"] = drop
        elif lower.startswith("debounce:"):
            ms = _parse_duration_ms(lower.split(":", 1)[1])
            if ms is None:
                return None, {}, f"无效 debounce:{token}"
            opts["debounce_ms"] = ms
        else:
            return None, {}, f"未知选项:{token}"

    if mode_token == "reset":
        return "reset", opts, None
    return mode_token, opts, None


def apply_queue_command(session_key: str, arg: str) -> str:
    mode, opts, err = parse_queue_command(arg)
    if err:
        return err
    if mode == "reset":
        clear_session_override(session_key)
        return f"已恢复入站队列默认(全局模式 {default_queue_mode()},cap={queue_cap()})。"

    data = get_session_override(session_key)
    data["mode"] = mode
    data.update(opts)
    _save_override_file(session_key, data)
    bits = [f"模式={mode}"]
    if "cap" in opts:
        bits.append(f"cap={opts['cap']}")
    if "drop" in opts:
        bits.append(f"drop={opts['drop']}")
    if "debounce_ms" in opts:
        bits.append(f"debounce={opts['debounce_ms']}ms")
    return f"本会话入站队列:{' · '.join(bits)}。"


def format_queue_status_line(session_key: str) -> str:
    override = get_session_override(session_key)
    mode = get_queue_mode(session_key)
    cap = session_queue_cap(session_key)
    drop = session_drop_policy(session_key)
    debounce = collect_debounce_ms(session_key)
    src = "会话覆盖" if override.get("mode") else "全局默认"
    return (
        f"入站队列: {mode} (cap={cap}, drop={drop}, debounce={debounce}ms, {src})"
    )