SSD Nodes Learn 🎉 VPS من $4.99/شهر
الأدلة Matt Connorبقلم Matt Connor · آخر تحديث في 2026-08-07

كيف تستضيف وكيل مراجعة PR بالذكاء الاصطناعي على VPS

شغّل مراجع PR على VPS تملكه باستخدام مشغّل GitHub Actions مستضاف ذاتياً، وتعليقات مضمّنة، وفلاتر للمسارات والحجم، واعرف التكلفة لكل PR.

ما الذي يفعله وكيل مراجعة PR مستضاف ذاتياً

وكيل مراجعة PR مستضاف ذاتياً هو برنامج صغير يعمل على خادم تملكه. يقرأ الفرق في طلب السحب (PR)، ويرسل الأسطر التي تغيّرت فقط إلى نموذج. تُنشر النتيجة على شكل تعليقات مراجعة مضمّنة. لا ينفّذ أبداً عملية checkout لفرعك، ولا يقرأ أي ملف لم يلمسه طلب السحب. بيانات الاعتماد الوحيدة التي يحتفظ بها هي مفتاح API واحد للنموذج، ورمز واحد يمكنه إضافة التعليقات ولا يملك أي صلاحية أخرى.

يمكن للنموذج قراءة الفرق. هذا الجزء محلول. المهم هو وجهة الفرق والجهة التي تحتفظ بالمفتاح. يعني روبوت المراجعة المستضاف أن كل فرق من كل مستودع خاص يغادر شبكتك، ويصل إلى سجلات طرف ثالث، ويخضع لسياسة الاحتفاظ الخاصة به. على VPS تملكه، ينتقل الفرق من GitHub إلى خادمك ثم إلى API النموذج، ويمكنك قراءة أسطر التعليمات البرمجية الأربعين التي تحدد ما يُرسل.

ما تحتاج إليه قبل البدء

  • خادم VPS يعمل بنظام Ubuntu 24.04، ومسجّل عليه بالفعل مشغّل GitHub Actions مستضاف ذاتياً في المستودع. أضف إليه التصنيف pr-review عند تسجيله، لأن سير العمل أدناه يحدّد المشغّل باستخدام هذا التصنيف.
  • مفتاح API من Anthropic من Claude Console.
  • مستودع تتحكم في من يمكنه فتح طلب سحب فيه. المستودع الخاص هو الحالة الأسهل. يوضّح قسم التفرع أدناه الحالة العامة، لكن الإجابة فيها أقل اطمئناناً.

ثبّت reviewer على VPS

تعمل خدمة runner باستخدام الحساب غير المميّز الذي أنشأته عند تشغيل ./svc.sh install. ثبّت reviewer باستخدام الحساب نفسه حتى تتمكن المهمة من تشغيله دون sudo. استبدل runner أدناه باسم حسابك.

sudo apt update && sudo apt install -y gh python3-venv
sudo install -d -m 755 -o runner -g runner /opt/pr-review
sudo -u runner python3 -m venv /opt/pr-review/venv
sudo -u runner /opt/pr-review/venv/bin/pip install anthropic
gh --version

يطبع gh --version القيمة gh version 2.45.0 على Ubuntu 24.04 اعتباراً من August 2026. يدعم أي إصدار يبدأ من 2.20 الراية --input المستخدمة أدناه. تعني رسالة Command 'gh' not found أن مكوّن universe غير مفعّل، لذا شغّل sudo add-apt-repository universe ثم حاول مرة أخرى.

مكان وجود المفتاح والرمز المميز

لكل سر مدة صلاحية مختلفة. لا يُخزَّن أيٌّ منهما في المستودع.

ANTHROPIC_API_KEY هو سر خاص بالمستودع. اضبطه من Settings، ثم Secrets and variables، ثم Actions. يشفّره GitHub ويحقنه في بيئة الخطوة أثناء التشغيل. لا يكون هذا السر ملفاً على القرص، ولا يظهر في سجل git.

