def normalize_target_input(raw: str) -> str: if not raw: return "" cleaned = raw.strip() if ":" in cleaned: prefix, rest = cleaned.split(":", 1) return f"{prefix.lower().strip()}:{rest.strip()}" return cleaned def detect_target_kind(raw: str, channel_chat_types: list[str] | None = None) -> str: trimmed = raw.strip() if trimmed.startswith("@") or trimmed.lower().startswith("user:"): return "user" if trimmed.startswith("#") or trimmed.lower().startswith("channel:"): return "group" if trimmed.lower().startswith("thread:"): return "channel" if channel_chat_types: if "direct" in channel_chat_types and "group" not in channel_chat_types: return "user" return "group" def looks_like_target_id(raw: str) -> bool: trimmed = raw.strip() core = trimmed for prefix in ("user:", "channel:", "group:", "thread:", "@", "#"): if core.lower().startswith(prefix): core = core[len(prefix) :].strip() break if not core: return False if core.isdigit(): return True if any("\u4e00" <= c <= "\u9fff" for c in core): return False if " " in core: return False if "-" in core and len(core) > 20: return True return False