operator_commands.py 3.0 KB

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