يعمل GITHUB_TOKEN بطريقة مختلفة. تنشئ Actions رمزاً مميزاً جديداً لكل مهمة وتتلفه عند انتهاء المهمة. تحدد كتلة permissions: في سير العمل ما يمكن لهذا الرمز فعله. وهنا تُطبَّق فعلياً قاعدة أقل الصلاحيات:

permissions:
  contents: read
  pull-requests: write

يمكن لهذا الرمز نشر مراجعة. لكنه لا يستطيع دفع commit، أو دمج فرع، أو تعديل ملف سير عمل، أو الوصول إلى مستودع آخر. الوكيل الذي يستطيع التعليق هو مراجع. أما الوكيل الذي يستطيع الدفع فهو منفّذ للـcommit، ولم يوافق أحد على ذلك. تعامل مع مفتاح النموذج بالحذر نفسه، لأنه ينفق الأموال من حسابك. يشرح إبقاء الأسرار بعيدة عن متناول وكيل الذكاء الاصطناعي مزيداً من هذا النوع من المشكلات.

تستبدل Actions سلسلة السر المطابقة تماماً بـ *** في سجلات المهام. وهي تطابق السلسلة الأصلية حرفياً فقط. لذلك يظهر المفتاح بوضوح إذا شفّرته بترميز base64، أو قسمته على سطرين، أو طبعته حرفاً واحداً في كل مرة. لا تضف خطوة تصحيح تطبع محتويات البيئة.

لماذا لا يرى طلب السحب من مستودع متشعب مفتاح API الخاص بك

قاعدة GitHub واضحة: باستثناء GITHUB_TOKEN، لا تُمرَّر الأسرار إلى المشغّل عندما يُشغَّل سير العمل من مستودع متشعب. لذلك يبدأ تشغيل pull_request من مستودع متشعب النص البرمجي من دون ANTHROPIC_API_KEY، ويفشل أول استدعاء لـ API مع invalid x-api-key.

الحل المغري هو تغيير المشغّل إلى pull_request_target، إذ يعمل ذلك ضمن سياق المستودع الأساسي ويحصل على الأسرار. لا تفعل ذلك هنا. تنص إرشادات الأمان الخاصة بـ GitHub على أن سير العمل هذا «يتمتع بامتيازات، ما يعني أنه يشارك ذاكرة التخزين المؤقت للفرع الرئيسي مع مشغلات سير العمل الأخرى ذات الامتيازات، وقد يملك صلاحية الكتابة إلى المستودع والوصول إلى الأسرار المشار إليها»، وأن النتيجة «يمكن استغلالها للاستيلاء على مستودع».

وتتحدث الإرشادات نفسها بوضوح عن المشغّل: «ينبغي ألّا تُستخدم المشغّلات المستضافة ذاتياً تقريباً أبداً مع المستودعات العامة على GitHub، لأن أي مستخدم يمكنه فتح طلبات سحب إلى المستودع واختراق البيئة».

يفرض ذلك خيارين في التصميم. تتضمن المهمة حارساً يضمن تشغيلها فقط على الفروع التي تُدفع إلى مستودعك الخاص. ولا يتضمن سير العمل أي خطوة actions/checkout على الإطلاق. لا يملك العامل الفرع على القرص، لذلك لا يكون طلب السحب الضار سوى نص يُرسل إلى نموذج. ولا يمكنه تشغيل نص برمجي للبناء على VPS الخاص بك، لأن أي شيء على VPS الخاص بك لا يشغّله مطلقاً.

جلب الفرق، لا المستودع

يجلب طلب واحد الفرق الكامل كنص عادي.

export GH_TOKEN=your_token   # in the workflow this comes from secrets.GITHUB_TOKEN
gh api /repos/OWNER/REPO/pulls/42 -H "Accept: application/vnd.github.diff"

نوع الوسائط Accept: application/vnd.github.diff هو الذي يحوّل الاستجابة من كائن JSON يصف طلب السحب إلى الفرق الموحّد نفسه، ويطبع gh api محتوى الاستجابة دون تغيير. يجب أن يبدأ السطر الأول الذي تراه بـ diff --git a/. تعني gh: Not Found (HTTP 404) أن الرمز المميّز لا يستطيع الوصول إلى المستودع. في الرمز المميّز الشخصي ذي الأذونات الدقيقة، يعني ذلك غالباً أن إذن Pull requests لم يُفعّل.

