operator_commands.py 2.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. """Deterministic parsing for Feishu advertising control commands."""
  2. from __future__ import annotations
  3. import re
  4. from dataclasses import dataclass
  5. ACTION_DAY_PAUSE = "DAY_PAUSE"
  6. ACTION_STOP = "STOP"
  7. ACTION_RESUME = "RESUME"
  8. ACTION_STATUS = "STATUS"
  9. ACTION_CONFIRM = "CONFIRM"
  10. ACTION_CANCEL = "CANCEL"
  11. WRITE_ACTIONS = {ACTION_DAY_PAUSE, ACTION_STOP, ACTION_RESUME}
  12. _COMMAND_ID_RE = re.compile(r"\bcmd_[0-9A-Za-z_-]+\b", re.IGNORECASE)
  13. _ACCOUNT_ID_RE = re.compile(r"(?<!\d)(\d{7,12})(?!\d)")
  14. @dataclass(frozen=True)
  15. class ParsedCommand:
  16. action: str
  17. scope_type: str = "ACCOUNTS"
  18. account_ids: tuple[int, ...] = ()
  19. command_id: str | None = None
  20. def normalize_text(raw: str) -> str:
  21. text = str(raw or "").strip()
  22. translations = str.maketrans(
  23. {
  24. ",": ",",
  25. "、": ",",
  26. ";": ",",
  27. ";": ",",
  28. ":": " ",
  29. ":": " ",
  30. "\t": " ",
  31. "\r": " ",
  32. "\n": " ",
  33. }
  34. )
  35. return re.sub(r"\s+", " ", text.translate(translations)).strip()
  36. def parse_command(raw: str) -> ParsedCommand | None:
  37. text = normalize_text(raw)
  38. if not text:
  39. return None
  40. lowered = text.lower()
  41. command_match = _COMMAND_ID_RE.search(text)
  42. if lowered.startswith("确认"):
  43. if not command_match:
  44. raise ValueError("确认命令缺少 command_id")
  45. return ParsedCommand(
  46. action=ACTION_CONFIRM,
  47. command_id=command_match.group(0).lower(),
  48. )
  49. if lowered.startswith("取消"):
  50. if not command_match:
  51. raise ValueError("取消命令缺少 command_id")
  52. return ParsedCommand(
  53. action=ACTION_CANCEL,
  54. command_id=command_match.group(0).lower(),
  55. )
  56. if "查看暂停状态" in text or "查询暂停状态" in text:
  57. return ParsedCommand(action=ACTION_STATUS, scope_type="ALL")
  58. if text.startswith(("停止", "持续停止", "永久停止")):
  59. action = ACTION_STOP
  60. elif text.startswith(("暂停", "今天暂停", "暂停到明天", "今天不投")):
  61. action = ACTION_DAY_PAUSE
  62. elif text.startswith(("恢复", "开启", "继续投放")):
  63. action = ACTION_RESUME
  64. else:
  65. return None
  66. if "全部" in text or "所有" in text:
  67. return ParsedCommand(action=action, scope_type="ALL")
  68. account_ids = tuple(
  69. dict.fromkeys(int(value) for value in _ACCOUNT_ID_RE.findall(text))
  70. )
  71. if not account_ids:
  72. raise ValueError("命令中没有有效账户 ID,也没有指定“全部”")
  73. return ParsedCommand(
  74. action=action,
  75. scope_type="ACCOUNTS",
  76. account_ids=account_ids,
  77. )