Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 75 additions & 1 deletion ccloop_lib/github_ops.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import json
import logging
from datetime import datetime, timezone
from typing import Optional

from github import Github, GithubException

from .state_machine import TaskState, can_transition, transition_labels

log = logging.getLogger("ccloop")

BOT_MARKER = "<!-- ccloop:auto -->"
_BOT_EMOJI_PREFIXES = ("🔄", "✅", "⚠️")
_BOT_EMOJI_PREFIXES = ("🔄", "✅", "⚠️", "📋", "🔍", "🧪")


def _is_bot_comment(body: str) -> bool:
Expand Down Expand Up @@ -119,6 +122,77 @@ def mark_processed(self, issue):
self.repo.create_label(self.processed_label, "0e8a16")
issue.add_to_labels(self.processed_label)

def transition_state(self, issue, target: TaskState) -> bool:
"""状态机驱动的 label 转换

Returns:
True if transition succeeded (or was already in target state)
"""
label_names = [lbl.name for lbl in issue.labels]
current = None
for s in TaskState:
if s.label in label_names:
current = s
break

if current == target:
return True

if current and not can_transition(current, target):
log.warning(f"#{issue.number} 状态转换不合法: {current.label} → {target.label}")
return False

if self.dry_run:
log.info(f"[DRY-RUN] 状态转换 #{issue.number}: "
f"{current.label if current else '(none)'} → {target.label}")
return True

to_add, to_remove = transition_labels(
current or target, target
) if current else ([target.label], [])

if current:
to_add, to_remove = transition_labels(current, target)

try:
for lbl in to_remove:
try:
issue.remove_from_labels(lbl)
except GithubException:
pass
for lbl in to_add:
try:
issue.add_to_labels(lbl)
except GithubException:
self.repo.create_label(lbl, "0e8a16")
issue.add_to_labels(lbl)
log.info(f"#{issue.number} 状态: {current.label if current else '(none)'} → {target.label}")
return True
except GithubException as e:
log.error(f"状态转换失败 #{issue.number}: {e}")
return False

def write_claim_comment(self, issue, worker_id: str = "ccloop") -> str:
"""写入 machine-readable claim comment(借鉴 AI Dispatcher 的抢占机制)

Returns:
claim comment body
"""
claim = {
"action": "claim",
"worker": worker_id,
"ts": datetime.now(timezone.utc).isoformat(),
"issue": issue.number,
}
body = f"<!-- ccloop:claim {json.dumps(claim)} -->\n🔄 `{worker_id}` 开始处理{_BOT_MARKER}"
self.comment(issue, body)
return body

def get_current_state(self, issue) -> TaskState | None:
"""从 issue labels 解析当前状态"""
from .state_machine import get_current_state as _gcs
return _gcs(issue.labels)

def comment(self, issue, msg: str):
if self.dry_run:
log.info(f"[DRY-RUN] 评论 #{issue.number}:\n{msg[:200]}")
Expand Down
9 changes: 7 additions & 2 deletions ccloop_lib/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,10 @@ def build_solve_prompt(issue_number: int, issue_title: str, issue_body: str,
repo_name: str, default_branch: str, repo_tree: str,
previous_errors: Optional[list] = None,
latest_comment: str = "",
task_type_hint: str = "") -> str:
task_type_guidance: str = "",
task_type_hint: str = "",
reference_context: str = "",
capability_context: str = "") -> str:
body = _truncate(issue_body or "(无描述)", config.MAX_BODY_CHARS, "issue 正文")
aci_section = get_aci_template(task_type_hint) if task_type_hint else ""
prompt = f"""你是自动解决 GitHub Issue 的 AI 助手。你在仓库目录中工作。
Expand All @@ -83,11 +86,13 @@ def build_solve_prompt(issue_number: int, issue_title: str, issue_body: str,
- 默认分支: {default_branch}
- 目录结构:
{repo_tree}

{capability_context}
{reference_context}
## Issue #{issue_number}: {issue_title}

{body}
{f'\\n## 最新评论\\n{latest_comment}' if latest_comment else ''}
{task_type_guidance}

## 工作流程
{aci_section}
Expand Down
Loading