التصفية قبل إنفاق أي رمز

هذا القسم هو ما يحدد الفرق بين روبوت يقرأ الناس ملاحظاته وروبوت يكتمونه. يعمل كل مرشح أدناه قبل أن يرى النموذج بايتاً واحداً.

  • مرشحات المسارات. استبعد ملفات القفل، والأدلة التي تحتوي على تبعيات مورّدة، والحزم المصغّرة، والتعليمات البرمجية المُولّدة. تعليق من النموذج على package-lock.json مجرد ضوضاء، وغالباً ما تحتوي هذه الملفات على معظم بايتات الفرق.
  • حد أقصى للحجم. عند تجاوز هذا الحد، تخطَّ المراجعة واخرج بحالة نجاح. سيؤدي تغيير يشمل 4,000 سطر إلى سطر صريح واحد يفيد بأنه كبير جداً بحيث يتعذر مراجعته تلقائياً، بدلاً من ستين تخميناً.
  • حد أدنى للخطورة وحد أقصى لعدد التعليقات. أبلغ عن النتائج العالية والمتوسطة الخطورة، وبحد أقصى عشر نتائج، بدءاً بالأعلى خطورة. لا أحد يقرأ التعليق الحادي عشر.

البرنامج النصي

احفظ هذا باسم /opt/pr-review/review.py. يقرأ إعداداته من البيئة، لذلك يمكن لسير العمل تغيير النماذج من دون تعديل الشيفرة.

#!/usr/bin/env python3
"""Review only the changed lines of one pull request."""
import json
import os
import subprocess
import sys

import anthropic

REPO = os.environ["GITHUB_REPOSITORY"]
PR = os.environ["PR_NUMBER"]
MODEL = os.environ.get("REVIEW_MODEL", "claude-haiku-4-5-20251001")
MAX_DIFF_BYTES = int(os.environ.get("MAX_DIFF_BYTES", "120000"))
MIN_SEVERITY = os.environ.get("MIN_SEVERITY", "medium")
MAX_COMMENTS = 10
RANK = {"low": 0, "medium": 1, "high": 2}
SKIP = ("package-lock.json", "poetry.lock", "/vendor/", "/node_modules/", ".min.js")

raw_diff = subprocess.run(
    ["gh", "api", f"/repos/{REPO}/pulls/{PR}",
     "-H", "Accept: application/vnd.github.diff"],
    check=True, capture_output=True, text=True,
).stdout

تقسيم الفرق حسب الملف هو ما يتيح تصفية المسارات. وترقيم كل سطر هو ما يجعل تعليقات المراجعة تظهر في مواضعها. لا تقبل GitHub التعليق المضمّن إلا على سطر يمثل جزءاً من الفرق، لذلك يجب أن يذكر النموذج رقم سطر حقيقياً. تزويده بالأرقام يعني أنّه يستطيع نسخ أحدها بدلاً من اختلاق رقم.

def per_file(diff_text):
    """Split a unified diff into one string per file."""
    sections, current = [], []
    for line in diff_text.splitlines():
        if line.startswith("diff --git ") and current:
            sections.append("\n".join(current))
            current = []
        current.append(line)
    if current:
        sections.append("\n".join(current))
    return sections


def annotate(section):
    """Prefix every line that exists in the new file with its line number."""
    out, n, in_hunk = [], 0, False
    for line in section.splitlines():
        if line.startswith("@@"):
            n = int(line.split("+")[1].split(",")[0].split(" ")[0])
            in_hunk = True
            out.append(line)
        elif not in_hunk or line.startswith(("-", "\\")):
            out.append(line)
        else:
            out.append(f"{n}\t{line}")
            n += 1
    return "\n".join(out)


