Spaces:
Sleeping
Sleeping
Complete: P0+P1+P2+Skills (2189 lines, 22 features)
Browse files- codepilot_v4.py +713 -17
codepilot_v4.py
CHANGED
|
@@ -340,6 +340,9 @@ class ProjectTools:
|
|
| 340 |
def read_file(self, path, offset=1, limit=200):
|
| 341 |
full = self._resolve(path)
|
| 342 |
if not os.path.exists(full): return f"❌ 不存在: {path}"
|
|
|
|
|
|
|
|
|
|
| 343 |
try:
|
| 344 |
content = Path(full).read_text(encoding="utf-8", errors="replace"); lines = content.splitlines()
|
| 345 |
self.read_cache[full] = {"time": os.path.getmtime(full), "content": content}
|
|
@@ -369,8 +372,12 @@ class ProjectTools:
|
|
| 369 |
return f"✅ {'建立' if is_new else '覆寫'}: {path}"
|
| 370 |
|
| 371 |
def run_command(self, command, timeout=120):
|
| 372 |
-
|
| 373 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
try:
|
| 375 |
r = subprocess.run(command, shell=True, cwd=self.cwd, capture_output=True, text=True, timeout=timeout)
|
| 376 |
return (r.stdout + (f"\nSTDERR:\n{r.stderr}" if r.stderr else ""))[:10000]
|
|
@@ -442,9 +449,278 @@ def execute_tool(tools, call):
|
|
| 442 |
elif n == "search_files": result = tools.search_files(p.get("pattern",""), p.get("glob"))
|
| 443 |
elif n == "list_files": result = tools.list_files(p.get("pattern","*"), int(p.get("max_depth",3)))
|
| 444 |
elif n == "git_status": result = tools.git_context()
|
|
|
|
|
|
|
| 445 |
else: result = f"❌ 未知: {n}"
|
| 446 |
except Exception as e: result = f"❌ {e}"
|
| 447 |
-
return truncate_tool_result(result)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 448 |
|
| 449 |
|
| 450 |
# ============================================================
|
|
@@ -657,6 +933,341 @@ class Hooks:
|
|
| 657 |
return None
|
| 658 |
|
| 659 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 660 |
# ============================================================
|
| 661 |
# P1-1: APPROVAL SYSTEM(權限/審批)
|
| 662 |
# ============================================================
|
|
@@ -1097,15 +1708,18 @@ def run_agent_loop(args):
|
|
| 1097 |
messages = [{"role": "system", "content": system_prompt}]
|
| 1098 |
|
| 1099 |
hooks = Hooks(project_dir)
|
| 1100 |
-
bg_tasks = BackgroundTaskManager()
|
| 1101 |
-
custom_agents = load_custom_agents(project_dir)
|
| 1102 |
-
|
|
|
|
|
|
|
|
|
|
| 1103 |
edited_files_this_session = []
|
| 1104 |
|
| 1105 |
if custom_agents:
|
| 1106 |
console.print(f"[dim]🤖 自訂代理: {', '.join(custom_agents.keys())}[/]")
|
| 1107 |
|
| 1108 |
-
console.print(f"[dim]指令: /init /verify /commit /agent /bg /approval | /duel /memo /grind /ls /git /clear /status /train /quit[/]\n")
|
| 1109 |
|
| 1110 |
while True:
|
| 1111 |
try: user_input = Prompt.ask("\n[bold green]🧑 You")
|
|
@@ -1188,6 +1802,46 @@ def run_agent_loop(args):
|
|
| 1188 |
auto_git_commit(tools, model, edited_files_this_session, console)
|
| 1189 |
continue
|
| 1190 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1191 |
elif cmd.startswith("/agent"):
|
| 1192 |
# P1-3: 自訂代理
|
| 1193 |
parts = cmd.split(None, 2)
|
|
@@ -1245,6 +1899,42 @@ def run_agent_loop(args):
|
|
| 1245 |
console.print("[dim]/bg list | /bg run <cmd> | /bg check <id> | /bg kill <id>[/]")
|
| 1246 |
continue
|
| 1247 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1248 |
elif cmd.startswith("/approval"):
|
| 1249 |
# P1-1: 切換審批模式
|
| 1250 |
parts = cmd.split()
|
|
@@ -1339,19 +2029,23 @@ def run_agent_loop(args):
|
|
| 1339 |
tools_used_this_turn = [] # 追蹤這輪用了哪些工具
|
| 1340 |
|
| 1341 |
for rnd in range(10):
|
| 1342 |
-
|
| 1343 |
-
|
| 1344 |
-
#
|
| 1345 |
-
response =
|
| 1346 |
-
|
| 1347 |
-
|
| 1348 |
-
|
| 1349 |
-
|
| 1350 |
-
|
|
|
|
|
|
|
|
|
|
| 1351 |
|
| 1352 |
tool_calls = parse_tool_calls(response)
|
| 1353 |
text_parts = TOOL_PATTERN.sub("", response).strip()
|
| 1354 |
-
if text_parts:
|
|
|
|
| 1355 |
console.print(f"\n[bold blue]🤖 CodePilot:[/]")
|
| 1356 |
console.print(Markdown(text_parts))
|
| 1357 |
full_response += response + "\n"
|
|
@@ -1427,6 +2121,7 @@ def run_agent_loop(args):
|
|
| 1427 |
|
| 1428 |
ctx.save_session(messages)
|
| 1429 |
|
|
|
|
| 1430 |
console.print("\n[cyan]👋[/]")
|
| 1431 |
|
| 1432 |
|
|
@@ -1483,6 +2178,7 @@ def main():
|
|
| 1483 |
p.add_argument("--distill", action="store_true")
|
| 1484 |
p.add_argument("--grind", action="store_true", help="LeetCode 自動刷題")
|
| 1485 |
p.add_argument("--grind-count", type=int, default=100, help="刷幾題")
|
|
|
|
| 1486 |
p.add_argument("--stats", action="store_true"); p.add_argument("--train", action="store_true")
|
| 1487 |
a = p.parse_args()
|
| 1488 |
if a.stats: show_stats()
|
|
|
|
| 340 |
def read_file(self, path, offset=1, limit=200):
|
| 341 |
full = self._resolve(path)
|
| 342 |
if not os.path.exists(full): return f"❌ 不存在: {path}"
|
| 343 |
+
# P2-3: 多模態檔案
|
| 344 |
+
mm = read_multimodal(full)
|
| 345 |
+
if mm is not None: return mm
|
| 346 |
try:
|
| 347 |
content = Path(full).read_text(encoding="utf-8", errors="replace"); lines = content.splitlines()
|
| 348 |
self.read_cache[full] = {"time": os.path.getmtime(full), "content": content}
|
|
|
|
| 372 |
return f"✅ {'建立' if is_new else '覆寫'}: {path}"
|
| 373 |
|
| 374 |
def run_command(self, command, timeout=120):
|
| 375 |
+
# P2-4: 安全分類器
|
| 376 |
+
safety, reason = classify_command(command)
|
| 377 |
+
if safety == "block":
|
| 378 |
+
return f"⛔ 危險指令被阻擋: {command}\n原因: {reason}"
|
| 379 |
+
if safety == "warn":
|
| 380 |
+
return f"⚠️ 警告: {reason}\n指令: {command}\n(在 --approval ask 模式下會要求確認)"
|
| 381 |
try:
|
| 382 |
r = subprocess.run(command, shell=True, cwd=self.cwd, capture_output=True, text=True, timeout=timeout)
|
| 383 |
return (r.stdout + (f"\nSTDERR:\n{r.stderr}" if r.stderr else ""))[:10000]
|
|
|
|
| 449 |
elif n == "search_files": result = tools.search_files(p.get("pattern",""), p.get("glob"))
|
| 450 |
elif n == "list_files": result = tools.list_files(p.get("pattern","*"), int(p.get("max_depth",3)))
|
| 451 |
elif n == "git_status": result = tools.git_context()
|
| 452 |
+
elif n == "web_fetch": result = web_fetch(p.get("url","")) # P2-1
|
| 453 |
+
elif n == "web_search": result = web_search(p.get("query","")) # P2-1
|
| 454 |
else: result = f"❌ 未知: {n}"
|
| 455 |
except Exception as e: result = f"❌ {e}"
|
| 456 |
+
return truncate_tool_result(result)
|
| 457 |
+
|
| 458 |
+
|
| 459 |
+
# ============================================================
|
| 460 |
+
# P2-1: WEB FETCH / WEB SEARCH
|
| 461 |
+
# ============================================================
|
| 462 |
+
def web_fetch(url, max_chars=8000):
|
| 463 |
+
"""讀取網頁內容(去掉 HTML 標籤)"""
|
| 464 |
+
try:
|
| 465 |
+
if not httpx: return "❌ 請安裝 httpx: pip install httpx"
|
| 466 |
+
resp = httpx.get(url, timeout=15, follow_redirects=True,
|
| 467 |
+
headers={"User-Agent": "CodePilot/1.0"})
|
| 468 |
+
resp.raise_for_status()
|
| 469 |
+
content = resp.text
|
| 470 |
+
# 簡易去 HTML 標籤
|
| 471 |
+
content = re.sub(r'<script[^>]*>.*?</script>', '', content, flags=re.DOTALL)
|
| 472 |
+
content = re.sub(r'<style[^>]*>.*?</style>', '', content, flags=re.DOTALL)
|
| 473 |
+
content = re.sub(r'<[^>]+>', ' ', content)
|
| 474 |
+
content = re.sub(r'\s+', ' ', content).strip()
|
| 475 |
+
return content[:max_chars]
|
| 476 |
+
except Exception as e:
|
| 477 |
+
return f"❌ 抓取失敗: {e}"
|
| 478 |
+
|
| 479 |
+
def web_search(query, max_results=5):
|
| 480 |
+
"""網路搜尋(使用 DuckDuckGo HTML,不需要 API key)"""
|
| 481 |
+
try:
|
| 482 |
+
if not httpx: return "❌ 請安裝 httpx: pip install httpx"
|
| 483 |
+
resp = httpx.get("https://html.duckduckgo.com/html/",
|
| 484 |
+
params={"q": query}, timeout=10,
|
| 485 |
+
headers={"User-Agent": "CodePilot/1.0"})
|
| 486 |
+
# 提取搜尋結果
|
| 487 |
+
results = []
|
| 488 |
+
for match in re.finditer(r'<a[^>]+href="(https?://[^"]+)"[^>]*class="result__a"[^>]*>(.*?)</a>', resp.text, re.DOTALL):
|
| 489 |
+
url = match.group(1)
|
| 490 |
+
title = re.sub(r'<[^>]+>', '', match.group(2)).strip()
|
| 491 |
+
results.append(f"- [{title}]({url})")
|
| 492 |
+
if len(results) >= max_results: break
|
| 493 |
+
# 也嘗試提取摘要
|
| 494 |
+
for match in re.finditer(r'<a[^>]+class="result__snippet"[^>]*>(.*?)</a>', resp.text, re.DOTALL):
|
| 495 |
+
snippet = re.sub(r'<[^>]+>', '', match.group(1)).strip()
|
| 496 |
+
if snippet and len(results) > 0:
|
| 497 |
+
idx = min(len(results)-1, len([r for r in results if not r.startswith(" ")]) - 1)
|
| 498 |
+
if idx >= 0: results.insert(idx+1, f" {snippet[:150]}")
|
| 499 |
+
return "\n".join(results) if results else f"無搜尋結果: {query}"
|
| 500 |
+
except Exception as e:
|
| 501 |
+
return f"❌ 搜尋失敗: {e}"
|
| 502 |
+
|
| 503 |
+
|
| 504 |
+
# ============================================================
|
| 505 |
+
# P2-2: STREAMING OUTPUT(逐字輸出)
|
| 506 |
+
# ============================================================
|
| 507 |
+
def stream_local_chat(model, messages, console, max_tokens=4096):
|
| 508 |
+
"""本地模型 streaming — 逐 token 顯示"""
|
| 509 |
+
if not hasattr(model, 'tokenizer') or not hasattr(model, 'model'):
|
| 510 |
+
return model.chat(messages, max_tokens) # 非本地模型 fallback
|
| 511 |
+
|
| 512 |
+
from transformers import TextIteratorStreamer
|
| 513 |
+
import threading
|
| 514 |
+
|
| 515 |
+
text = model.tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
|
| 516 |
+
inputs = model.tokenizer(text, return_tensors="pt").to(model.model.device)
|
| 517 |
+
|
| 518 |
+
streamer = TextIteratorStreamer(model.tokenizer, skip_prompt=True, skip_special_tokens=True)
|
| 519 |
+
gen_kwargs = dict(**inputs, max_new_tokens=max_tokens, do_sample=True,
|
| 520 |
+
temperature=0.7, top_p=0.9, repetition_penalty=1.1,
|
| 521 |
+
pad_token_id=model.tokenizer.pad_token_id, streamer=streamer)
|
| 522 |
+
|
| 523 |
+
thread = threading.Thread(target=model.model.generate, kwargs=gen_kwargs)
|
| 524 |
+
thread.start()
|
| 525 |
+
|
| 526 |
+
console.print(f"\n[bold blue]🤖 CodePilot:[/]", end="")
|
| 527 |
+
full_text = ""
|
| 528 |
+
for chunk in streamer:
|
| 529 |
+
print(chunk, end="", flush=True)
|
| 530 |
+
full_text += chunk
|
| 531 |
+
print() # newline
|
| 532 |
+
thread.join()
|
| 533 |
+
return full_text
|
| 534 |
+
|
| 535 |
+
|
| 536 |
+
# ============================================================
|
| 537 |
+
# P2-3: MULTIMODAL(圖片/PDF 讀取)
|
| 538 |
+
# ============================================================
|
| 539 |
+
def read_multimodal(path):
|
| 540 |
+
"""讀取圖片/PDF/notebook 的文字描述"""
|
| 541 |
+
ext = Path(path).suffix.lower()
|
| 542 |
+
|
| 543 |
+
if ext in (".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".svg"):
|
| 544 |
+
# 圖片:回傳檔案資訊
|
| 545 |
+
try:
|
| 546 |
+
size = os.path.getsize(path)
|
| 547 |
+
return f"[Image: {path}, {size/1024:.0f}KB, {ext}]\n(圖片內容無法在文字模式顯示。如需分析圖片,請使用支援多模態的雲端模型。)"
|
| 548 |
+
except: return f"❌ 無法讀取圖片: {path}"
|
| 549 |
+
|
| 550 |
+
elif ext == ".pdf":
|
| 551 |
+
# PDF:嘗試用 pdfminer 或 fallback
|
| 552 |
+
try:
|
| 553 |
+
from pdfminer.high_level import extract_text
|
| 554 |
+
text = extract_text(path, maxpages=20)
|
| 555 |
+
return f"[PDF: {path}, {len(text)} chars extracted]\n\n{text[:10000]}"
|
| 556 |
+
except ImportError:
|
| 557 |
+
try:
|
| 558 |
+
# fallback: pdftotext 指令
|
| 559 |
+
r = subprocess.run(["pdftotext", "-l", "20", path, "-"],
|
| 560 |
+
capture_output=True, text=True, timeout=30)
|
| 561 |
+
return f"[PDF: {path}]\n\n{r.stdout[:10000]}"
|
| 562 |
+
except:
|
| 563 |
+
return f"[PDF: {path}] (安裝 pdfminer.six 以讀取: pip install pdfminer.six)"
|
| 564 |
+
|
| 565 |
+
elif ext == ".ipynb":
|
| 566 |
+
# Jupyter Notebook:提取 code cells 和 markdown
|
| 567 |
+
try:
|
| 568 |
+
nb = json.loads(Path(path).read_text())
|
| 569 |
+
cells = nb.get("cells", [])
|
| 570 |
+
output = []
|
| 571 |
+
for i, cell in enumerate(cells[:30]):
|
| 572 |
+
ctype = cell.get("cell_type", "")
|
| 573 |
+
source = "".join(cell.get("source", []))
|
| 574 |
+
if ctype == "markdown":
|
| 575 |
+
output.append(f"[Markdown Cell {i+1}]\n{source}")
|
| 576 |
+
elif ctype == "code":
|
| 577 |
+
output.append(f"[Code Cell {i+1}]\n```python\n{source}\n```")
|
| 578 |
+
return "\n\n".join(output)[:10000]
|
| 579 |
+
except Exception as e:
|
| 580 |
+
return f"❌ 無法讀取 notebook: {e}"
|
| 581 |
+
|
| 582 |
+
return None # 非多模態檔案
|
| 583 |
+
|
| 584 |
+
|
| 585 |
+
# ============================================================
|
| 586 |
+
# P2-4: SHELL SANDBOX(指令安全分類)
|
| 587 |
+
# ============================================================
|
| 588 |
+
# 不用 ML,用規則分類 — 比 ML 更可靠且不需要額外模型
|
| 589 |
+
DANGEROUS_PATTERNS = [
|
| 590 |
+
r"rm\s+(-rf?|--recursive)\s+[/~]", # rm -rf /
|
| 591 |
+
r"rm\s+-rf?\s+\.", # rm -rf .
|
| 592 |
+
r">(>?)\s*/dev/sd", # 覆寫磁碟
|
| 593 |
+
r"mkfs\.", # 格式化
|
| 594 |
+
r"dd\s+if=", # 磁碟操作
|
| 595 |
+
r":()\{.*\|.*&\s*\};:", # fork bomb
|
| 596 |
+
r"chmod\s+777\s+/", # 危險權限
|
| 597 |
+
r"curl.*\|\s*(bash|sh)", # pipe to shell
|
| 598 |
+
r"wget.*\|\s*(bash|sh)", # pipe to shell
|
| 599 |
+
]
|
| 600 |
+
|
| 601 |
+
WARN_PATTERNS = [
|
| 602 |
+
r"git\s+push\s+.*--force", # force push
|
| 603 |
+
r"git\s+reset\s+--hard", # hard reset
|
| 604 |
+
r"git\s+clean\s+-fd", # clean untracked
|
| 605 |
+
r"npm\s+publish", # publish package
|
| 606 |
+
r"pip\s+install\s+--force", # force install
|
| 607 |
+
r"docker\s+system\s+prune", # docker cleanup
|
| 608 |
+
r"DROP\s+TABLE", # SQL drop
|
| 609 |
+
r"DELETE\s+FROM\s+\w+\s*;?\s*$", # SQL delete all
|
| 610 |
+
r"sudo\s+", # sudo
|
| 611 |
+
]
|
| 612 |
+
|
| 613 |
+
def classify_command(command):
|
| 614 |
+
"""
|
| 615 |
+
分類指令安全等級:
|
| 616 |
+
- 'block': 直接阻擋
|
| 617 |
+
- 'warn': 需要額外確認
|
| 618 |
+
- 'safe': 安全
|
| 619 |
+
"""
|
| 620 |
+
for p in DANGEROUS_PATTERNS:
|
| 621 |
+
if re.search(p, command, re.IGNORECASE):
|
| 622 |
+
return "block", f"危險指令匹配: {p}"
|
| 623 |
+
for p in WARN_PATTERNS:
|
| 624 |
+
if re.search(p, command, re.IGNORECASE):
|
| 625 |
+
return "warn", f"需要確認: {p}"
|
| 626 |
+
return "safe", ""
|
| 627 |
+
|
| 628 |
+
|
| 629 |
+
# ============================================================
|
| 630 |
+
# P2-5: MCP LITE(簡易外部工具協議)
|
| 631 |
+
# ============================================================
|
| 632 |
+
class MCPLite:
|
| 633 |
+
"""
|
| 634 |
+
簡易 MCP — 讀取 .codepilot/mcp.json,連接外部工具伺服器。
|
| 635 |
+
支援 stdio 和 http 兩種傳輸方式。
|
| 636 |
+
|
| 637 |
+
.codepilot/mcp.json:
|
| 638 |
+
{
|
| 639 |
+
"servers": {
|
| 640 |
+
"database": {
|
| 641 |
+
"command": "python db_mcp_server.py",
|
| 642 |
+
"type": "stdio"
|
| 643 |
+
},
|
| 644 |
+
"api": {
|
| 645 |
+
"url": "http://localhost:9000/mcp",
|
| 646 |
+
"type": "http"
|
| 647 |
+
}
|
| 648 |
+
}
|
| 649 |
+
}
|
| 650 |
+
"""
|
| 651 |
+
def __init__(self, project_dir):
|
| 652 |
+
self.servers = {}
|
| 653 |
+
self.processes = {}
|
| 654 |
+
mcp_file = os.path.join(project_dir, ".codepilot", "mcp.json")
|
| 655 |
+
if os.path.exists(mcp_file):
|
| 656 |
+
try:
|
| 657 |
+
config = json.loads(Path(mcp_file).read_text())
|
| 658 |
+
self.servers = config.get("servers", {})
|
| 659 |
+
except: pass
|
| 660 |
+
|
| 661 |
+
def call(self, server_name, method, params=None):
|
| 662 |
+
"""呼叫 MCP 伺服器"""
|
| 663 |
+
server = self.servers.get(server_name)
|
| 664 |
+
if not server:
|
| 665 |
+
return f"❌ MCP 伺服器不存在: {server_name}(可用: {', '.join(self.servers.keys())})"
|
| 666 |
+
|
| 667 |
+
if server.get("type") == "http":
|
| 668 |
+
return self._call_http(server, method, params)
|
| 669 |
+
else:
|
| 670 |
+
return self._call_stdio(server_name, server, method, params)
|
| 671 |
+
|
| 672 |
+
def _call_http(self, server, method, params):
|
| 673 |
+
try:
|
| 674 |
+
if not httpx: return "❌ 需要 httpx"
|
| 675 |
+
resp = httpx.post(server["url"], json={
|
| 676 |
+
"jsonrpc": "2.0", "id": 1, "method": method,
|
| 677 |
+
"params": params or {}
|
| 678 |
+
}, timeout=30)
|
| 679 |
+
resp.raise_for_status()
|
| 680 |
+
result = resp.json()
|
| 681 |
+
return json.dumps(result.get("result", result), ensure_ascii=False, indent=2)
|
| 682 |
+
except Exception as e:
|
| 683 |
+
return f"❌ MCP HTTP 錯誤: {e}"
|
| 684 |
+
|
| 685 |
+
def _call_stdio(self, name, server, method, params):
|
| 686 |
+
try:
|
| 687 |
+
# 啟動進程(如果還沒啟動)
|
| 688 |
+
if name not in self.processes or self.processes[name].poll() is not None:
|
| 689 |
+
self.processes[name] = subprocess.Popen(
|
| 690 |
+
server["command"], shell=True,
|
| 691 |
+
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
| 692 |
+
stderr=subprocess.PIPE, text=True)
|
| 693 |
+
|
| 694 |
+
proc = self.processes[name]
|
| 695 |
+
request = json.dumps({"jsonrpc": "2.0", "id": 1, "method": method,
|
| 696 |
+
"params": params or {}}) + "\n"
|
| 697 |
+
proc.stdin.write(request)
|
| 698 |
+
proc.stdin.flush()
|
| 699 |
+
|
| 700 |
+
# 讀取回應(1 行 JSON)
|
| 701 |
+
import select
|
| 702 |
+
ready, _, _ = select.select([proc.stdout], [], [], 10)
|
| 703 |
+
if ready:
|
| 704 |
+
line = proc.stdout.readline()
|
| 705 |
+
result = json.loads(line)
|
| 706 |
+
return json.dumps(result.get("result", result), ensure_ascii=False, indent=2)
|
| 707 |
+
return "⏰ MCP 伺服器無回應"
|
| 708 |
+
except Exception as e:
|
| 709 |
+
return f"❌ MCP stdio 錯誤: {e}"
|
| 710 |
+
|
| 711 |
+
def list_servers(self):
|
| 712 |
+
if not self.servers: return "(無 MCP 伺服器。建立 .codepilot/mcp.json)"
|
| 713 |
+
lines = []
|
| 714 |
+
for name, cfg in self.servers.items():
|
| 715 |
+
stype = cfg.get("type", "stdio")
|
| 716 |
+
target = cfg.get("url", cfg.get("command", "?"))
|
| 717 |
+
lines.append(f" 🔌 {name} ({stype}): {target}")
|
| 718 |
+
return "\n".join(lines)
|
| 719 |
+
|
| 720 |
+
def cleanup(self):
|
| 721 |
+
for proc in self.processes.values():
|
| 722 |
+
try: proc.kill()
|
| 723 |
+
except: pass
|
| 724 |
|
| 725 |
|
| 726 |
# ============================================================
|
|
|
|
| 933 |
return None
|
| 934 |
|
| 935 |
|
| 936 |
+
# ============================================================
|
| 937 |
+
# SKILL SYSTEM(技能系統 — 仿 Claude Code SkillTool)
|
| 938 |
+
# ============================================================
|
| 939 |
+
"""
|
| 940 |
+
Skill 和 Agent 的關鍵差異(來自 Claude Code 原始碼):
|
| 941 |
+
- Skill → 注入指令到「當前」context window(不建新 context)
|
| 942 |
+
- Agent → spawn 一個「新的」隔離 context window
|
| 943 |
+
|
| 944 |
+
Skill 定義方式:
|
| 945 |
+
.codepilot/skills/<name>/SKILL.md
|
| 946 |
+
|
| 947 |
+
SKILL.md 格式:
|
| 948 |
+
---
|
| 949 |
+
name: API Generator
|
| 950 |
+
description: Generate RESTful API endpoints from a data model
|
| 951 |
+
tools: [read_file, edit_file, write_file, run_command]
|
| 952 |
+
arguments:
|
| 953 |
+
- name: model_file
|
| 954 |
+
description: Path to the data model file
|
| 955 |
+
- name: framework
|
| 956 |
+
description: Web framework (fastapi, express, gin)
|
| 957 |
+
default: fastapi
|
| 958 |
+
hooks:
|
| 959 |
+
post_edit_file: "black {file}"
|
| 960 |
+
---
|
| 961 |
+
|
| 962 |
+
你是一位 API 專家。根據用戶提供的 data model,生成完整的 RESTful CRUD API。
|
| 963 |
+
|
| 964 |
+
步驟:
|
| 965 |
+
1. 讀取 model_file 了解數據結構
|
| 966 |
+
2. 生成路由文件
|
| 967 |
+
3. 生成測試文件
|
| 968 |
+
4. 執行測試確認通過
|
| 969 |
+
|
| 970 |
+
內建 Skills(bundled):
|
| 971 |
+
- create-skill: 幫你建立新的 skill
|
| 972 |
+
- refactor: 重構程式碼
|
| 973 |
+
- test-gen: 自動產生測試
|
| 974 |
+
- doc-gen: 自動產生文檔
|
| 975 |
+
- debug: 除錯助手
|
| 976 |
+
"""
|
| 977 |
+
|
| 978 |
+
class SkillManager:
|
| 979 |
+
"""管理和執行 Skills"""
|
| 980 |
+
|
| 981 |
+
def __init__(self, project_dir):
|
| 982 |
+
self.project_dir = project_dir
|
| 983 |
+
self.skills = {}
|
| 984 |
+
|
| 985 |
+
# 載入自訂 skills
|
| 986 |
+
skills_dir = os.path.join(project_dir, ".codepilot", "skills")
|
| 987 |
+
if os.path.isdir(skills_dir):
|
| 988 |
+
for skill_dir in Path(skills_dir).iterdir():
|
| 989 |
+
if skill_dir.is_dir():
|
| 990 |
+
skill_md = skill_dir / "SKILL.md"
|
| 991 |
+
if skill_md.exists():
|
| 992 |
+
skill = self._parse_skill(skill_md)
|
| 993 |
+
if skill:
|
| 994 |
+
self.skills[skill["name"]] = skill
|
| 995 |
+
|
| 996 |
+
# 載入全域 skills
|
| 997 |
+
global_skills = CONFIG_DIR / "skills" if isinstance(CONFIG_DIR, Path) else Path(CONFIG_DIR) / "skills"
|
| 998 |
+
if global_skills.is_dir():
|
| 999 |
+
for skill_dir in global_skills.iterdir():
|
| 1000 |
+
if skill_dir.is_dir() and (skill_dir / "SKILL.md").exists():
|
| 1001 |
+
skill = self._parse_skill(skill_dir / "SKILL.md")
|
| 1002 |
+
if skill and skill["name"] not in self.skills:
|
| 1003 |
+
self.skills[skill["name"]] = skill
|
| 1004 |
+
|
| 1005 |
+
# 註冊內建 bundled skills
|
| 1006 |
+
self._register_bundled_skills()
|
| 1007 |
+
|
| 1008 |
+
def _parse_skill(self, skill_md_path):
|
| 1009 |
+
"""解析 SKILL.md"""
|
| 1010 |
+
try:
|
| 1011 |
+
content = Path(skill_md_path).read_text(encoding="utf-8")
|
| 1012 |
+
skill = {
|
| 1013 |
+
"name": skill_md_path.parent.name,
|
| 1014 |
+
"path": str(skill_md_path.parent),
|
| 1015 |
+
"description": "",
|
| 1016 |
+
"prompt": content,
|
| 1017 |
+
"tools": None, # None = 全部工具, list = 限定
|
| 1018 |
+
"arguments": [],
|
| 1019 |
+
"hooks": {},
|
| 1020 |
+
"model": None,
|
| 1021 |
+
"fork": False, # True = 在隔離 context 中執行
|
| 1022 |
+
}
|
| 1023 |
+
|
| 1024 |
+
# 解析 YAML frontmatter
|
| 1025 |
+
if content.startswith("---"):
|
| 1026 |
+
parts = content.split("---", 2)
|
| 1027 |
+
if len(parts) >= 3:
|
| 1028 |
+
for line in parts[1].strip().split("\n"):
|
| 1029 |
+
line = line.strip()
|
| 1030 |
+
if not line or line.startswith("#"):
|
| 1031 |
+
continue
|
| 1032 |
+
if ":" in line:
|
| 1033 |
+
k, v = line.split(":", 1)
|
| 1034 |
+
k, v = k.strip(), v.strip()
|
| 1035 |
+
if k == "name": skill["name"] = v
|
| 1036 |
+
elif k == "description": skill["description"] = v
|
| 1037 |
+
elif k == "model": skill["model"] = v
|
| 1038 |
+
elif k == "fork": skill["fork"] = v.lower() in ("true", "yes", "1")
|
| 1039 |
+
elif k == "tools":
|
| 1040 |
+
if v.startswith("["):
|
| 1041 |
+
skill["tools"] = [x.strip().strip("'\"") for x in v[1:-1].split(",")]
|
| 1042 |
+
elif k == "arguments":
|
| 1043 |
+
pass # 複雜結構,在下面處理
|
| 1044 |
+
elif k == "hooks":
|
| 1045 |
+
pass # 在下面處理
|
| 1046 |
+
|
| 1047 |
+
# 解析 arguments(簡易版)
|
| 1048 |
+
in_args = False
|
| 1049 |
+
current_arg = {}
|
| 1050 |
+
for line in parts[1].strip().split("\n"):
|
| 1051 |
+
line = line.strip()
|
| 1052 |
+
if line.startswith("arguments:"):
|
| 1053 |
+
in_args = True; continue
|
| 1054 |
+
if in_args:
|
| 1055 |
+
if line.startswith("- name:"):
|
| 1056 |
+
if current_arg: skill["arguments"].append(current_arg)
|
| 1057 |
+
current_arg = {"name": line.split(":", 1)[1].strip()}
|
| 1058 |
+
elif line.startswith("description:") and current_arg:
|
| 1059 |
+
current_arg["description"] = line.split(":", 1)[1].strip()
|
| 1060 |
+
elif line.startswith("default:") and current_arg:
|
| 1061 |
+
current_arg["default"] = line.split(":", 1)[1].strip()
|
| 1062 |
+
elif not line.startswith(" ") and not line.startswith("-"):
|
| 1063 |
+
in_args = False
|
| 1064 |
+
if current_arg and "name" in current_arg:
|
| 1065 |
+
skill["arguments"].append(current_arg)
|
| 1066 |
+
|
| 1067 |
+
# hooks
|
| 1068 |
+
in_hooks = False
|
| 1069 |
+
for line in parts[1].strip().split("\n"):
|
| 1070 |
+
line = line.strip()
|
| 1071 |
+
if line.startswith("hooks:"):
|
| 1072 |
+
in_hooks = True; continue
|
| 1073 |
+
if in_hooks and ":" in line and line.startswith(" "):
|
| 1074 |
+
hk, hv = line.strip().split(":", 1)
|
| 1075 |
+
skill["hooks"][hk.strip()] = hv.strip().strip('"').strip("'")
|
| 1076 |
+
elif in_hooks and not line.startswith(" "):
|
| 1077 |
+
in_hooks = False
|
| 1078 |
+
|
| 1079 |
+
skill["prompt"] = parts[2].strip()
|
| 1080 |
+
|
| 1081 |
+
return skill
|
| 1082 |
+
except Exception as e:
|
| 1083 |
+
return None
|
| 1084 |
+
|
| 1085 |
+
def _register_bundled_skills(self):
|
| 1086 |
+
"""註冊內建 skills"""
|
| 1087 |
+
bundled = {
|
| 1088 |
+
"create-skill": {
|
| 1089 |
+
"name": "create-skill",
|
| 1090 |
+
"description": "建立新的 skill",
|
| 1091 |
+
"prompt": """幫用戶在 .codepilot/skills/<name>/SKILL.md 建立一個新的 skill。
|
| 1092 |
+
|
| 1093 |
+
先問用戶:
|
| 1094 |
+
1. Skill 名稱
|
| 1095 |
+
2. 用途描述
|
| 1096 |
+
3. 需要用到哪些工具
|
| 1097 |
+
|
| 1098 |
+
然後��生 SKILL.md,包含 YAML frontmatter 和詳細指令。""",
|
| 1099 |
+
"tools": ["write_file", "list_files"],
|
| 1100 |
+
"arguments": [{"name": "name", "description": "skill 名稱"}],
|
| 1101 |
+
"hooks": {},
|
| 1102 |
+
"fork": False,
|
| 1103 |
+
"path": "(bundled)",
|
| 1104 |
+
},
|
| 1105 |
+
"refactor": {
|
| 1106 |
+
"name": "refactor",
|
| 1107 |
+
"description": "重構程式碼:提取函數、重命名、簡化邏輯",
|
| 1108 |
+
"prompt": """你是重構專家。閱讀用戶指定的文件,進行以下改進:
|
| 1109 |
+
1. 提取重複的程式碼為函數
|
| 1110 |
+
2. 改善命名(變數、函數、類別)
|
| 1111 |
+
3. 簡化複雜的條件邏輯
|
| 1112 |
+
4. 加入或改進 docstring
|
| 1113 |
+
5. 確保修改後測試仍然通過
|
| 1114 |
+
|
| 1115 |
+
每次只做一個小修改,驗證後再做下一個。""",
|
| 1116 |
+
"tools": ["read_file", "edit_file", "run_command", "search_files"],
|
| 1117 |
+
"arguments": [{"name": "file", "description": "要重構的文件路徑"}],
|
| 1118 |
+
"hooks": {},
|
| 1119 |
+
"fork": False,
|
| 1120 |
+
"path": "(bundled)",
|
| 1121 |
+
},
|
| 1122 |
+
"test-gen": {
|
| 1123 |
+
"name": "test-gen",
|
| 1124 |
+
"description": "自動產生測試",
|
| 1125 |
+
"prompt": """你是測試工程師。為用戶指定的文件或函數產生完整的測試。
|
| 1126 |
+
|
| 1127 |
+
步驟:
|
| 1128 |
+
1. 讀取原始碼,了解所有公開函數和類別
|
| 1129 |
+
2. 為每個函數產生:正常輸入、邊界值、錯誤輸入的測試
|
| 1130 |
+
3. 使用專案現有的測試框架(pytest/jest/等)
|
| 1131 |
+
4. 把測試寫入對應的 tests/ 目錄
|
| 1132 |
+
5. 執行測試確認通過""",
|
| 1133 |
+
"tools": ["read_file", "write_file", "run_command", "search_files", "list_files"],
|
| 1134 |
+
"arguments": [{"name": "file", "description": "要產生測試的文件"}],
|
| 1135 |
+
"hooks": {},
|
| 1136 |
+
"fork": False,
|
| 1137 |
+
"path": "(bundled)",
|
| 1138 |
+
},
|
| 1139 |
+
"doc-gen": {
|
| 1140 |
+
"name": "doc-gen",
|
| 1141 |
+
"description": "自動產生文檔(docstring / README / API docs)",
|
| 1142 |
+
"prompt": """你是技術文件專家。為用戶的程式碼產生或改善文檔。
|
| 1143 |
+
|
| 1144 |
+
可以:
|
| 1145 |
+
1. 為所有函數加上 docstring
|
| 1146 |
+
2. 產生或更新 README.md
|
| 1147 |
+
3. 產生 API 文檔(如有 web framework)
|
| 1148 |
+
4. 產生 CHANGELOG
|
| 1149 |
+
|
| 1150 |
+
根據用戶的要求決定做哪個。""",
|
| 1151 |
+
"tools": ["read_file", "edit_file", "write_file", "search_files", "list_files"],
|
| 1152 |
+
"arguments": [{"name": "target", "description": "文件或目錄", "default": "."}],
|
| 1153 |
+
"hooks": {},
|
| 1154 |
+
"fork": False,
|
| 1155 |
+
"path": "(bundled)",
|
| 1156 |
+
},
|
| 1157 |
+
"debug": {
|
| 1158 |
+
"name": "debug",
|
| 1159 |
+
"description": "除錯助手:分析錯誤訊息、找出原因、修復",
|
| 1160 |
+
"prompt": """你是除錯專家。用戶會給你一個錯誤訊息或描述問題。
|
| 1161 |
+
|
| 1162 |
+
步驟:
|
| 1163 |
+
1. 分析錯誤訊息,定位問題文件和行數
|
| 1164 |
+
2. 讀取相關程式碼
|
| 1165 |
+
3. 搜尋可能相關的其他文件
|
| 1166 |
+
4. 找出根本原因
|
| 1167 |
+
5. 提出修復方案
|
| 1168 |
+
6. 實施修復
|
| 1169 |
+
7. 跑測試驗證
|
| 1170 |
+
|
| 1171 |
+
先分析再動手,不要急著改。""",
|
| 1172 |
+
"tools": ["read_file", "edit_file", "run_command", "search_files", "list_files", "git_status"],
|
| 1173 |
+
"arguments": [{"name": "error", "description": "錯誤訊息或問題描述"}],
|
| 1174 |
+
"hooks": {},
|
| 1175 |
+
"fork": False,
|
| 1176 |
+
"path": "(bundled)",
|
| 1177 |
+
},
|
| 1178 |
+
}
|
| 1179 |
+
for name, skill in bundled.items():
|
| 1180 |
+
if name not in self.skills:
|
| 1181 |
+
self.skills[name] = skill
|
| 1182 |
+
|
| 1183 |
+
def list_skills(self):
|
| 1184 |
+
"""列出所有可用 skills"""
|
| 1185 |
+
lines = []
|
| 1186 |
+
bundled = []
|
| 1187 |
+
custom = []
|
| 1188 |
+
for name, s in sorted(self.skills.items()):
|
| 1189 |
+
icon = "📦" if s.get("path") == "(bundled)" else "🔧"
|
| 1190 |
+
desc = s.get("description", "")
|
| 1191 |
+
args = ", ".join(a["name"] for a in s.get("arguments", []))
|
| 1192 |
+
entry = f" {icon} {name}: {desc}"
|
| 1193 |
+
if args: entry += f" [dim]({args})[/]"
|
| 1194 |
+
if s.get("path") == "(bundled)":
|
| 1195 |
+
bundled.append(entry)
|
| 1196 |
+
else:
|
| 1197 |
+
custom.append(entry)
|
| 1198 |
+
if custom:
|
| 1199 |
+
lines.append("[bold]自訂 Skills:[/]")
|
| 1200 |
+
lines.extend(custom)
|
| 1201 |
+
if bundled:
|
| 1202 |
+
lines.append("[bold]內建 Skills:[/]")
|
| 1203 |
+
lines.extend(bundled)
|
| 1204 |
+
return "\n".join(lines) if lines else "(無 skill。用 /skill create-skill 建立)"
|
| 1205 |
+
|
| 1206 |
+
def invoke(self, skill_name, args_dict, model, tools, console, messages=None):
|
| 1207 |
+
"""
|
| 1208 |
+
執行 skill。
|
| 1209 |
+
核心差異:skill 注入到當前 context(不像 agent 建新 context)
|
| 1210 |
+
"""
|
| 1211 |
+
from rich.markdown import Markdown
|
| 1212 |
+
|
| 1213 |
+
skill = self.skills.get(skill_name)
|
| 1214 |
+
if not skill:
|
| 1215 |
+
console.print(f"[red]❌ 未知 skill: {skill_name}[/]")
|
| 1216 |
+
console.print(self.list_skills())
|
| 1217 |
+
return None, None
|
| 1218 |
+
|
| 1219 |
+
console.print(f"\n[bold magenta]⚡ Skill: {skill['name']}[/] — {skill.get('description','')}")
|
| 1220 |
+
|
| 1221 |
+
# 組裝 skill prompt + 用戶參數
|
| 1222 |
+
skill_prompt = skill["prompt"]
|
| 1223 |
+
|
| 1224 |
+
# 替換參數
|
| 1225 |
+
for arg_def in skill.get("arguments", []):
|
| 1226 |
+
arg_name = arg_def["name"]
|
| 1227 |
+
arg_val = args_dict.get(arg_name, arg_def.get("default", ""))
|
| 1228 |
+
skill_prompt = skill_prompt.replace(f"{{{arg_name}}}", str(arg_val))
|
| 1229 |
+
|
| 1230 |
+
if skill.get("fork"):
|
| 1231 |
+
# Fork 模式:隔離 context(像 agent)
|
| 1232 |
+
console.print(f" [dim](fork mode — 隔離 context)[/]")
|
| 1233 |
+
fork_messages = [
|
| 1234 |
+
{"role": "system", "content": skill_prompt},
|
| 1235 |
+
{"role": "user", "content": json.dumps(args_dict, ensure_ascii=False)},
|
| 1236 |
+
]
|
| 1237 |
+
full_response = ""
|
| 1238 |
+
for rnd in range(8):
|
| 1239 |
+
with console.status(f"[magenta]{skill_name} round {rnd+1}..."):
|
| 1240 |
+
try: response = model.chat(fork_messages)
|
| 1241 |
+
except: break
|
| 1242 |
+
tcalls = parse_tool_calls(response)
|
| 1243 |
+
text = TOOL_PATTERN.sub("", response).strip()
|
| 1244 |
+
if text: console.print(Markdown(text))
|
| 1245 |
+
full_response += response + "\n"
|
| 1246 |
+
if not tcalls: break
|
| 1247 |
+
fork_messages.append({"role": "assistant", "content": response})
|
| 1248 |
+
results = []
|
| 1249 |
+
for call in tcalls:
|
| 1250 |
+
# 工具權限過濾
|
| 1251 |
+
if skill.get("tools") and call["tool"] not in skill["tools"]:
|
| 1252 |
+
results.append(f"[{call['tool']}] ❌ 此 skill 不允許"); continue
|
| 1253 |
+
result = execute_tool(tools, call)
|
| 1254 |
+
console.print(f" [dim]🔧 {call['tool']}[/]")
|
| 1255 |
+
results.append(f"[{call['tool']}] {result}")
|
| 1256 |
+
# 觸發 skill 自帶的 hooks
|
| 1257 |
+
if call["tool"] in ("edit_file", "write_file"):
|
| 1258 |
+
fpath = call["params"].get("path", "")
|
| 1259 |
+
hook_cmd = skill.get("hooks", {}).get(f"post_{call['tool']}")
|
| 1260 |
+
if hook_cmd and fpath:
|
| 1261 |
+
subprocess.run(hook_cmd.replace("{file}", fpath), shell=True,
|
| 1262 |
+
cwd=tools.project_dir, capture_output=True, timeout=30)
|
| 1263 |
+
fork_messages.append({"role": "user", "content": "Tool results:\n" + "\n\n".join(results)})
|
| 1264 |
+
return full_response, None
|
| 1265 |
+
else:
|
| 1266 |
+
# 注入模式(預設):把 skill 指令注入當前 context
|
| 1267 |
+
inject_msg = f"[Skill: {skill_name}]\n\n{skill_prompt}\n\nUser arguments: {json.dumps(args_dict, ensure_ascii=False)}"
|
| 1268 |
+
return None, inject_msg # 回傳注入內容,由主循環處理
|
| 1269 |
+
|
| 1270 |
+
|
| 1271 |
# ============================================================
|
| 1272 |
# P1-1: APPROVAL SYSTEM(權限/審批)
|
| 1273 |
# ============================================================
|
|
|
|
| 1708 |
messages = [{"role": "system", "content": system_prompt}]
|
| 1709 |
|
| 1710 |
hooks = Hooks(project_dir)
|
| 1711 |
+
bg_tasks = BackgroundTaskManager()
|
| 1712 |
+
custom_agents = load_custom_agents(project_dir)
|
| 1713 |
+
mcp = MCPLite(project_dir)
|
| 1714 |
+
skill_mgr = SkillManager(project_dir) # Skill 系統
|
| 1715 |
+
approval_mode = args.approval or "auto"
|
| 1716 |
+
use_streaming = args.stream and provider_key == "local"
|
| 1717 |
edited_files_this_session = []
|
| 1718 |
|
| 1719 |
if custom_agents:
|
| 1720 |
console.print(f"[dim]🤖 自訂代理: {', '.join(custom_agents.keys())}[/]")
|
| 1721 |
|
| 1722 |
+
console.print(f"[dim]指令: /init /verify /commit /agent /bg /approval /web /mcp /stream | /duel /memo /grind /ls /git /clear /status /train /quit[/]\n")
|
| 1723 |
|
| 1724 |
while True:
|
| 1725 |
try: user_input = Prompt.ask("\n[bold green]🧑 You")
|
|
|
|
| 1802 |
auto_git_commit(tools, model, edited_files_this_session, console)
|
| 1803 |
continue
|
| 1804 |
|
| 1805 |
+
elif cmd.startswith("/skill"):
|
| 1806 |
+
# Skill 系統
|
| 1807 |
+
parts = cmd.split(None, 2)
|
| 1808 |
+
if len(parts) < 2 or parts[1] == "list":
|
| 1809 |
+
console.print(skill_mgr.list_skills())
|
| 1810 |
+
continue
|
| 1811 |
+
skill_name = parts[1]
|
| 1812 |
+
# 收集參數
|
| 1813 |
+
skill_def = skill_mgr.skills.get(skill_name)
|
| 1814 |
+
skill_args = {}
|
| 1815 |
+
if skill_def:
|
| 1816 |
+
# 如果指令裡有第三段,用它作為第一個參數
|
| 1817 |
+
if len(parts) > 2 and skill_def.get("arguments"):
|
| 1818 |
+
skill_args[skill_def["arguments"][0]["name"]] = parts[2]
|
| 1819 |
+
else:
|
| 1820 |
+
for arg_def in skill_def.get("arguments", []):
|
| 1821 |
+
default = arg_def.get("default", "")
|
| 1822 |
+
val = Prompt.ask(f" {arg_def['name']} ({arg_def.get('description','')})", default=default)
|
| 1823 |
+
if val: skill_args[arg_def["name"]] = val
|
| 1824 |
+
result, inject = skill_mgr.invoke(skill_name, skill_args, model, tools, console, messages)
|
| 1825 |
+
if inject:
|
| 1826 |
+
# 注入模式:加入當前對話
|
| 1827 |
+
messages.append({"role": "user", "content": inject})
|
| 1828 |
+
with console.status("[bold cyan]執行 skill..."):
|
| 1829 |
+
response = chat_with_recovery(model, messages, ctx=ctx, console=console)
|
| 1830 |
+
console.print(f"\n[bold blue]🤖 CodePilot:[/]")
|
| 1831 |
+
from rich.markdown import Markdown as _Md
|
| 1832 |
+
console.print(_Md(TOOL_PATTERN.sub("", response).strip()))
|
| 1833 |
+
messages.append({"role": "assistant", "content": response})
|
| 1834 |
+
# 處理工具呼叫
|
| 1835 |
+
tcalls = parse_tool_calls(response)
|
| 1836 |
+
if tcalls:
|
| 1837 |
+
results = []
|
| 1838 |
+
for call in tcalls:
|
| 1839 |
+
console.print(f" [dim]🔧 {call['tool']}[/]")
|
| 1840 |
+
r = execute_tool(tools, call)
|
| 1841 |
+
results.append(f"[{call['tool']}] {r}")
|
| 1842 |
+
messages.append({"role": "user", "content": "Tool results:\n" + "\n\n".join(results)})
|
| 1843 |
+
continue
|
| 1844 |
+
|
| 1845 |
elif cmd.startswith("/agent"):
|
| 1846 |
# P1-3: 自訂代理
|
| 1847 |
parts = cmd.split(None, 2)
|
|
|
|
| 1899 |
console.print("[dim]/bg list | /bg run <cmd> | /bg check <id> | /bg kill <id>[/]")
|
| 1900 |
continue
|
| 1901 |
|
| 1902 |
+
elif cmd.startswith("/web "):
|
| 1903 |
+
# P2-1: 快速網頁搜尋/抓取
|
| 1904 |
+
query = cmd[5:].strip()
|
| 1905 |
+
if query.startswith("http"):
|
| 1906 |
+
console.print(f"[dim]🌐 抓取 {query}...[/]")
|
| 1907 |
+
result = web_fetch(query)
|
| 1908 |
+
else:
|
| 1909 |
+
console.print(f"[dim]🔍 搜尋: {query}...[/]")
|
| 1910 |
+
result = web_search(query)
|
| 1911 |
+
console.print(result[:2000])
|
| 1912 |
+
continue
|
| 1913 |
+
|
| 1914 |
+
elif cmd.startswith("/mcp"):
|
| 1915 |
+
# P2-5: MCP 伺服器管理
|
| 1916 |
+
parts = cmd.split(None, 3)
|
| 1917 |
+
if len(parts) < 2 or parts[1] == "list":
|
| 1918 |
+
console.print(f"[bold]🔌 MCP 伺服器[/]")
|
| 1919 |
+
console.print(mcp.list_servers())
|
| 1920 |
+
elif len(parts) >= 3:
|
| 1921 |
+
server = parts[1]
|
| 1922 |
+
method = parts[2]
|
| 1923 |
+
params = json.loads(parts[3]) if len(parts) > 3 else {}
|
| 1924 |
+
console.print(f"[dim]🔌 {server}.{method}...[/]")
|
| 1925 |
+
result = mcp.call(server, method, params)
|
| 1926 |
+
console.print(result[:1000])
|
| 1927 |
+
else:
|
| 1928 |
+
console.print("[dim]/mcp list | /mcp <server> <method> [json_params][/]")
|
| 1929 |
+
continue
|
| 1930 |
+
|
| 1931 |
+
elif cmd == "/stream on":
|
| 1932 |
+
use_streaming = (provider_key == "local")
|
| 1933 |
+
console.print(f"[green]{'✅ Streaming ON' if use_streaming else '❌ Streaming 只支援本地模型'}[/]")
|
| 1934 |
+
continue
|
| 1935 |
+
elif cmd == "/stream off":
|
| 1936 |
+
use_streaming = False; console.print("[dim]Streaming OFF[/]"); continue
|
| 1937 |
+
|
| 1938 |
elif cmd.startswith("/approval"):
|
| 1939 |
# P1-1: 切換審批模式
|
| 1940 |
parts = cmd.split()
|
|
|
|
| 2029 |
tools_used_this_turn = [] # 追蹤這輪用了哪些工具
|
| 2030 |
|
| 2031 |
for rnd in range(10):
|
| 2032 |
+
try:
|
| 2033 |
+
if use_streaming and rnd == 0 and provider_key == "local":
|
| 2034 |
+
# P2-2: Streaming 輸出(第一輪,本地模型)
|
| 2035 |
+
response = stream_local_chat(model, messages, console)
|
| 2036 |
+
else:
|
| 2037 |
+
with console.status(f"[bold cyan]{'思考中' if rnd == 0 else f'工具 round {rnd+1}'}..."):
|
| 2038 |
+
response = chat_with_recovery(
|
| 2039 |
+
model, messages, ctx=ctx, console=console,
|
| 2040 |
+
fallback_model=local_model_ref if provider_key != "local" else None)
|
| 2041 |
+
except Exception as e:
|
| 2042 |
+
console.print(f"[red]❌ 所有重試失敗: {e}[/]")
|
| 2043 |
+
break
|
| 2044 |
|
| 2045 |
tool_calls = parse_tool_calls(response)
|
| 2046 |
text_parts = TOOL_PATTERN.sub("", response).strip()
|
| 2047 |
+
if text_parts and not (use_streaming and rnd == 0):
|
| 2048 |
+
# streaming 模式已經顯示過了,不��複
|
| 2049 |
console.print(f"\n[bold blue]🤖 CodePilot:[/]")
|
| 2050 |
console.print(Markdown(text_parts))
|
| 2051 |
full_response += response + "\n"
|
|
|
|
| 2121 |
|
| 2122 |
ctx.save_session(messages)
|
| 2123 |
|
| 2124 |
+
mcp.cleanup()
|
| 2125 |
console.print("\n[cyan]👋[/]")
|
| 2126 |
|
| 2127 |
|
|
|
|
| 2178 |
p.add_argument("--distill", action="store_true")
|
| 2179 |
p.add_argument("--grind", action="store_true", help="LeetCode 自動刷題")
|
| 2180 |
p.add_argument("--grind-count", type=int, default=100, help="刷幾題")
|
| 2181 |
+
p.add_argument("--stream", action="store_true", help="啟用 streaming 輸出(本地模型)")
|
| 2182 |
p.add_argument("--stats", action="store_true"); p.add_argument("--train", action="store_true")
|
| 2183 |
a = p.parse_args()
|
| 2184 |
if a.stats: show_stats()
|