169 lines
5.5 KiB
Python
Executable File
169 lines
5.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
CNB CodeBuddy NPC 看板调度器
|
||
通过 Issue @npc/CodeBuddy 触发云端编码
|
||
|
||
用法:
|
||
# 创建仓库
|
||
python3 npc-dispatch.py --create-repo muchen-org/test-repo
|
||
|
||
# 触发任务(自动建 Issue + @CodeBuddy 启动)
|
||
python3 npc-dispatch.py "实现一个 HTTP 服务器" --repo muchen-org/test-repo
|
||
|
||
# 带验收标准
|
||
python3 npc-dispatch.py "写 REST API" --repo muchen-org/test --criteria "有单元测试" "支持 POST/GET"
|
||
"""
|
||
|
||
import argparse
|
||
import json
|
||
import os
|
||
import sys
|
||
import urllib.error
|
||
import urllib.request
|
||
from pathlib import Path
|
||
|
||
API = "https://api.cnb.cool"
|
||
|
||
|
||
def load_token():
|
||
"""从 ~/.hermes/.env 读取 CNB_TOKEN"""
|
||
env_path = Path.home() / ".hermes" / ".env"
|
||
try:
|
||
with open(env_path) as f:
|
||
for line in f:
|
||
line = line.strip()
|
||
if line.startswith("CNB_TOKEN="):
|
||
return line.split("=", 1)[1].strip()
|
||
except FileNotFoundError:
|
||
pass
|
||
return os.environ.get("CNB_TOKEN", "")
|
||
|
||
|
||
def api_call(method, path, data=None):
|
||
"""CNB API 统一调用"""
|
||
token = load_token()
|
||
if not token:
|
||
print("❌ CNB_TOKEN 未配置", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
url = f"{API}{path}"
|
||
headers = {
|
||
"Authorization": f"Bearer {token}",
|
||
"accept": "application/json",
|
||
"Content-Type": "application/json",
|
||
}
|
||
body = json.dumps(data).encode("utf-8") if data else None
|
||
req = urllib.request.Request(url, data=body, headers=headers, method=method)
|
||
|
||
try:
|
||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||
content = resp.read().decode()
|
||
# 空响应(201 Created)直接返回 None
|
||
if not content.strip():
|
||
return {"status": resp.status, "empty": True}
|
||
return json.loads(content)
|
||
except urllib.error.HTTPError as e:
|
||
error_body = e.read().decode()
|
||
print(f"❌ HTTP {e.code}: {error_body}", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
|
||
def create_repo(org, name, description="", visibility="public"):
|
||
"""创建仓库"""
|
||
result = api_call("POST", f"/{org}/-/repos", {
|
||
"name": name,
|
||
"description": description,
|
||
"visibility": visibility,
|
||
})
|
||
# 201 Created 返回空 body,用 path/name 构造结果
|
||
if result.get("empty"):
|
||
return {"path": f"{org}/{name}", "name": name, "visibility": visibility}
|
||
return result
|
||
|
||
|
||
def create_issue_with_npc(repo, task_title, task_body, criteria=None, work_mode=True):
|
||
"""创建 Issue 并触发 NPC"""
|
||
# 构造 body
|
||
body_parts = [f"## 任务描述\n{task_body}\n"]
|
||
if criteria:
|
||
body_parts.append("## 验收标准\n")
|
||
for i, c in enumerate(criteria, 1):
|
||
body_parts.append(f"{i}. {c}")
|
||
body = "\n".join(body_parts)
|
||
|
||
# 创建 Issue(带 work_mode: true)
|
||
issue = api_call("POST", f"/{repo}/-/issues", {
|
||
"title": f"@npc/CodeBuddy 替我上班:{task_title}",
|
||
"body": body,
|
||
"labels": ["enhancement"],
|
||
"priority": "P2",
|
||
"work_mode": work_mode,
|
||
})
|
||
print(f"✅ Issue 已创建: #{issue.get('number')} - {issue.get('title')}")
|
||
|
||
# 立即发评论再次触发(保险起见)
|
||
issue_number = issue.get("iid") or issue.get("number")
|
||
comment = api_call("POST", f"/{repo}/-/issues/{issue_number}/comments", {
|
||
"body": "@npc/CodeBuddy 替我上班,请开始执行这个任务。",
|
||
"work_mode": True,
|
||
})
|
||
print(f"✅ 触发评论已发: NPC 约 3 分钟内开始响应")
|
||
|
||
return {
|
||
"issue_number": issue_number,
|
||
"title": issue.get("title"),
|
||
"url": issue.get("html_url"),
|
||
"state": issue.get("state"),
|
||
}
|
||
|
||
|
||
def dispatch_to_npc(task_desc, repo="", criteria=None):
|
||
"""主入口:分派任务给 NPC"""
|
||
if not repo:
|
||
repo = os.environ.get("CNB_DEFAULT_REPO", "muchen-org/auto-npc")
|
||
|
||
# 确保仓库存在
|
||
repo_info = None
|
||
try:
|
||
repo_info = api_call("GET", f"/{repo}")
|
||
except SystemExit:
|
||
print(f"📦 仓库 {repo} 不存在,尝试创建...")
|
||
if not repo or "/" not in repo:
|
||
print("❌ 需要有效的 org/repo 格式", file=sys.stderr)
|
||
sys.exit(1)
|
||
org, name = repo.split("/", 1)
|
||
create_repo(org, name, description="Auto-created by 小唯看板")
|
||
repo_info = api_call("GET", f"/{repo}")
|
||
|
||
# 提取简短标题
|
||
short_title = task_desc[:60] + ("..." if len(task_desc) > 60 else "")
|
||
return create_issue_with_npc(repo, short_title, task_desc, criteria)
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description="CNB CodeBuddy NPC 看板调度器")
|
||
parser.add_argument("task", nargs="?", help="任务描述")
|
||
parser.add_argument("--repo", help="目标仓库 (org/name)")
|
||
parser.add_argument("--criteria", nargs="+", help="验收标准(多项)")
|
||
parser.add_argument("--create-repo", metavar="ORG/NAME", help="创建仓库")
|
||
parser.add_argument("--no-work-mode", action="store_true", help="禁用 work_mode(仅编码不能 push)")
|
||
|
||
args = parser.parse_args()
|
||
|
||
if args.create_repo:
|
||
org, name = args.create_repo.split("/", 1)
|
||
result = create_repo(org, name)
|
||
print(f"✅ 仓库已创建: {result.get('path') or args.create_repo}")
|
||
return
|
||
|
||
if not args.task:
|
||
parser.print_help()
|
||
sys.exit(1)
|
||
|
||
result = dispatch_to_npc(args.task, args.repo, args.criteria)
|
||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|