kept = [s for s in per_file(raw_diff)
        if not any(p in s.split("\n", 1)[0] for p in SKIP)]
payload = "\n".join(annotate(s) for s in kept)

if not payload.strip():
    print("every changed file was filtered out")
    raise SystemExit(0)
if len(payload) > MAX_DIFF_BYTES:
    print(f"diff is {len(payload)} bytes, over the {MAX_DIFF_BYTES} cap")
    raise SystemExit(0)

يحمل عنوان المقطع أرقام الأسطر. يوضح @@ -12,7 +12,9 @@ أنّ مقطع الملف الجديد يبدأ عند السطر 12، لذلك يبدأ العداد من هناك ويتقدم في الأسطر المضافة وغير المتغيرة فقط. تمر الأسطر المحذوفة من دون ترقيم، لأنها غير موجودة في الملف الجديد. يتجاوز الشرط الخاص بالأسطر التي تبدأ بشرطة مائلة عكسية العلامة التي يكتبها git عند نهاية الملف للإشارة إلى عدم وجود سطر جديد، إذ إن احتسابها كان سيزيح كل رقم لاحق بمقدار واحد.

يستخدم كلا الخروجين الحالة 0، وليس 1. يجب أن يظهر طلب السحب المصفّى أو الكبير جداً على أنّه فحص ناجح. أما الفحص الفاشل الذي لا يستطيع الإنسان اتخاذ إجراء بشأنه فيُتجاهل، وبمجرد تجاهل فحص واحد تُتجاهل جميع الفحوص.

SYSTEM = (
    "You review one pull request diff. Every line that exists in the new file is "
    "prefixed with its line number and a tab character. "
    "Report only defects you can see in the lines shown: a crash, a resource leak, "
    "a security mistake, a wrong boundary condition, a broken contract with code "
    "that is visible in this diff. Do not comment on style, naming or formatting. "
    "Do not guess about code you cannot see. Leave out anything you are not "
    "certain about. An empty findings list is a normal and common answer. "
    'Reply with JSON only, in this shape: {"findings": [{"path": "src/app.py", '
    '"line": 42, "severity": "high", "comment": "what is wrong, then why"}]} '
    "Every line number must be one you can see in the left column of that file."
)

client = anthropic.Anthropic()
message = client.messages.create(
    model=MODEL,
    max_tokens=2000,
    system=SYSTEM,
    messages=[{"role": "user", "content": payload}],
)
print(f"stop={message.stop_reason} in={message.usage.input_tokens} "
      f"out={message.usage.output_tokens}", file=sys.stderr)

text = message.content[0].text
findings = json.loads(text[text.find("{"):text.rfind("}") + 1])["findings"]
findings = [f for f in findings if RANK.get(f["severity"], 0) >= RANK[MIN_SEVERITY]]
findings.sort(key=lambda f: -RANK.get(f["severity"], 0))
del findings[MAX_COMMENTS:]

if not findings:
    print("nothing above the severity threshold; posting no comment")
    raise SystemExit(0)

review = {
    "event": "COMMENT",
    "body": f"Automated review of the changed lines. {len(findings)} finding(s).",
    "comments": [
        {"path": f["path"].removeprefix("b/"), "line": f["line"], "side": "RIGHT",
         "body": f"**{f['severity']}** {f['comment']}"}
        for f in findings
    ],
}
subprocess.run(
    ["gh", "api", "-X", "POST", f"/repos/{REPO}/pulls/{PR}/reviews", "--input", "-"],
    input=json.dumps(review), text=True, check=True,
)

هناك 3 تفاصيل أساسية في هذه الكتلة. تُقتطع JSON بين أول { وآخر }، لأن النموذج قد يغلّف إجابته أحياناً داخل كتلة شيفرة، وjson.loads لا يتعامل مع هذا التغليف. ويُجرَّد path من b/ في بدايته، لأن هذه البادئة تأتي من عنوان الفرق، بينما تريد GitHub مساراً نسبياً إلى المستودع. ويرسل --input - المراجعة كاملة في استدعاء API واحد، لذلك تصل 10 نتائج في إشعار واحد بدلاً من 10 إشعارات.

