xiaowei-system/scripts/npc-dispatch.py

169 lines
5.5 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()