چگونه یک سیستم پشتیبانی هوش مصنوعی بسازیم که بهطور خودکار با Next.js و Jev باگها را به GitHub هدایت کند
هر وبسایتی بازخورد دریافت میکند و بیشتر این بازخوردها در نهایت در جایی نامناسب ثبت میشوند. بازدیدی دکمهای خراب را پیدا میکند و به شما ایمیل میزند. شخص دیگری در شبکههای اجتماعی نظری درباره صفحهای میگذارد که بارگذاری نمیشود
هر وبسایتی بازخورد دریافت میکند، و بیشتر آنها در جاهای ناجوری سر درمیآورند. یک بازدیدکننده دکمهای خراب را پیدا میکند و به شما ایمیل میزند. شخص دیگری در شبکههای اجتماعی نظری درباره صفحهای میگذارد که روی گوشیاش بارگذاری نمیشود. نفر سوم فرم تماس شما را با ایدهای برای یک قابلیت پر میکند، و آن پیام در صندوق ورودی شما بین یک خبرنامه و یک رسید گم میشود.
وقتی بالاخره مینشینید تا مشکلات را برطرف کنید، گزارشهای باگ در سه جای مختلف پخش شدهاند. نیمی از آنها جزئیاتی ندارند و آنهایی هم که راه2شان به گیتهاب (GitHub) باز میشود، بهصورت دستی کپی شدهاند، گاهی اوقات در حالی که ایمیل بازدیدکننده هنوز در یک ایشو (Issue) عمومی چسبانده شده است.
من میخواستم چیز بهتری برای پروژههای شخصی خودم داشته باشم، بنابراین آن را ساختم. IssueRelay به هر وبسایت ریاکتی (React) یک ابزارک (ویجت) پشتیبانی کوچک میدهد که در آن بازدیدکنندگان میتوانند سؤالی بپرسند، باگی را گزارش کنند، یا قابلیتی را پیشنهاد دهند. هر گزارش ابتدا در پایگاه داده خودتان ذخیره میشود. سپس یک مدل هوش مصنوعی به نام Jev آن را دستهبندی میکند، مجموعهای از قوانین ساده در کد تعیین میکند که این گزارش به کجا برود، و شما آن را در یک داشبورد خصوصی بررسی میکنید.
وقتی تأیید میکنید که یک گزارش واقعاً یک باگ است، IssueRelay یک ایشو تمیز و مرتب در گیتهاب برای آن ایجاد میکند، در حالی که اطلاعات خصوصی بازدیدکننده حذف شده است. وقتی بعداً آن ایشو را در گیتهاب ببندید (Close کنید)، تیکت پشتیبانی نیز بسته خواهد شد.
در این آموزش، یاد خواهید گرفت که کل این سیستم چگونه کار میکند، از ویجت موجود در مرورگر گرفته تا وبهوکی (Webhook) که گیتهاب و داشبورد را با هم همگامسازی میکند. همچنین خواهید دید که چگونه میتوانید نسخه خودتان را در حدود ۱۵ دقیقه مستقر (Deploy) کنید.
پروژه IssueRelay بهصورت متنباز (Open Source) در گیتهاب در آدرس andrewbaisden/issuerelay قرار دارد، ویجت آن روی npm با نام @issuerelay/widget منتشر شده است، و همین حالا روی وبسایت نمونهکار (Portfolio) من در محیط عملیاتی (Production) در حال اجراست.
من تمام کدهای این پروژه را در این مقاله قرار نمیدهم. مخزن (Repository) شامل تمامی فایلها است و راهنمای راهاندازی، نصب را گامبهگام توضیح میدهد. در عوض، قطعات کوچک کدی که حامل ایدههای مهم هستند را به شما نشان میدهم، توضیح میدهم هرکدام چه کاری انجام میدهند، و آنچه را که در طول ساخت، تست و استقرار آن آموختهام با شما به اشتراک میگذارم.
Table of Contents
- فهرست مطالب
- پیشنیازها
- چگونه یک سیستم پشتیبانی هوش مصنوعی میتواند به هر وبسایتی کمک کند
- چه چیزی خواهیم ساخت
- Jev چیست؟
- پشته فناوری (Tech Stack)
- یک گزارش چگونه در سیستم سفر میکند
- چگونه IssueRelay مخصوص خود را مستقر کنید
- اجرای آن روی یک وبسایت واقعی
- تست کردن سرتاسری آن (و آنچه یاد گرفتم)
- چگونه ساخته شد: فازها و توسعه به کمک هوش مصنوعی
- انتشار ویجت در npm
- گامهای بعدی
- نتیجهگیری
Prerequisites
برای همراهی و استقرار نسخه خودتان، باید موارد زیر را داشته باشید:
- دانش کاربردی از ریاکت، نکستجیاس و تایپاسکریپت: این پلتفرم از App Router در Next.js استفاده میکند و ویجت نیز یک کامپوننت ریاکت است.
- Node.js 24 و pnpm: برای اجرای پروژه بهصورت محلی (Local) یا استفاده از دستوری که اپلیکیشن گیتهاب شما را میسازد، نصب شده باشند.
- یک حساب کاربری گیتهاب: به همراه مخزن وبسایت یا اپلیکیشنی که میخواهید ویجت را در آن نصب کنید. باگهای تأییدشده در آنجا به عنوان ایشو ثبت میشوند.
- یک حساب کاربری Vercel: طرح رایگان (Hobby) کافی است. شما یک پایگاه داده Neon PostgreSQL از طریق بازارچه Vercel اضافه خواهید کرد و Neon نیز طرح رایگان دارد.
- یک حساب کاربری TypeSafe: در آدرس typesafe.ai برای Jev، یعنی مدل هوش مصنوعی که گزارشها را تریاژ (دستهبندی و اولویتبندی) میکند. قبل از اینکه هر گزارشی بتواند به یک ایشو در گیتهاب تبدیل شود، به یک کلید API از کنسول TypeSafe نیاز دارید.
- یک وبسایت ریاکت: جایی که بتوانید یک کامپوننت به آن اضافه کنید. یک سایت Next.js سادهترین مکان برای شروع است.
- اختیاری: یک حساب کاربری Resend: اگر میخواهید ایمیلهای مربوط به حساب کاربری مانند بازنشانی رمز عبور را داشته باشید.
برای همراهی با این آموزش نیازی نیست متخصص هوش مصنوعی باشید. Jev از طریق یک SDK کوچک و تایپشده استفاده میشود، و بیشتر کارهای جذاب مربوط به مهندسی وب معمولی است: پایگاههای داده، اعتبارسنجی، احراز هویت و وبهوکها.
چگونه یک سیستم پشتیبانی هوش مصنوعی میتواند به هر وبسایتی کمک کند
یک سیستم پشتیبانی به نظر میرسد چیزی است که فقط شرکتهای بزرگ به آن نیاز دارند، اما مشکلی که آن را حل میکند در تقریباً هر وبسایتی دیده میشود:
- سایتهای نمونهکار پیامهایی از سوی استخدامکنندگان، سؤالاتی درباره پروژهها، و گزارشهایی درباره صفحاتی که در یک مرورگر خاص از کار میافتند، دریافت میکنند.
- محصولات SaaS گزارشهای باگ را به همراه سؤالات مربوط به صورتحساب و درخواستهای قابلیت دریافت میکنند، و هرکدام به شخص یا فرآیند متفاوتی نیاز دارند.
- سایتهای مستندسازی گزارشهایی با مضمون «این مثال کار نمیکند» دریافت میکنند که در واقع باگهای خود محصول هستند.
- پروژههای متنباز کاربرانی دارند که خودشان یک ایشو در گیتهاب باز نمیکنند اما با کمال میل روی یک دکمه در وبسایت کلیک میکنند.
- سایتهای مشتریان که برای شخص دیگری ساختهاید، بازخوردهایی دریافت میکنند که مشتری چند روز بعد بدون هیچ جزئیاتی برای شما فوروارد میکند.
یک سیستم خوب به شما مکانی واحد میدهد که در آن تمام گزارشها جمعآوری میشوند، هر گزارش را حتی در صورت از کار افتادن سایر سرویسها ایمن نگه میدارد، و گزارشها را دستهبندی میکند تا بتوانید وقت خود را روی مواردی بگذارید که واقعاً اهمیت دارند.
بخش هوش مصنوعی در دستهبندی کمک میکند، اما هرگز نباید همهچیز را کنترل کند. یک مدل ممکن است با اطمینان کامل اشتباه کند و ایجاد یک مشکل (Issue) عمومی در گیتهاب بر اساس یک حدس و گمان چیزی نیست که شما بخواهید. بنابراین، IssueRelay در تمام طول مسیر از یک قانون ساده پیروی میکند: هوش مصنوعی پیشنهاد میدهد، انسان تأیید میکند و کد قوانین را اجرا میکند.
آنچه خواهیم ساخت
این مسیر طیشده توسط یک گزارش واحد در IssueRelay است:
بازدیدکننده ویجت را در سایت شما باز کرده و موضوعی را انتخاب میکند:
آنها مشکل را شرح میدهند و بهصورت اختیاری میتوانند نام و ایمیل خود را وارد کنند تا بتوانید پیگیری کنید:
ویجت گزارش را به پلتفرم IssueRelay شما ارسال میکند؛ پلتفرم آن را ذخیره کرده و با یک کد پیگیری پشتیبانی پاسخ میدهد که بازدیدکننده بعداً میتواند از آن استفاده کند:
از آنجا به بعد، گزارش در داشبورد شما به یک تیکت تبدیل میشود. Jev آن را دستهبندی میکند، شما آن را بررسی میکنید، و اگر یک باگ واقعی باشد، با یک کلیک یک Issue در مخزن گیتهاب شما ایجاد میشود.
لیست کامل ویژگیها به شرح زیر است:
- یک ویجت قابل جاسازی (Embeddable): به عنوان یک کامپوننت ریاکت داخل یک Shadow DOM ساخته شده است، بنابراین نیازی به تنظیمات CSS ندارد و هرگز با استایلهای سایت شما تداخل پیدا نمیکند. این ویجت در Next.js App Router و تحت یک خطمشی سختگیرانه امنیت محتوا (CSP) کار میکند.
- ثبت ایمن و پایدار: هر گزارش قبل از اجرای هر کار دیگری در PostgreSQL ذخیره میشود، همراه با محافظت در برابر تکرار، محدودیت نرخ (Rate Limit) برای هر پروژه، و لیستی از آدرسهای مجاز سایت.
- تریاژ محدود شده توسط هوش مصنوعی: Jev تنها از روی متن، نوع و میزان شدت مشکل را پیشنهاد میکند.
- یک داشبورد خصوصی: با فیلترها، تاریخچه دستهبندی، و تصمیمات بررسی انسانی که بهطور جداگانه از خروجی هوش مصنوعی ذخیره میشوند.
- ارتقاء دقیق به گیتهاب: مشکلات تنها پس از تأیید پیشنمایش توسط مالک، توسط یک اپلیکیشن گیتهاب ایجاد میشوند و اطلاعات تماس هرگز از IssueRelay خارج نمیشوند.
- همگامسازی دوطرفه: بستن یا باز کردن مجدد مشکل در گیتهاب، تیکت را از طریق وبهوکهای امضاشده بهروزرسانی میکند.
- میزبانی شخصی (Self-hosting): یک دکمه استقرار، یک صفحه راهاندازی اولیه و یک صفحه تنظیمات، اجرای نسخه شخصی خودتان را بدون نیاز به دستکاری پایگاه داده امکانپذیر میکند.
جیف (Jev) چیست؟
Jev مدلی از شرکت TypeSafe است که برای آنچه TypeSafe آن را وظایف «سیستم یک» (System One) مینامد ساخته شده است: قضاوتهای سریع و محدود در مقایسه با نوشتن طولانی و باز.
بهجای اینکه از یک مدل بخواهید یک پاراگراف بنویسد و سپس سعی کنید آن را تجزیه (Parse) کنید، شما مقداری وضعیت (State) و مجموعهای از سوالات را به Jev میدهید و هر سوال دارای لیست مشخصی از پاسخهای ممکن است. Jev برای هر سوال یک پاسخ را انتخاب میکند و احتمال اختصاصیافته به هر گزینه را برمیگرداند.
این ساختار دقیقاً همان چیزی است که تریاژ پشتیبانی به آن نیاز دارد. یک تیکت یا یک باگ است، یا یک سوال، درخواست ویژگی، مشکل صورتحساب، یا اسپم. شدت آن نیز کم، متوسط، زیاد یا بحرانی است. هیچ متن اضافهای برای ابداع توسط مدل وجود ندارد، هیچ تزریق پرامپی (Prompt Injection) نمیتواند باعث شود که عنوانی برای مشکل بنویسد و هیچ متن آزادی برای پاکسازی پس از آن وجود ندارد. خروجی یک برچسب و یک عدد است و کد شما میتواند هر دو را بررسی کند.
همچنین ارزان و سریع است. در زمان نگارش این مقاله، TypeSafe قیمت Jev را ۴۲ دلار به ازای هر میلیارد توکن ورودی اعلام کرده است و یک پیام پشتیبانی تنها چند ده توکن مصرف میکند. ادغام IssueRelay فقط پیام بازدیدکننده و موضوعی را که انتخاب کردهاند به Jev ارسال میکند. این سیستم هرگز نامها، آدرسهای ایمیل، شناسههای تیکت یا هر چیز دیگری که هویت فرد را مشخص کند، ارسال نمیکند.
پشته فنی (Tech Stack)
IssueRelay با یک پشته مدرن TypeScript ساخته شده است، و این همان پشتهای است که من برای پروژههای شخصی خودم استفاده میکنم. اگر سایر مقالات مرا خوانده باشید، بسیاری از بخشهای آن برایتان آشنا به نظر میرسد:
- Next.js 16 (مسیراب اپلیکیشن یا App Router) و React 19 برای پلتفرم و داشبورد
- TypeScript سختگیرانه در همه جا، به همراه Zod برای بررسی هر ورودی که از مرز اعتماد عبور میکند: درخواستهای API عمومی، متغیرهای محیطی، خروجی هوش مصنوعی و payloads وبهوک گیتهاب
- PostgreSQL همراه با Drizzle ORM و مایگریشنهای SQL بررسیشده
- Better Auth برای حسابهای کاربری داشبورد
- SDK رسمی TypeSafe برای Jev و Octokit برای اپلیکیشن گیتهاب
- Vitest، کتابخانه تست ریاکت (React Testing Library) و Playwright برای تستها، و Biome برای لینت کردن و فرمتبندی
- فضاهای کاری pnpm برای نگهداری همهچیز در یک مونوپپ (Monorepo)
- Vercel، Neon و Resend در محیط پروداکشن (Production)
مونوپپ به پکیجهای کوچکی تقسیم شده است که هر کدام وظیفه مشخصی دارند:
| پکیج | مسئولیت |
|---|---|
| apps/web | پلتفرم: API عمومی تیکت، داشبورد، راهاندازی و وبهوک گیتهاب |
| packages/widget | ویجت مرورگر منتشر شده در npm. این پکیج هرگز کدهای سمت سرور را ایمپورت نمیکند. |
| packages/support-contracts | ساختارهای درخواست و پاسخ که توسط ویجت و API به اشتراک گذاشته شدهاند |
| packages/db | شمای Drizzle، مایگریشنها و هر کوئری پایگاه داده |
| packages/ai | آدابتور Jev، سرویس تریاژ و خطمشی مسیریابی |
| packages/github | کلاینت اپلیکیشن گیتهاب، پیشنویسهای ایشو، دروازه حریم خصوصی و مدیریت وبهوک |
| packages/auth | راهاندازی Better Auth، نشستها (sessions) و بررسیهای عضویت در فضای کاری |
این مرزها بسیار مهمتر از آن چیزی هستند که در نگاه اول به نظر میرسند. کامپوننتهای ریاکت هرگز مستقیماً با گیتهاب، Jev یا پایگاه داده ارتباط برقرار نمیکنند. کد مرورگر هرگز شامل هیچ اطلاعات محرمانهای نیست. پکیج هوش مصنوعی نمیتواند پایگاه داده را ایمپورت (Import) کند.
رعایت دقیق این خطوط قرمز باعث شد که تست کردن سیستم و استدلال درباره آن بسیار آسانتر شود و به همین دلیل است که میتوان ویجت را بدون همراه داشتن هیچگونه کد سمتی روی npm منتشر کرد.
یک گزارش چگونه در سیستم حرکت میکند
بیایید مسیر یک گزارش را از مرورگر بازدیدکننده تا بسته شدن یک ایشو در گیتهاب دنبال کنیم.
مرحله ۱: ویجت
ویجت یک کامپوننت معمولی ریاکت است که آن را از طریق npm نصب میکنید:
npm install @issuerelay/widget
سپس آن را یک بار رندر میکنید، برای مثال از درون یک کامپوننت کلاینت در layout ریشه خود:
"use client";
import {
HttpSupportSubmissionClient,
SupportWidget,
} from "@issuerelay/widget";
const submissionClient = new HttpSupportSubmissionClient({
apiBaseUrl: "https://your-issuerelay.vercel.app",
});
export function Support() {
return (
<SupportWidget
projectKey="pk_your_project_key"
submissionClient={submissionClient}
theme="system"
position="bottom-right"
/>
);
}
کامپوننت HttpSupportSubmissionClient بخشی است که با پلتفرم شما گفتگو میکند. این بخش هر گزارش را بدون کوکی یا اعتبارنامه به IssueRelay API شما ارسال میکند و به هر گزارش یک شناسه ارسال (submission ID) میدهد تا در صورت بروز خطای شبکه، تلاش مجدد (retry) منجر به ایجاد تیکت دوم نشود.
SupportWidget همان دکمه و پنلی است که بازدیدکنندگان شما میبینند. پارامتر projectKey به پلتفرم میگوید که گزارش متعلق به کدام پروژه است. این یک شناسه عمومی است، نه یک رمز عبور؛ بنابراین قرار دادن آن در کد وبسایت کاملاً امن است. محافظت واقعی در سمت سرور انجام میشود که فقط گزارشها را از آدرسهای سایت فهرستشده برای آن پروژه میپذیرد.
عبارت "use client" به این دلیل قرار داده شده است که کلاینت ارسال در مرورگر ایجاد میشود. در روتور اپلیکیشن نکستجیاس (Next.js App Router)، شما ویجت را در کامپوننت کلاینت کوچک خودتان قرار میدهید و آن کامپوننت را از لایوت (layout) خود رندر میکنید.
در پشت صحنه، ویجت در یک Shadow DOM با استایلهای باندلشدهی خودش رندر میشود؛ بنابراین سایت شما نیازی به Tailwind یا ایمپورت CSS ندارد و استایلهای شما نمیتوانند به طور تصادفی ظاهر آن را تغییر دهند.
همچنین نیازی نیست این کد را به صورت دستی بنویسید. صفحه تنظیمات پروژه در IssueRelay دقیقاً همین قطعه کد را با آدرس پلتفرم و کلید پروژه شما که از قبل پر شده است، نمایش میدهد.
مرحله ۲: اول ذخیره کن، بعداً فکر کن
وقتی گزارش به API میرسد، اولین کاری که IssueRelay انجام میدهد ذخیره کردن آن است. نه دستهبندی میکند، نه آن را به جایی میفرستد، بلکه صرفاً آن را درون یک تراکنش در PostgreSQL ذخیره میکند.
این مهمترین تصمیم طراحی در کل سیستم است. ارائهدهندگان هوش مصنوعی ممکن است قطعی داشته باشند. گیتهاب ممکن است قطعی داشته باشد. اگر پلتفرم قبل از ذخیره گزارش با Jev تماس میگرفت و زمانپایان (timeout) در Jev رخ میداد، پیام بازدیدکننده گم میشد و آنها هرگز متوجه نمیشدند.
بنابراین قانون ساده است: یک گزارش تنها پس از ذخیرهسازی ایمن پذیرفته میشود و خرابی در هر مرحله بعدی هرگز نمیتواند آن را پاک کند. اگر Jev از دسترس خارج باشد، تیکت در داشبورد منتظر میماند تا دوباره تریاژ را اجرا کنید.
قبل از ذخیرهسازی، API چند مورد را بررسی میکند:
- بدنه درخواست با قرارداد مشترک Zod مطابقت دارد، بنابراین ورودی نامعتبر با یک خطای واضح رد میشود.
- کلید پروژه وجود دارد و مبدا (origin) درخواست یکی از آدرسهای مجاز سایت برای آن پروژه است.
- پروژه از حد مجاز نرخ درخواست (rate limit) عبور نکرده است.
- شناسه ارسال (submission ID) قبلاً استفاده نشده است. ارسال تکراری به جای ایجاد رکورد تکراری، ارجاع به تیکت اصلی را برمیگرداند.
مرحله ۳: تریاژ با Jev
هنگامی که یک تیکت ذخیره شد، سرویس تریاژ از Jev میخواهد که آن را دستهبندی کند. این هسته آدابتور Jev است که از فایل packages/ai/src/jev-classifier.ts گرفته شده است (کمی برای صرفهجویی در فضا پیرایش شده است):
const response = await this.client.systemOne({
state: {
message: input.message,
category_hint: input.categoryHint ?? null,
},
questions: {
ticket_type: choice(
"What kind of support ticket is this? The visitor-selected category hint is a weak signal, not ground truth: judge from the message content.",
{
question: "The visitor asks how something works or what something is.",
bug: "Something is broken, errors, or behaves incorrectly.",
feature_request: "The visitor requests new functionality or an improvement.",
spam: "Unsolicited advertising, scams, or irrelevant bulk content.",
// ...account, billing, feedback, and other
},
),
severity: choice("How urgent is this ticket?", {
low: "Minor inconvenience, cosmetic issue, or general question.",
medium: "Broken functionality with a workaround, or a routine request.",
high: "Major functionality unavailable, no workaround, time-sensitive.",
critical: "Security breach, data loss, privacy exposure, or billing harm.",
}),
},
});
systemOne فراخوانی SDK نوعامن (TypeSafe) برای سؤالات محدود است. شیء state هر چیزی است که Jev مجاز به دیدن آن است: پیام و موضوعی که بازدیدکننده انتخاب کرده است.
توجه کنید چه چیزی وجود ندارد. هیچ نام، ایمیل و شناسه تیکتی وجود ندارد، زیرا هیچکدام در دستهبندی کمکی نمیکنند و همه آنها دادههای خصوصی هستند که پلتفرم شما را ترک میکنند.
هر گزینه یک سؤال و پاسخهای احتمالی آن را تعریف میکند. توضیحات کنار هر برچسب (label) به Jev میگوید که آن برچسب چه معنایی دارد. سؤال نوع تیکت همچنین به Jev میگوید که موضوع انتخابی بازدیدکننده را به عنوان یک راهنمایی ضعیف در نظر بگیرد، زیرا افراد اغلب برای یک سؤال، گزینه «گزارش باگ» را انتخاب میکنند یا برای چیزی که آشکارا خراب است، «پرسیدن یک سؤال» را برمیگزینند.
آنچه برمیگردد به طور خودکار مورد اعتماد قرار نمیگیرد. آدابتور پاسخ را با یک شمای Zod اعتبارسنجی میکند، بررسی میکند که هر دو پاسخ از برچسبهای لیستهای مجاز باشند و از احتمالی (probability) که Jev به برچسب نوع انتخابی داده است به عنوان امتیاز اطمینان (confidence score) استفاده میکند.
اگر آن احتمال گم شده باشد یا خارج از بازه 0 تا 1 باشد، نتیجه رد میشود و به جای اختصاص یک عدد ساختگی، تیکت در حالت بررسی باقی میماند.
مرحله ۴: کد تصمیمگیری میکند
Jev یک نوع و یک میزان شدت را پیشنهاد میکند. این هوش مصنوعی نیست که تصمیم میگیرد یک تیکت به کجا برود یا آیا به یک ایشو در گیتهاب تبدیل شود یا خیر. این وظیفه توابع ساده در فایل packages/ai/src/policy.ts است:
export function routeForType(type: TicketType): TicketRoute {
switch (type) {
case "bug":
return "engineering";
case "feature_request":
return "product";
case "spam":
return "ignore";
default:
return "support";
}
}
export function evaluateGitHubEscalation(input: {
type: TicketType;
route: TicketRoute;
confidence: number;
}): EscalationEvaluation {
const reasons: string[] = [];
if (input.type !== "bug") reasons.push(`type is ${input.type}, not bug`);
if (input.route !== "engineering") reasons.push(`route is ${input.route}, not engineering`);
if (!(input.confidence >= GITHUB_ESCALATION_CONFIDENCE_THRESHOLD)) {
reasons.push(`confidence ${input.confidence} is below ${GITHUB_ESCALATION_CONFIDENCE_THRESHOLD}`);
}
return { eligible: reasons.length === 0, reasons };
}
routeForType هر نوع تیکت را به یک صف نگاشت میکند. باگها به تیم مهندسی میروند، درخواستهای ویژگی به محصول میروند، اسپمها ایزوله میشوند و بقیه موارد به پشتیبانی میروند. از آنجا که این یک دستور switch معمولی است، میتوانید آن را بخوانید، تست کنید و بدون دست زدن به هوش مصنوعی تغییر دهید.
evaluateGitHubEscalation تعیین میکند که آیا یک تیکت اصلا مجاز به تبدیل شدن به یک مسئله (Issue) در گیتهاب هست یا خیر. این تیکت باید یک باگ باشد، در صف مهندسی قرار داشته باشد و میزان اطمینان (Confidence) آن حداقل 0.9 باشد. به جای بازگرداندن یک مقدار سادهی درست یا نادرست (true/false)، این تابع دلایل رد شدن تیکت را جمعآوری میکند تا داشبورد آنها را نمایش دهد و شما همیشه بدانید که چرا دکمهی Create GitHub issue وجود ندارد.
آستانهی 0.9 در یک فایل پیکربندی قرار دارد و در کنار آن توضیحی نوشته شده که نشان میدهد این یک نقطهی شروع کالیبرهنشده است، نه یک دقت اندازهگیریشده. من میخواستم این موضوع در کد کاملاً شفاف باشد: امتیاز مدل برابر با 0.99 به این معنا نیست که مدل در 99 درصد مواقع حق دارد.
مرحله ۵: بررسی انسانی در داشبورد
هر تیکت وارد یک داشبورد خصوصی میشود. صفحهی پروژهها نشان میدهد که چه تعداد تیکت در هر وضعیت وجود دارد:
هر پروژه دارای فهرستی از تیکتها با فیلترهایی برای وضعیت، مسیر (route)، نوع، شدت و مرجع (reference) است:
با باز کردن یک تیکت، گزارش بازدیدکننده، دستهبندی فعلی هوش مصنوعی، تاریخچه کامل دستهبندی و گاهشماری (timeline) از تمام اتفاقات رخداده نمایش داده میشود. شما میتوانید تریاژ را دوباره اجرا کنید، تیکت را حلوفصل کنید، یا تصمیم بررسی را ثبت کنید که مسیر، وضعیت یا پیشنهاد گیتهاب را تغییر دهد.
یک جزئیات کوچک که برای من اهمیت دارد: تصمیمات انسانی در جدول مخصوص به خودشان، همراه با نام نویسنده و یک دلیل اجباری ذخیره میشوند. تاریخچهی هوش مصنوعی هرگز بازنویسی نمیشود. اگر نظر Jev را لغو کنید، باز هم میتوانید دقیقاً ببینید که Jev چه چیزی گفته و در چه زمانی؛ این موضوع زمانی که میخواهید بدانید مدل واقعاً چقدر خوب کار میکند، بسیار مهم است.
این داشبورد توسط Better Auth محافظت میشود و هر عملیات خواندن و نوشتنی به یک فضای کاری (workspace) محدود شده است. عملیات تغییر (Mutations) نیازمند یک درخواست هممبدا (same origin) هستند و فقط مالکان فضای کاری میتوانند روی گیتهاب منتشر کنند.
مرحله ۶: از گزارش باگ تا مسئلهای در گیتهاب (GitHub Issue)
هنگامی که یک تیکت از قوانین عبور میکند، داشبورد پیشنمایشی از دقیقاً همان مسئلهای را که ایجاد خواهد شد، نمایش میدهد:
به آن تصویر صفحه (screenshot) با دقت نگاه کنید. بازدیدکننده نام و ایمیل خود را گذاشته است و هر دو در داشبورد بالا قابل مشاهده هستند، اما هیچکدام در پیشنمایش مسئله ظاهر نمیشوند.
این یک اتفاق تصادفی نیست. پیش از ایجاد هر مسئلهای، گزارش از یک دروازهی حریم خصوصی در مسیر packages/github/src/privacy.ts عبور میکند که به دنبال آدرسهای ایمیل، شماره تلفنها، شماره کارتها، کلیدهای خصوصی، توکنهای API، توکنهای وب جیسون (JWT) و پسوردهای تخصیصیافته میگردد. همچنین گزارش را با اطلاعات تماسی که بازدیدکننده ارسال کرده مطابقت میدهد؛ بنابراین عبارتهایی مانند «سلام، Sam Visitor هستم» نمیتوانند نامی را به یک مسئلهی عمومی درز دهند. اگر موردی پیدا شود، پیشنمایش مسدود شده و هیچچیز منتشر نمیشود.
وقتی روی Create GitHub issue کلیک میکنید، چند محافظ امنیتی دیگر نیز اجرا میشوند:
- یک اپلیکیشن گیتهاب (GitHub App)، نه یک توکن شخصی: این اپلیکیشن فقط روی مخازن (repositories) انتخابی شما نصب میشود و تنها دسترسیهای لازم برای نوشتن مسائل و خواندن فراداده (metadata) را دارد و بس.
- ابتدا ثبت ادعا (Claim)، سپس ایجاد: تیکت قبل از فراخوانی گیتهاب در پایگاه داده به عنوان «در حال ایجاد» علامتگذاری میشود، بنابراین دو کلیک همزمان هرگز نمیتوانند دو مسئلهی تکراری ایجاد کنند.
- یک نشانگر پنهان: بدنه هر مسئله با یک کامنت HTML مبهم که به تیکت متصل است، به پایان میرسد. اگر درخواستی دچار زمانپایانی (timeout) شود و نتیجه نامعلوم باشد، IssueRelay پیش از تلاش مجدد، مخزن را با استفاده از اپلیکیشن خودش جستجو میکند تا دقیقاً همان نشانگر را پیدا کند. این ابزار هرگز کورکورانه مسئلهای را که ممکن است از قبل ایجاد کرده باشد، دوباره تلاش نمیکند.
در اینجا یک مسئلهی واقعی را میبینید که IssueRelay از روی گزارش یک بازدیدکننده روی مخزن عمومی نمونهکارهای من ایجاد کرده است. این مسئله توسط ربات اپلیکیشن ایجاد شده، برچسب bug خورده و حاوی گزارش و دستهبندی هوش مصنوعی است، اما هیچ اطلاعات تماسی در آن وجود ندارد:
مرحله ۷: همگامسازی گیتهاب و داشبورد
بخش نهایی چرخه را کامل میکند. هنگامی که مسئله را در گیتهاب ببندید، گیتهاب یک وبهوک (webhook) به IssueRelay ارسال میکند و تیکت به وضعیت حلشده منتقل میشود. اگر مسئله را دوباره باز کنید، تیکت به صف برمیگردد.
یک نقطهی پایانی وبهوک (webhook endpoint) به طور ذاتی عمومی است، بنابراین اولین کاری که انجام میدهد این است که ثابت کند درخواست واقعاً از گیتهاب آمده است. این تابع اعتبارسنجی از مسیر packages/github/src/webhook-auth.ts است:
export function verifyWebhookSignature(input: {
secret: string;
rawBody: Uint8Array;
signatureHeader: string | null;
}): boolean {
const { secret, rawBody, signatureHeader } = input;
if (!secret || !signatureHeader?.startsWith("sha256=")) {
return false;
}
const hex = signatureHeader.slice("sha256=".length);
if (!/^[0-9a-f]{64}$/.test(hex)) return false;
const expected = createHmac("sha256", secret).update(rawBody).digest();
const actual = Buffer.from(hex, "hex");
if (expected.length !== actual.length) return false;
return timingSafeEqual(expected, actual);
}
گیتهاب هر ارسالی را با یک راز (secret) که فقط گیتهاب و پلتفرم شما از آن مطلع هستند امضا میکند و امضا را در هدر X-Hub-Signature-256 میفرستد. این تابع امضای HMAC SHA256 خودش را روی بایتهای خام درخواست محاسبه کرده و آن دو را با هم مقایسه میکند.
دو نکته وجود دارد که اشتباه کردن در آنها آسان است. اول اینکه، امضا باید روی بایتهای دقیقی که گیتهاب فرستاده محاسبه شود، پیش از هرگونه تجزیه (parsing) جیسون؛ زیرا تجزیه و فرمتبندی مجدد، بایتها را تغییر میدهد. دوم اینکه، مقایسه با استفاده از timingSafeEqual انجام میشود که صرفنظر از اینکه بایت اول یا آخر متفاوت باشد، دقیقاً به همان اندازه زمان میبرد؛ بنابراین یک مهاجم نمیتواند با اندازهگیری زمانهای پاسخدهی، امضا را یک کاراکتر در واحد زمان حدس بزند. همچنین این تابع برای هر نوع خطایی مقدار false را برمیگرداند بدون اینکه بگوید کدام خطا رخ داده است، بنابراین هیچ اطلاعاتی درز نمیکند.
پس از بررسی امضا، IssueRelay شناسه هر ارسال (delivery ID) را ذخیره میکند، بنابراین ارسالهای تکراری نادیده گرفته میشوند. این ابزار فقط مسئلهای را بهروزرسانی میکند که متعلق به نصب اپلیکیشن و مخزن منطبق باشد و رویدادها را به همان ترتیبی که در گیتهاب رخ دادهاند اعمال میکند، نه به ترتیبی که دریافت شدهاند.
چگونه IssueRelay اختصاصی خود را مستقر (Deploy) کنید
شما میتوانید IssueRelay خود را روی Vercel و Neon در حدود ۱۵ دقیقه راهاندازی کنید. راهنمای کامل، از جمله عیبیابی، در فایل docs/SELF_HOSTING.md موجود است. در اینجا نسخه کوتاه آن آورده شده است.
مرحله ۱: استقرار (Deploy)
مسیر پیشنهادی، دکمهی Deploy with Vercel در فایل README است. این دکمه مخزن را در حساب گیتهاب شما کپی میکند، یک پایگاه داده Neon اضافه میکند و سه راز تصادفی درخواست میکند.
اولین بیلد (build) به عمد با خطا مواجه میشود؛ زیرا صفحه کلون Vercel هیچ تنظیماتی برای دایرکتوری ریشه (Root Directory) ندارد، بنابراین شما باید Root Directory را در تنظیمات پروژه روی apps/web قرار دهید و دوباره استقراره را انجام دهید. سپس بیلد تولیدی (production build) تمام جدولهای پایگاه داده را برای شما ایجاد میکند.
این راهنما همچنین یک مسیر «انشعاب و واردسازی» (Fork and Import) را توصیف میکند که بهروزرسانیهای آینده را با یک کلیک ممکن میسازد، اما آن مسیر هنوز به طور کامل از ابتدا تا انتها آزمایش نشده است.
مرحله ۲: اجرای صفحهی راهاندازی (Setup Page)
صفحهی /setup را در سایت جدید خود باز کنید. این صفحه فقط زمانی کار میکند که پایگاه داده هیچ حسابی نداشته باشد و شما SETUP_TOKEN ایجاد شده در طول استقرار را وارد کنید؛ بنابراین هیچکس که ابتدا URL شما را پیدا کند نمیتواند پلتفرم شما را به نام خود ثبت کند. این صفحه حساب مالک و اولین پروژه شما را ایجاد میکند، سپس کلید ویجت و کد آمادهی ویجت را برای کپی کردن نمایش میدهد.
فیلد آدرس سایت با http://localhost:3000 شروع میشود که مکانی است که اپلیکیشن Next.js روی رایانه شما اجرا میشود. آدرس زندهی خود را نیز اضافه کنید، مانند https://my-site.vercel.app یا دامنه شخصی خودتان. اگر فراموش کنید، ویجت با ادب به بازدیدکنندگان خواهد گفت: «ما نتوانستیم پیام شما را ارسال کنیم»، بنابراین این اولین چیزی است که هنگام نرسیدن گزارش باید بررسی کنید.
مرحله ۳: ایجاد اپلیکیشن گیتهاب (GitHub App)
تنظیم دستی یک اپلیکیشن گیتهاب ممکن است چند اشتباه ساده به همراه داشته باشد. بدترین آنها فراموش کردن اشتراک در رویداد Issues است که خودم در طول آزمایش مرتکب آن شدم. بنابراین IssueRelay شامل دستوری است که اپلیکیشن را از روی یک مانیفست برای شما ایجاد میکند:
pnpm github:create-app --platform https://your-issuerelay.vercel.app
این دستور گیتهاب را در مرورگر شما با همهی فیلدهای از پیشپرشده باز میکند: یک اپلیکیشن خصوصی با مجوز نوشتن مسائل و خواندن فراداده، مشترک در رویداد Issues، و با وبهوکی که به پلتفرم شما اشاره میکند.
شما روی Create GitHub app کلیک میکنید، گیتهاب به یک سرور محلی موقت که توسط دستور راهاندازی شده بازمیگردد، و دستور مذکور شناسه اپلیکیشن (App ID)، کلید خصوصی و راز وبهوک را در فایلی که توسط git نادیده گرفته میشود (git ignored) مینویسد. این اطلاعات هرگز در ترمینال شما چاپ نمیشوند. سپس ترمینال مرحلهی بعدی راهاندازی را فهرست میکند.
مرحله ۴: افزودن کلیدها و استقرار مجدد
سه مقدار اپلیکیشن گیتهاب و TYPESAFE_API_KEY خود را به متغیرهای محیطی پروژه Vercel اضافه کنید و سپس دوباره استقرار را انجام دهید. وجود Jev برای مسائل گیتهاب الزامی است: بدون آن، گزارشها همچنان به داشبورد شما میرسند، اما هیچکدام نمیتوانند به یک مسئله تبدیل شوند.
مرحله ۵: اتصال مخزن خود
اپلیکیشن را روی مخزن وبسایتی که ویجت در آن قرار خواهد گرفت نصب کنید، سپس صفحهی Settings پروژهی خود را در داشبورد باز کرده و آن را متصل کنید. این صفحه از گیتهاب میپرسد که کدام شناسه نصب و مخزن متعلق به آن نام است، بنابراین یک خطای تایپی نمیتواند مخزن اشتباه را متصل کند.
مرحله ۶: نصب ویجت
ویجت را با استفاده از کدی که در صفحه تنظیمات قرار دارد روی وبسایت خود نصب کنید و اولین گزارش خود را بفرستید.
برای اینکه بعداً نسخه خود را بهروز نگه دارید، تغییرات را از مخزن اصلی (repository) دریافت (pull) کنید. این راهنما مرحله تکبارهای لازم برای نسخههای ساختهشده با دکمه Deploy را پوشش میدهد، زیرا آن نسخهها فورتهای (forks) گیتهاب نیستند.
اجرا روی یک وبسایت واقعی
نسخه نمایشی یک چیز است، اما من میخواستم از IssueRelay بهطور واقعی استفاده کنم؛ بنابراین این ویجت اکنون روی نمونهکار (portfolio) من در andrewbaisden.com در حال اجراست:
طراحی وبسایت احتمالاً تغییر خواهد کرد، بنابراین اگر در آینده این مقاله را میخوانید، نسخههای قبلی را میتوانید در گیتهاب من پیدا کنید.
نصب آن چند نکته به من آموخت. نمونهکار من برای تستهایش هنوز روی ریاکت ۱۸ بود، در حالی که App Router از قبل با ریاکت ۱۹ رندر میشد. بنابراین ابتدا آن را به ریاکت ۱۹ ارتقا دادم و مطمئن شدم قبل از اضافه کردن ویجت، تمام تستهای موجود پاس میشوند. این ویجت با تمهای روشن و تاریک سایت مطابقت دارد، در گوشه پایین سمت راست قرار میگیرد و دارای تست واحد (unit test) و تست مرورگر مخصوص به خود در مخزن نمونهکار است.
سپس آن را درست مانند یک بازدیدکننده تست کردم. سه گزارش واقعی از سایت زنده ارسال کردم: یک سوال، یک باگ و یک درخواست قابلیت (feature request). جیف (Jev) هر سه را همانطور که میخواستم دستهبندی کرد، با امتیازهایی بین ۰.۹۵ و ۱.۰۰، و پالیسی آنها را به بخش پشتیبانی، مهندسی و محصول هدایت کرد. باگ مربوطه در مخزن عمومی نمونهکار من به مسئله شماره ۳ تبدیل شد که همان مسئلهای است که در اسکرینشات قبلی نشان داده شده است. من نام و ایمیل خودم را همراه با آن گزارش وارد کرده بودم، و هیچکدام در مسئله عمومی ظاهر نمیشوند.
تست سرتاسری یا End-to-End (و آنچه یاد گرفتم)
من پروژهای نمیخواستم که فقط روی سیستم خودم کار کند؛ بنابراین تست کردن به جای اینکه برای انتهای کار گذاشته شود، بخشی از هر فاز بود.
مجموعه تست دارای لایههای مختلفی است:
- تستهای واحد برای ویجت، قرارداد API، پالیسی هوش مصنوعی، دروازه حریم خصوصی، صفحه راهاندازی و موارد دیگر. تعداد آنها ۱۸۰ عدد است و هیچکدام به پایگاه داده نیاز ندارند.
- تستهای یکپارچهسازی پایگاه داده که در برابر یک پایگاه داده آزمایشی جداگانه از نوع PostgreSQL اجرا میشوند، از جمله تستهای همروندی (concurrency) که ثابت میکنند دو کلیک نمیتوانند دو مسئله (issue) یکسان در گیتهاب ایجاد کنند.
- تستهای مرورگر با Playwright که سرورهای خود را روی پورتهای مجزا راهاندازی میکنند، با یک پایگاه داده جداگانه که برای هر بار اجرا از نو ساخته میشود، به طوری که یک تست هرگز نمیتواند دادههای واقعی را لمس کند. یکی از آن سرورها در برابر یک پایگاه داده خالی اجرا میشود تا صفحه راهاندازی اجرای اول را تست کند.
- بررسی پکیج که فایل فشرده (tarball) دقیق npm را میسازد و آن را در یک اپلیکیشن Vite با یک خطمشی امنیت محتوای سختگیرانه (Strict CSP) و در یک اپلیکیشن Next.js نصب میکند، که هر دو بیرون از مونورپو (monorepo) قرار دارند، و سپس در هر کدام یک گزارش ارسال میکند.
- یک تست سفری زنده با ۲۰ بررسی در برابر یک اپلیکیشن واقعی گیتهاب و یک مخزن دورریختنی: ارسال یک گزارش، تریاژ کردن آن، پیشنمایش آن، ایجاد مسئله، بررسی اینکه هیچ داده خصوصی منتشر نشده است، بستن مسئله در گیتهاب و انتظار برای وبهوک (webhook)، باز کردن مجدد آن، و بررسی گاهشمار (timeline).
من آن سفر زنده را سه بار اجرا کردم: ابتدا روی سیستم محلی خودم از طریق یک تونل، سپس روی محیط پروداکشن (production)، و در نهایت روی یک نسخه کاملاً تازه که با پیروی صرف از راهنمای راهاندازی، آن را دیپلوی کرده بودم. هر سه مورد، ۲۰ از ۲۰ را با موفقیت پاس کردند.
با این حال، جالبتر از موارد موفق، مشکلاتی است که هر مرحله آشکار کرد:
- رویداد Issues به راحتی فراموش میشود: اولین باری که یک اپلیکیشن گیتهاب را با دست ایجاد کردم، هیچ اشتراک رویدادی نداشت، بنابراین گیتهاب هرگز به IssueRelay نگفته بود که مسائل چه زمانی بسته شدهاند. این اشتباه دلیل وجود دستور create-app است.
- بازدیدکنندگان نام خود را ذکر میکنند: گزارشی مانند «سارا اینجا هستم، صفحه خراب است» از طرف بازدیدکنندهای به نام سارا، نام او را در یک مسئله عمومی قرار میداد. دروازه حریم خصوصی اکنون هر گزارش را با جزئیات تماسی که همراه آن آمده است مقایسه میکند.
- Zod و خطمشی سختگیرانه CSP در مرورگر با هم سازگار نیستند: کتابخانه Zod 4 به طور خلاصه تست میکند که آیا میتواند از new Function استفاده کند یا خیر، و سایتهایی با خطمشی امنیت محتوای سختگیرانه آن را به عنوان یک تخلف گزارش میکنند. من Zod را از ویجت حذف کردم و به جای آن چکهای اعتبارسنجی کوچکی نوشتم، همراه با تستی که ثابت میکند آنها با اسکیماهای Zod سرور روی ۲۷۰ ترکیب فرم توافق دارند.
- جریان کلون Vercel گزینه Root Directory ندارد و Vercel فریمورک را فقط یک بار انتخاب میکند: دیپلوی تازه من دو بار با شکست مواجه شد: یک بار به این دلیل که Vercel ریشه مخزن را بیلد کرد، و بار دیگر به این دلیل که فریمورک همچنان روی «Other» تنظیم شده بود. این مخزن اکنون Next را در vercel.json پین میکند و راهنما در مورد شکست اول هشدار میدهد.
- کپیهای دکمه Deploy فورت (fork) نیستند: یک دستور ساده git pull از مخزن اصلی از ادغام امتناع میکند، بنابراین راهنما اکنون دارای یک دستور تکباره برای اتصال یک کپی به مخزن اصلی است.
- مسائل گیتهاب به Jev نیاز دارند: من در ابتدا Jev را به عنوان اختیاری فهرست کرده بودم. بررسی دقیق راهنما نشان داد که بدون آن، هیچ گزارش واقعی نمیتواند به آستانه اطمینان برسد. راهنما و صفحه تنظیمات اکنون این موضوع را به وضوح بیان میکنند.
- سر و صدای لاگ (Log noise) اهمیت دارد: هر اتصال به پایگاه داده یک هشدار SSL در سطح خطا (error level) ثبت میکرد که باعث میشد یک استقرار سالم خراب به نظر برسد. راهحل این بود که حالت SSLی را که درایور از قبل استفاده میکرد به صراحت مشخص کنیم، بنابراین هشدار ناپدید شد در حالی که بررسیهای گواهی دقیقاً به همان شکل باقی ماندند.
درسی که بیشتر از همه در ذهنم ماند: استقرار از روی مستندات خودتان، کلمه به کلمه، باگهایی را پیدا میکند که هیچ تستی قادر به پیدا کردن1 آن نیست. تکتک مشکلات استقرار بالا برای تستهای خودکار نامرئی بودند و لحظهای که یک فرد واقعی راهنما را دنبال کرد، کاملاً آشکار شدند.
نحوه ساخت: فازها و توسعه هوش مصنوعی کمکی
پروژه IssueRelay در فازهای کوچکی ساخته شد و هر فاز قبل از شروع فاز بعدی با یک سند تحویل کتبی به پایان رسید:
| فاز | نتیجه |
|---|---|
| 0 | تعریف محصول، معماری، تصمیمگیریها، امنیت و برنامههای تست |
| 1 و 2 | پایه و اساس مونورپو، مدل دامنه، اسکیمای PostgreSQL و دادههای اولیه (seed data) |
| 3 و 4 | ویجت، سایت نمایشی و API عمومی تیکتها |
| 5 و 6 | تریاژ هوش مصنوعی با Jev و داشبورد اپراتور |
| 7 و 8 | تشدید (escalation) تأییدشده گیتهاب و همگامسازی امضاشده وبهوک |
| 9 | اعتبارسنجی زنده کل سفر در یک مخزن دورریختنی |
| 10 | سختسازی تولید (Production hardening) |
| ۱۱ و ۱۲ | اعتبارسنجی و انتشار ویجت در npm |
| استقرار | Vercel، Neon و Resend در محیط تولید |
| 13 | نصب ویجت روی نمونهکار (پورتفولیو) من |
| ۱۴ و ۱۵ | استفاده داخلی از محصول (در حال اجرا) |
| 16 | میزبانی شخصی: دکمه استقرار، صفحه راهاندازی، تنظیمات پروژه و دستور مانیفست برنامه |
تنظیمات توسعهدهنده من
من بیشتر کارها را در ترمینال انجام دادم. پیکربندی من شامل موارد زیر است:
- Ghostty به عنوان ترمینال من، که در حال اجرای Claude Code ،Codex و OpenCode است
- Cursor به عنوان ویرایشگر من
- برنامههای دسکتاپ بومی برای ChatGPT، Claude و OpenCode
مدل اصلی من برای ساخت IssueRelay مدل Claude Opus 5.5 در Claude Code بود. برای بررسیهای کد (Code reviews) و بررسی یک فاز قبل از تأیید نهایی، از مدلهای دیگر از جمله GPT-6 Sol و Grok، به همراه چندین مدل پیشرو و رایگان دیگر استفاده کردم.
یک مدل دوم که همان کد را با دیدی تازه بررسی کرد، مشکلات واقعی را کشف کرد. برای مثال، بررسی فازهای ۷ و ۸ توسط Grok منجر به کشف ۱۵ مورد شد. هفت مورد معتبر بودند، از جمله یک مشکل رقابتی (Race condition) در ادعای ایجاد مسئله و شناسههای قابل حدس برای مسائل، و هر هفت مورد قبل از اینکه ادامه دهم برطرف شدند. سه مورد دیگر تا حدی معتبر بودند و پنج مورد با ذکر دلایل کتبی به تعویق افتادند.
مدلهای تازه منتشر شده Sonnet 5.5 از Anthropic و GPT-6.1 Sol از OpenAI در این پروژه استفاده نشدند.
چگونه پرامپتهای بهتر کیفیت کدbase را بهبود بخشیدند
بزرگترین بهبود در کیفیت، ناشی از یک مدل هوش مصنوعی باهوشتر نبود؛ بلکه حاصل ارائه دستورالعملهای بهتر و ساختاری مناسبتر برای کار کردن به مدل بود. مواردی که نتیجه دادند عبارتند از:
- یک فاز در هر زمان: هر پرامپت دقیقا یک فاز را با نتیجهای مشخص درخواست میکرد و هوش مصنوعی اجازه نداشت فاز بعدی را شروع کند تا زمانی که من آن را تأیید کنم. تغییرات کوچک و قابل بررسی، بسیار راحتتر از یک قابلیت غولپیکر بازبینی میشوند.
- ارائه یک برنامه قبل از نوشتن هرگونه کد: برای فازهای بزرگتر، ابتدا یک برنامه درخواست میکردم («یک برنامه ایجاد کن و پس از تأیید من، آن را اجرا کن»). خواندن یک برنامه دو دقیقه طول میکشد، اما اصلاح یک پیادهسازی اشتباه ممکن است یک بعدازظهر کامل زمان ببرد.
- قوانینی که در مخزن (Repository) زندگی میکنند: یک فایل AGENTS.md حاوی قوانین پروژه است؛ مانند «یک تیکت تأییدشده را قبل از فراخوانیهای هوش مصنوعی خارجی یا گیتهاب ذخیره کن»، «هرگز اطلاعات تماس را در گیتهاب منتشر نکن» و «کورکورانه ایجاد یک مسئله مبهم در گیتهاب را دوباره تلاش نکن». هر جلسه هوش مصنوعی این فایل را میخواند، بنابراین قوانین به یاد آوردن من برای تکرار آنها وابسته نیستند.
- گزارشدهی صادقانه: دستورالعملها میگویند که هرگز نباید یک بررسی اجرانشده را به عنوان موفق گزارش کرد، و در هر تحویل، دستورات اجرا شده و نتایج واقعی آنها (از جمله خطاها) ثبت میشود.
- شرایط واضح برای کامیت کردن: پرامپتهایی مانند «وقتی تستها پاس شدند و هیچ مشکل دیگری وجود نداشت، کامیت و پوش کن» به این معنا بود که مجموعه کامل تستها قبل از رسیدن هر چیزی به شاخه اصلی (main) اجرا میشود.
- درخواست مدرک، نه وعده: به جای اینکه بپرسم «آیا میزبانی شخصی کار میکند؟»، از هوش مصنوعی خواستم با دنبال کردن راهنما روی یک استقرار تازه، آن را راستیآزمایی کند. همین یک درخواست، هفت مشکل مستندات و پیکربندی را آشکار کرد.
- بازخورد دادن از استفاده واقعی: وقتی خودم یک سایت تستی را مستقر کردم و تمام چیزهایی را که مرا گیج کرده بود یادداشت کردم، آن یادداشتها مستقیماً به راهنما، صفحه راهاندازی و صفحه تنظیمات بازگردانده شدند.
انتشار ویجت در npm
ویجت تنها بخش از IssueRelay است که به عنوان @issuerelay/widget منتشر میشود. سایر بخشها به صورت خصوصی در داخل monorepo باقی میمانند.
من نمیخواستم چیزی را منتشر کنم که فقط در فضای کاری خودم کار کند، بنابراین بررسی انتشار، دقیقاً همان فایل فشردهای (tarball) را که npm دریافت خواهد کرد، میسازد و آن را بازرسی میکند.
این بسته باید فقط شامل پنج فایل باشد. نباید به پکیجهای خصوصی، توابع داخلی Node، متغیرهای محیطی یا هر چیزی که شبیه به کلید (Key) باشد ارجاع دهد. سپس باید در دو برنامه کاملاً جدید در خارج از مخزن نصب شده و کار کند؛ یکی از آنها تحت یک سیاست امنیت محتوای سختگیرانه (CSP)، با صفر تخطی از سیاست.
نسخهها از طریق GitHub Actions با قابلیت انتشار معتبر (Trusted publishing) و منشأ (Provenance) در npm منتشر میشوند، بنابراین هیچ توکن بلندمدتی از npm وجود ندارد که نشت کند.
نتیجه یک پکیج حدوداً ۱۰ کیلوبایتی فشردهشده است که نیازی به تنظیمات CSS ندارد و فقط به React و React Hook Form وابسته است. فایل راهنمای ویجت (README) تمام ویژگیها (Props) را مستند کرده است.
قدم بعدی چیست
پروژه IssueRelay برای میزبانی شخصی کامل است و من هر روز از آن روی نمونهکار خودم استفاده میکنم. برخی مواردی که دوست دارم در آینده بررسی کنم:
- نسخهای میزبانیشده از IssueRelay، به طوری که بتوانید ثبتنام کنید و بدون نیاز به استقرار چیزی توسط خودتان، ویجت را اضافه کنید
- آزمایش مسیر «فرک و وارد کردن» (Fork and Import) از ابتدا تا انتها تا بتواند به عنوان روش پیشنهادی برای استقرار تبدیل شود
- ایدههای موجود در نقشه راه (Roadmap)، مانند تشخیص گزارشهای تکراری، پیوند دادن چندین گزارش به یک مسئله، اعلانها (Notifications) و همگامسازی نظرات گیتهاب
نتیجهگیری
در این آموزش، دیددید چگونه یک سیستم پشتیبانی مبتنی بر هوش مصنوعی بسازید که بازخوردهای پراکنده وبسایت را به تیکتهای بررسیشده تبدیل کرده و باگهای تأییدشده را به گیتهاب هدایت میکند. در طول مسیر، یاد گرفتید که چگونه:
- یک ویجت React قابل جاسازی بسازید که روی هر سایتی بدون نیاز به تنظیمات CSS یا تداخل استایلها کار کند
- پیش از فراخوانی هر سرویس خارجی، هر گزارش را ذخیره کنید تا قطعی سرویسدهنده هرگز باعث از دست رفتن دادهها نشود
- از Jev برای دستهبندی محدود و اعتبارسنجیشده استفاده کنید که به جای متن آزاد، برچسبها و احتمالات را برمیگرداند
- تصمیمات مربوط به مسیریابی و انتشار را در کد ساده و قابل تست، با حضور انسان در حلقه (human in the loop) نگه دارید
- مسائل (Issues) گیتهاب را با استفاده از یک اپلیکیشن گیتهاب، یک دروازه حریم خصوصی و یک نشانگر که از ایجاد موارد تکراری جلوگیری میکند، به طور ایمن ایجاد کنید
- گیتهاب و داشبورد خود را با وبهوکهای امضا شده و تأیید شده همگام نگه دارید
- نسخه خود را روی Vercel و Neon مستقر کنید و آن را از ابتدا تا انتها، از جمله در برابر مستندات خودتان، آزمایش کنید
بهترین راه برای درک IssueRelay امتحان کردن آن است. میتوانید کد را در گیتهاب بررسی کنید، نسخه خود را با راهنمای میزبانی شخصی مستقر کنید و ویجت را با دستور npm install @issuerelay/widget از npm به سایت خود اضافه کنید. اگر این ابزار به شما کمک کرد، ثبت ستاره (Star) در مخزن مایه دلگرمی ماست.
نظر مهندس بهمن آبادی: موفقیت این سیستم در تفکیک هوش مصنوعی از تصمیمگیری نهایی است؛ جایی که LLM فقط تریاژ را انجام میدهد، اما اعمال تغییرات و ساخت ایشو در گیتهاب کاملاً بر عهده قوانین قطعی کد است. این معماری نهتنها جلوی خطاهای مدل را میگیرد، بلکه با پنهانسازی امن اطلاعات شخصی، یک پایپلاین پشتیبانی کاملاً خودکار و قابل اتکا میسازد.