عندما لا يوجد ما يستحق الإبلاغ، لا ينشر البرنامج النصي شيئاً. فالروبوت الذي يكتب «لم تُعثر على مشكلات» في كل طلب سحب يعلّم الناس تجاوز هذه الرسالة سريعاً، ثم يتجاوزون الرسالة التي كانت مهمة.

اربطه بسير العمل

احفظ هذا باسم .github/workflows/pr-review.yml:

name: pr-review

on:
  pull_request:
    types: [opened, synchronize, reopened]
    paths-ignore:
      - '**.md'
      - 'docs/**'

permissions:
  contents: read
  pull-requests: write

concurrency:
  group: pr-review-${{ github.event.pull_request.number }}
  cancel-in-progress: true

jobs:
  review:
    if: github.event.pull_request.head.repo.full_name == github.repository && !contains(github.event.pull_request.labels.*.name, 'no-ai-review')
    runs-on: [self-hosted, linux, pr-review]
    steps:
      - name: Review the changed lines
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
          REVIEW_MODEL: claude-haiku-4-5-20251001
          MIN_SEVERITY: medium
        run: /opt/pr-review/venv/bin/python /opt/pr-review/review.py

لا يوجد GITHUB_REPOSITORY داخل كتلة env: لأن Actions يعيّنه لكل مهمة مسبقاً. وتهم مجموعة concurrency بالنسبة إلى التكلفة: من دونها، يؤدي دفع ثلاثة إصلاحات سريعة إلى فرع إلى تشغيل ثلاث عمليات مراجعة كاملة، وتدفع تكلفة العمليات الثلاث كلها. أما معها، فلا تستمر إلا العملية الأخيرة.

ينفّذ السطر if: مهمتين. يتخطى النصف الأول طلبات السحب من المستودعات المتفرعة، التي ستفشل على أي حال لعدم وجود المفتاح. أما النصف الثاني، فيوفّر لفريقك مفتاح إيقاف: أضف التصنيف no-ai-review إلى طلب سحب، ولن تُشغّل المهمة.

افتح طلب سحب وراقب ما يحدث:

gh run list --workflow=pr-review.yml --limit 3
gh run view --log
gh pr view 42 --comments

تعمل العملية بشكل صحيح إذا انتهت خلال بضع ثوانٍ وظهر nothing above the severity threshold; posting no comment في السجل. هذه هي النتيجة المتوقعة لطلب سحب صغير ونظيف.

ما تكلفة مراجعة طلب سحب مؤتمتة؟

يمثّل الـdiff الجزء الأكبر من الإدخال تقريباً، لذلك يحدد حجم الـdiff السعر. فيما يلي diff مقاسه 500 سطر مع system prompt، وقد حُسب باستخدام نقطة نهاية عدّ الرموز بدلاً من تقديره.

ChartTokens for one 500 line pull request diff, measured August 2026
The data behind this chart
[
  {
    "label": "Haiku 4.5",
    "input_tokens": "8,000",
    "output_tokens": "1,200"
  },
  {
    "label": "Sonnet 5",
    "input_tokens": "10,400",
    "output_tokens": "1,560"
  },
  {
    "label": "Opus 5",
    "input_tokens": "10,400",
    "output_tokens": "1,560"
  }
]

استهلك ذلك الـdiff 8,000 من رموز الإدخال على Haiku 4.5، و10,400 على Sonnet 5. النص نفسه، لكن العدد مختلف. تستخدم نماذج Claude ابتداءً من 4.7 أداة ترميز أحدث تنتج رموزاً أكثر بنحو 30% للإدخال نفسه، وتوثّق Anthropic ذلك في صفحة الأسعار. خذ هذا العامل في الحسبان عند مقارنة نموذج أحدث بآخر أقدم اعتماداً على سعر كل مليون رمز فقط.

