أتمتة Triage لتذاكر الدعم: من Gmail إلى Linear بدون فوضى
مستوى القارئ: متوسط
هتكسب من المقال ده workflow عملي يقلل فرز رسائل الدعم من ساعة يوميًا إلى 10 دقائق مراجعة، بدل ما الفريق يفتح Gmail وLinear ويفضل ينسخ ويلصق.
المشكلة باختصار
لو عندك SaaS صغير بيستقبل 30 إلى 60 رسالة دعم يوميًا، اللي بيحصل فعلاً إن الرسائل المهمة بتتدفن وسط أسئلة متكررة. عميل بيقول الدفع فشل. عميل تاني بيقول زرار مش ظاهر. وثالث بيسأل عن فاتورة. الطريقة اليدوية بتفشل لأن كل رسالة محتاجة قراءة، تصنيف، أولوية، ثم إنشاء ticket.
الافتراض إن عندك Gmail للدعم، وفريق هندسي شغال على Linear، وحجم الرسائل أقل من 500 رسالة يوميًا. فوق الرقم ده هتحتاج queue حقيقي ومراقبة أفضل.
الفكرة: افصل القراءة عن القرار
ركز في النقطة دي: الأوتوميشن مش المفروض يرد على العميل ولا يقرر بدل الفريق. أفضل طريقة هنا إنه يعمل triage فقط. يعني يقرأ الرسائل غير المقروءة، يستخرج عنوانًا واضحًا، يصنفها، يمنع التكرار، ثم ينشئ issue في Linear.
مثال بسيط: رسالة عنوانها “Payment failed after upgrade” تتحول إلى ticket بعنوان “Payment failure after plan upgrade”، أولوية عالية، label اسمه billing. رسالة “Where is invoice?” ممكن تتحول إلى ticket منخفض الأولوية أو تفضل خارج Linear لو عندك support macro جاهز.
حسب توثيق Gmail API، طريقة users.messages.list ترجع id وthreadId، وبعدها تستخدم users.messages.get لجلب تفاصيل الرسالة. المصدر: Google Workspace Gmail API. وفي Linear، إنشاء issue يتم عبر mutation اسمها issueCreate وتقدر ترجع success وبيانات التذكرة. المصدر: Linear GraphQL API.
الخطوات العملية
- اعمل label في Gmail باسم
support-triageأو استخدم query مثلto:support@example.com is:unread. - هات
LINEAR_API_KEYوLINEAR_TEAM_IDمن Linear. - شغل السكربت كل 15 دقيقة من cron أو GitHub Actions.
- احفظ hash لكل رسالة عشان تمنع إنشاء نفس ticket مرتين.
- ابدأ بقواعد بسيطة قبل أي AI: كلمات الدفع تروح billing، كلمات login تروح auth، وكلمات slow تروح performance.
import hashlib
import os
import requests
LINEAR_API_KEY = os.environ["LINEAR_API_KEY"]
LINEAR_TEAM_ID = os.environ["LINEAR_TEAM_ID"]
LINEAR_URL = "https://api.linear.app/graphql"
RULES = [
("billing", 2, ["payment", "invoice", "card", "charge"]),
("auth", 2, ["login", "password", "2fa", "otp"]),
("performance", 3, ["slow", "timeout", "latency", "loading"]),
]
def classify(subject, body):
text = f"{subject} {body}".lower()
for label, priority, words in RULES:
if any(word in text for word in words):
return label, priority
return "support", 4
def create_linear_issue(subject, body, sender):
label, priority = classify(subject, body)
digest = hashlib.sha256(f"{sender}:{subject}".encode()).hexdigest()[:12]
mutation = """
mutation IssueCreate($input: IssueCreateInput!) {
issueCreate(input: $input) {
success
issue { id title url }
}
}
"""
payload = {
"query": mutation,
"variables": {
"input": {
"teamId": LINEAR_TEAM_ID,
"title": f"[{label}] {subject[:90]}",
"description": f"From: {sender}\n\nHash: {digest}\n\n{body[:2500]}",
"priority": priority,
}
},
}
r = requests.post(
LINEAR_URL,
json=payload,
headers={"Authorization": LINEAR_API_KEY, "Content-Type": "application/json"},
timeout=20,
)
r.raise_for_status()
return r.json()