الأسعار المعلنة في August 2026: يبلغ سعر Haiku 4.5 مقدار $1 لكل مليون رمز إدخال و$5 لكل مليون رمز إخراج. ويبلغ سعر Sonnet 5 مقدار $2 و$10 ضمن السعر التمهيدي الساري حتى 31 August 2026، ثم يصبح $3 و$15. أما Opus 5 فسعره $5 و$25.

ChartCost of that one review at list prices, August 2026
The data behind this chart
[
  {
    "label": "Haiku 4.5",
    "cost_per_pr_cents": 1.4,
    "cost_200_prs_usd": "2.80"
  },
  {
    "label": "Sonnet 5",
    "cost_per_pr_cents": 3.64,
    "cost_200_prs_usd": "7.28"
  },
  {
    "label": "Opus 5",
    "cost_per_pr_cents": 9.1,
    "cost_200_prs_usd": "18.20"
  }
]

تبلغ التكلفة 1.4 سنتاً لكل طلب سحب على Haiku 4.5، و9.1 سنتاً على Opus 5. يدفع فريق يدمج 200 طلب سحب شهرياً نحو $2.80 على Haiku 4.5، أو $7.28 على Sonnet 5، أو $18.20 على Opus 5. ابتداءً من 1 September 2026، اضرب صف Sonnet 5 في 1.5.

هناك عاملان يرفعان الفاتورة الفعلية فوق هذا التقدير. يراجع المشغّل synchronize كل عملية push، لذلك تكلّف branch نشطة أجرت ثماني عمليات push ثماني مراجعات، ولا تساعد قاعدة التزامن إلا عندما تصل عمليات push متقاربة زمنياً. تفترض الأرقام أيضاً أن مرشحات المسارات تعمل: يمكن لملف lock واحد غير مفلتر أن يضاعف الإدخال بمفرده.

لا يساعد prompt caching هنا. يجب أن تكون البادئة المخزنة مؤقتاً مطابقة حرفياً بين الاستدعاءات، بينما يختلف الـdiff في كل مرة. والجزء الثابت الوحيد هو system prompt، لكنه أقصر بكثير من الحد الأدنى القابل للتخزين المؤقت. للاطلاع على القاعدة العامة، راجع متى يحقق prompt caching جدواه، ولاختيار النموذج المناسب من النماذج الثلاثة أعلاه، راجع أي نموذج Claude تستخدم لكل مهمة.

قِس الـdiffs الخاصة بك قبل تفعيل ذلك

أضف هذا السطر بعد إنشاء payload، ثم شغّل البرنامج النصي يدوياً على عدة طلبات سحب من الشهر الماضي:

print(client.messages.count_tokens(
    model=MODEL, system=SYSTEM, messages=[{"role": "user", "content": payload}]
).input_tokens)

لا تشغّل نقطة نهاية العدّ النموذج، لذلك لا تستهلك رموز إدخال أو إخراج، وتستخدم أداة الترميز الخاصة بالنموذج الذي تحدده. شغّلها على عشرة طلبات سحب فعلية من مستودعك، واستخدم الوسيط بدلاً من المتوسط الحسابي حتى لا تشوّه عملية ترحيل ضخمة واحدة التقدير.

لماذا يجري إسكات روبوتات المراجعة وكيف تتجنب ذلك

هناك سلوكان يقضيان على الثقة بهذه الروبوتات، ولكل منهما إصلاح في الشفرة أعلاه.

مراجعة كل شيء دفعة واحدة. الروبوت الذي يترك أربعين تعليقاً لن يقرأ أحد تعليقاته. حد الخطورة وسقف التعليقات البالغ عشرة ليسا من باب المجاملة، بل هما ما يحافظ على ظهور النتائج الفعلية. ويعني الفرز حسب الخطورة قبل الاقتطاع أن السقف يحذف النتائج الأقل أهمية، لا عشر نتائج عشوائية.

التعليق بثقة على شيء لا يستطيع التحقق منه. هذا هو السلوك الذي يدفع المهندسين إلى تعطيل الروبوت نهائياً. فعندما يُعرض على نموذج 200 سطر من قاعدة شفرية تتكون من 40,000 سطر، سيظل يكتب: «هذا يعطل إبطال ذاكرة التخزين المؤقت في redis_client.py» عن ملف لم يره قط. تدفع تعليمات النظام النموذج إلى الالتزام بلغة واضحة: أبلغ فقط عن العيوب الظاهرة في الأسطر المعروضة، واترك أي شيء لا تكون متأكداً منه. إن تسمية الإخفاق مباشرةً أفضل من طلب الدقة عموماً، وإخبار النموذج بأن النتيجة الفارغة طبيعية هو ما يمنعه من اختلاق شيء يقوله عن تغيير مكوّن من سطرين.

انشر المراجعة بصفتها COMMENT، وليس بصفتها REQUEST_CHANGES. يجب ألا يتمكن رأي النموذج من منع الدمج، وما إن يصبح قادراً على ذلك حتى يزيل شخص لديه مهلة نهائية سير العمل كله بدلاً من الجدال معه.

أنماط الفشل، مع السلاسل النصية التي ستظهر لك

HTTP 422 عند نشر المراجعة. تطبع gh القيمة gh: Unprocessable Entity (HTTP 422)، ويذكر نص الاستجابة الحقل: Pull request review thread line must be part of the diff. لا يستطيع GitHub تثبيت ذلك التعليق على موضع محدد. تتمثل الأسباب المعتادة في رقم سطر اخترعه النموذج، أو path لا يزال يحمل البادئة b/، أو تعليق على سطر محذوف، وعندها يجب ضبط side على LEFT بدلاً من RIGHT. اطبع JSON الخاص بالمراجعة قبل نشره، وتحقق يدوياً من تعليق واحد بمقارنته مع الفرق.

invalid x-api-key من واجهة model API. تفشل الخطوة عند أول استدعاء لـ messages.create. إما أن السر ANTHROPIC_API_KEY غير مضبوط في المستودع، أو أن طلب السحب صادر من fork، ولذلك لم تمرر Actions أي أسرار. كان ينبغي لحارس fork في سطر if: أن يتجاوز الخطوة، لذا تحقق من ذلك السطر أولاً.

gh: Resource not accessible by integration (HTTP 403). لا يستطيع رمز الوظيفة الكتابة إلى طلبات السحب. أضف pull-requests: write إلى كتلة permissions:. إذا كان موجوداً بالفعل، فانتقل إلى Settings، ثم Actions، ثم General، حيث يمكن لسياسة المؤسسة تقييد ما يُسمح لأي رمز workflow بطلبه.

json.decoder.JSONDecodeError. لم يُرجع النموذج JSON قابلاً للتحليل. السبب الشائع هو أن الاستجابة بلغت حد الرموز وتوقفت في منتصف الكائن. يطبع سطر السجل stop_reason لهذا السبب تحديداً: تعني قيمة max_tokens أنه يجب رفع max_tokens أو خفض MAX_COMMENTS.

لا تُشغَّل workflow مطلقاً. لا يعرض gh run list أي شيء لطلب السحب. تحقق من أن paths-ignore لم يستبعد جميع الملفات المتغيرة، ثم تحقق من حارس fork وحارس label، وبعد ذلك تحقق من أن runner يعمل باستخدام sudo systemctl status 'actions.runner.*' على VPS. يترك runner غير المتصل الوظيفة في قائمة الانتظار من دون أي رسالة خطأ في طلب السحب.

تعود كل مراجعة فارغة. اضبط MIN_SEVERITY على low لتشغيل واحد. إذا ظهرت النتائج، فهذا يعني أن العتبة تعمل كما ينبغي. وإذا لم يظهر شيء، فاطبع payload وتأكد من أن عوامل التصفية لم تستبعد الفرق بالكامل.

تشغيله إلى جانب الوكلاء الآخرين

المراجع صغير، لذلك قد تميل إلى وضعه على الخادم الذي يشغّل كل شيء آخر. افصله إذا كان المستودع مهماً. يحتفظ هذا المكوّن برمز مميز يمكنه التعليق على التعليمات البرمجية، وبمفتاح ينفق أموالك، كما أن العامل المستضاف ذاتياً هو، بحكم تصميمه، مكاناً تُنفّذ فيه تعليمات سير العمل. حساب مخصص دون صلاحيات مميّزة ومن دون صلاحيات sudo، على مضيف لا يشغّل أي شيء آخر، هو الحد الأدنى المطلوب. إذا كنت تشغّل أيضاً وكلاء تفاعليين يسحبون التعليمات البرمجية، فإن آلة افتراضية مؤقتة لكل وكيل هي النمط العملي، بينما يشرح تشغيل وكيل برمجي على VPS الإعداد العام. إذا كانت Anthropic API جديدة عليك، فإن إنشاء أول تطبيق Claude API على VPS نقطة بداية أبسط من هذا.

FAQ

هل يحتاج وكيل مراجعة PR بالذكاء الاصطناعي إلى صلاحية الكتابة في مستودعي؟

لا. يحتاج إلى pull-requests: write لنشر مراجعة وإلى contents: read لجلب diff. هذه هي الصلاحيات المطلوبة بالكامل، وتحددها في كتلة permissions: ضمن سير العمل، التي تقيّد ما يمكن لـGITHUB_TOKEN الخاص بكل مهمة تنفيذه. باستخدام هذين السطرين، يستطيع الوكيل إضافة تعليق إلى pull request، لكنه لا يستطيع دفع commit أو دمج branch. انشر المراجعات باستخدام event: COMMENT بدلاً من REQUEST_CHANGES، حتى لا يتمكن الوكيل أيضاً من منع الدمج.

لماذا يفشل تعليق المراجعة لدي مع HTTP 422؟

لا تقبل GitHub تعليق مراجعة مضمّناً إلا على سطر يقع ضمن diff الخاص بـpull request، وتُرجع Pull request review thread line must be part of the diff إذا لم يكن السطر كذلك. تحقق من أن path نسبي إلى المستودع ولا يحتوي على بادئة b/ الواردة في ترويسة diff، وأن رقم السطر يقع داخل hunk الخاص بذلك الملف. يجب أن تكون side هي RIGHT للسطر المضاف أو غير المتغير، وLEFT للسطر المحذوف. إن إضافة رقم سطر الملف الجديد إلى بداية كل سطر في diff قبل إرساله إلى النموذج تمنع النموذج من اختراع الأرقام من الأساس.

هل يمكنني تشغيل هذا على مستودع عام يحتوي على pull requests من forks؟

لا، ليس بهذا التصميم. لا تمرّر GitHub الأسرار إلى سير عمل يُشغّل من fork، ولذلك يكون مفتاح النموذج مفقوداً ويفشل التشغيل. كما توضح GitHub أن self-hosted runners «ينبغي تقريباً ألا تُستخدم أبداً مع المستودعات العامة»، لأن أي شخص يستطيع فتح pull request يؤدي إلى تشغيل تعليمات برمجية على جهازك. في مشروع عام، إمّا أن تقيّد المراجع بالفروع المدفوعة إلى المستودع نفسه، وهو ما يفعله guard if:، أو تنقل خطوة المراجعة إلى GitHub-hosted runner وتقبل مغادرة diff لبنيتك التحتية.

أي نموذج ينبغي أن أستخدم لمراجعة pull request؟

ابدأ بـHaiku 4.5. قراءة diff محدود مقابل قائمة ثابتة من أنواع العيوب ليست مسألة تتطلب استدلالاً معقداً، كما أن استخدام النموذج الأرخص يبقي الفاتورة الشهرية عند رقم لا يثير اعتراض أحد. انتقل إلى Sonnet 5 إذا وجدت أنه يفوّت أخطاء حقيقية في لغتك أو framework، وقِس ذلك بدلاً من افتراضه. يُعد Opus 5 الأغلى بين النماذج الثلاثة بفارق كبير لكل pull request، ولذلك يسهل تبرير استخدامه في branch إصدار بدلاً من استخدامه مع كل push إلى كل feature branch.