Spaces:
Sleeping
Sleeping
P0+P1 complete: approval, background tasks, custom agents, auto git commit
Browse files- codepilot_v4.py +316 -4
codepilot_v4.py
CHANGED
|
@@ -645,7 +645,6 @@ class Hooks:
|
|
| 645 |
cmd_template = self.hooks.get(event)
|
| 646 |
if not cmd_template:
|
| 647 |
return None
|
| 648 |
-
# 替換變數
|
| 649 |
cmd = cmd_template
|
| 650 |
if context:
|
| 651 |
for k, v in context.items():
|
|
@@ -658,6 +657,230 @@ class Hooks:
|
|
| 658 |
return None
|
| 659 |
|
| 660 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 661 |
def build_system_prompt(tools, project_memory=""):
|
| 662 |
memory_section = f"\n\n## Project Memory (CODEPILOT.md)\n{project_memory}" if project_memory else ""
|
| 663 |
return f"""You are CodePilot, an expert AI programming assistant working in the user's project.
|
|
@@ -873,10 +1096,16 @@ def run_agent_loop(args):
|
|
| 873 |
else:
|
| 874 |
messages = [{"role": "system", "content": system_prompt}]
|
| 875 |
|
| 876 |
-
hooks = Hooks(project_dir)
|
| 877 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 878 |
|
| 879 |
-
console.print("[dim]/init /
|
| 880 |
|
| 881 |
while True:
|
| 882 |
try: user_input = Prompt.ask("\n[bold green]🧑 You")
|
|
@@ -954,6 +1183,81 @@ def run_agent_loop(args):
|
|
| 954 |
n = Prompt.ask("刷幾題?", default="50")
|
| 955 |
run_grind(args, int(n)); continue
|
| 956 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 957 |
elif cmd == "/status":
|
| 958 |
s = db.count()
|
| 959 |
t = Table(title="📊 統計"); t.add_column("", style="cyan"); t.add_column("", style="green")
|
|
@@ -1057,6 +1361,12 @@ def run_agent_loop(args):
|
|
| 1057 |
results = []
|
| 1058 |
for call in tool_calls:
|
| 1059 |
console.print(f" [dim]🔧 {call['tool']}[/]")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1060 |
result = execute_tool(tools, call) # 已含 P0-2 截斷
|
| 1061 |
tools_used_this_turn.append(call["tool"])
|
| 1062 |
|
|
@@ -1168,6 +1478,8 @@ def main():
|
|
| 1168 |
help="模型: local, openai, anthropic, openrouter, ollama, codex")
|
| 1169 |
p.add_argument("--api-key", type=str); p.add_argument("--cloud-model", type=str)
|
| 1170 |
p.add_argument("--duel", action="store_true", help="啟動時開啟 Duel 模式")
|
|
|
|
|
|
|
| 1171 |
p.add_argument("--distill", action="store_true")
|
| 1172 |
p.add_argument("--grind", action="store_true", help="LeetCode 自動刷題")
|
| 1173 |
p.add_argument("--grind-count", type=int, default=100, help="刷幾題")
|
|
|
|
| 645 |
cmd_template = self.hooks.get(event)
|
| 646 |
if not cmd_template:
|
| 647 |
return None
|
|
|
|
| 648 |
cmd = cmd_template
|
| 649 |
if context:
|
| 650 |
for k, v in context.items():
|
|
|
|
| 657 |
return None
|
| 658 |
|
| 659 |
|
| 660 |
+
# ============================================================
|
| 661 |
+
# P1-1: APPROVAL SYSTEM(權限/審批)
|
| 662 |
+
# ============================================================
|
| 663 |
+
APPROVAL_MODES = {
|
| 664 |
+
"auto": "全自動(只擋危險指令)",
|
| 665 |
+
"auto-edit": "文件修改自動,shell 指令要確認",
|
| 666 |
+
"ask": "每次工具呼叫都確認",
|
| 667 |
+
}
|
| 668 |
+
|
| 669 |
+
# 不需要確認的工具(只讀)
|
| 670 |
+
SAFE_TOOLS = {"read_file", "search_files", "list_files", "git_status"}
|
| 671 |
+
|
| 672 |
+
def check_approval(tool_name, params, approval_mode, console):
|
| 673 |
+
"""檢查工具是否需要用戶確認。回傳 True = 允許, False = 拒絕"""
|
| 674 |
+
if approval_mode == "auto":
|
| 675 |
+
return True # 全自動(危險指令在 run_command 裡已經擋了)
|
| 676 |
+
if tool_name in SAFE_TOOLS:
|
| 677 |
+
return True # 只讀工具永遠通過
|
| 678 |
+
if approval_mode == "auto-edit" and tool_name in ("edit_file", "write_file"):
|
| 679 |
+
return True # auto-edit 模式下文件修改自動通過
|
| 680 |
+
|
| 681 |
+
# 需要用戶確認
|
| 682 |
+
from rich.prompt import Confirm
|
| 683 |
+
param_preview = json.dumps(params, ensure_ascii=False)[:120]
|
| 684 |
+
console.print(f" [yellow]⚠️ {tool_name}({param_preview})[/]")
|
| 685 |
+
return Confirm.ask(" 允許執行?", default=True)
|
| 686 |
+
|
| 687 |
+
|
| 688 |
+
# ============================================================
|
| 689 |
+
# P1-2: BACKGROUND TASKS(背景任務管理)
|
| 690 |
+
# ============================================================
|
| 691 |
+
import threading, uuid as _uuid
|
| 692 |
+
|
| 693 |
+
class BackgroundTaskManager:
|
| 694 |
+
"""背景任務管理器 — 長時間指令不阻塞主循環"""
|
| 695 |
+
|
| 696 |
+
def __init__(self):
|
| 697 |
+
self._tasks = {} # id → {process, command, start_time, output}
|
| 698 |
+
|
| 699 |
+
def start(self, command, cwd):
|
| 700 |
+
"""啟動背景任務"""
|
| 701 |
+
task_id = str(_uuid.uuid4())[:6]
|
| 702 |
+
proc = subprocess.Popen(
|
| 703 |
+
command, shell=True, cwd=cwd,
|
| 704 |
+
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
| 705 |
+
text=True)
|
| 706 |
+
self._tasks[task_id] = {
|
| 707 |
+
"process": proc, "command": command,
|
| 708 |
+
"start_time": datetime.now(), "output_lines": []
|
| 709 |
+
}
|
| 710 |
+
# 背景讀取輸出
|
| 711 |
+
def _reader():
|
| 712 |
+
for line in proc.stdout:
|
| 713 |
+
self._tasks[task_id]["output_lines"].append(line)
|
| 714 |
+
threading.Thread(target=_reader, daemon=True).start()
|
| 715 |
+
return task_id
|
| 716 |
+
|
| 717 |
+
def check(self, task_id):
|
| 718 |
+
"""檢查任務狀態"""
|
| 719 |
+
t = self._tasks.get(task_id)
|
| 720 |
+
if not t: return {"status": "not_found"}
|
| 721 |
+
running = t["process"].poll() is None
|
| 722 |
+
elapsed = (datetime.now() - t["start_time"]).seconds
|
| 723 |
+
output = "".join(t["output_lines"][-20:]) # 最後 20 行
|
| 724 |
+
return {
|
| 725 |
+
"status": "running" if running else "done",
|
| 726 |
+
"exit_code": t["process"].returncode,
|
| 727 |
+
"elapsed": elapsed,
|
| 728 |
+
"output": output,
|
| 729 |
+
"command": t["command"],
|
| 730 |
+
}
|
| 731 |
+
|
| 732 |
+
def list_tasks(self):
|
| 733 |
+
"""列出所有背景任務"""
|
| 734 |
+
results = []
|
| 735 |
+
for tid, t in self._tasks.items():
|
| 736 |
+
running = t["process"].poll() is None
|
| 737 |
+
elapsed = (datetime.now() - t["start_time"]).seconds
|
| 738 |
+
results.append(f" {'🟢' if running else '⚫'} {tid}: {t['command'][:50]} ({elapsed}s)")
|
| 739 |
+
return "\n".join(results) if results else " (無背景任務)"
|
| 740 |
+
|
| 741 |
+
def kill(self, task_id):
|
| 742 |
+
"""終止任務"""
|
| 743 |
+
t = self._tasks.get(task_id)
|
| 744 |
+
if t and t["process"].poll() is None:
|
| 745 |
+
t["process"].kill()
|
| 746 |
+
return True
|
| 747 |
+
return False
|
| 748 |
+
|
| 749 |
+
|
| 750 |
+
# ============================================================
|
| 751 |
+
# P1-3: CUSTOM AGENTS(自訂代理 .codepilot/agents/*.md)
|
| 752 |
+
# ============================================================
|
| 753 |
+
def load_custom_agents(project_dir):
|
| 754 |
+
"""載入 .codepilot/agents/*.md 自訂代理"""
|
| 755 |
+
agents_dir = os.path.join(project_dir, ".codepilot", "agents")
|
| 756 |
+
agents = {}
|
| 757 |
+
if not os.path.isdir(agents_dir):
|
| 758 |
+
return agents
|
| 759 |
+
for f in sorted(Path(agents_dir).glob("*.md")):
|
| 760 |
+
content = f.read_text(encoding="utf-8")
|
| 761 |
+
name = f.stem
|
| 762 |
+
# 解析 YAML frontmatter
|
| 763 |
+
config = {"name": name, "prompt": content}
|
| 764 |
+
if content.startswith("---"):
|
| 765 |
+
parts = content.split("---", 2)
|
| 766 |
+
if len(parts) >= 3:
|
| 767 |
+
try:
|
| 768 |
+
# 簡易 YAML 解析
|
| 769 |
+
for line in parts[1].strip().split("\n"):
|
| 770 |
+
if ":" in line:
|
| 771 |
+
k, v = line.split(":", 1)
|
| 772 |
+
k, v = k.strip(), v.strip()
|
| 773 |
+
if v.startswith("[") and v.endswith("]"):
|
| 774 |
+
v = [x.strip().strip("'\"") for x in v[1:-1].split(",")]
|
| 775 |
+
config[k] = v
|
| 776 |
+
except: pass
|
| 777 |
+
config["prompt"] = parts[2].strip()
|
| 778 |
+
agents[name] = config
|
| 779 |
+
return agents
|
| 780 |
+
|
| 781 |
+
|
| 782 |
+
def run_custom_agent(agent_config, user_task, model, tools, console):
|
| 783 |
+
"""執行自訂代理"""
|
| 784 |
+
from rich.markdown import Markdown
|
| 785 |
+
name = agent_config["name"]
|
| 786 |
+
prompt = agent_config["prompt"]
|
| 787 |
+
allowed = agent_config.get("tools") # list or None
|
| 788 |
+
denied = agent_config.get("disallowedTools", [])
|
| 789 |
+
|
| 790 |
+
console.print(f"\n[bold magenta]🤖 Agent: {name}[/]")
|
| 791 |
+
|
| 792 |
+
agent_messages = [
|
| 793 |
+
{"role": "system", "content": prompt},
|
| 794 |
+
{"role": "user", "content": user_task},
|
| 795 |
+
]
|
| 796 |
+
|
| 797 |
+
full_response = ""
|
| 798 |
+
for rnd in range(5): # 子代理最多 5 輪
|
| 799 |
+
with console.status(f"[magenta]{name} 思考中 (round {rnd+1})..."):
|
| 800 |
+
try: response = model.chat(agent_messages)
|
| 801 |
+
except Exception as e: console.print(f"[red]❌ {e}[/]"); break
|
| 802 |
+
|
| 803 |
+
tool_calls = parse_tool_calls(response)
|
| 804 |
+
text_parts = TOOL_PATTERN.sub("", response).strip()
|
| 805 |
+
if text_parts:
|
| 806 |
+
console.print(f" [magenta][{name}][/] {text_parts[:300]}")
|
| 807 |
+
full_response += response + "\n"
|
| 808 |
+
if not tool_calls: break
|
| 809 |
+
|
| 810 |
+
agent_messages.append({"role": "assistant", "content": response})
|
| 811 |
+
results = []
|
| 812 |
+
for call in tool_calls:
|
| 813 |
+
# 權限檢查
|
| 814 |
+
if allowed and call["tool"] not in allowed:
|
| 815 |
+
results.append(f"[{call['tool']}] ❌ 此代理不允許使用 {call['tool']}")
|
| 816 |
+
continue
|
| 817 |
+
if call["tool"] in denied:
|
| 818 |
+
results.append(f"[{call['tool']}] ❌ 此代理禁止使用 {call['tool']}")
|
| 819 |
+
continue
|
| 820 |
+
result = execute_tool(tools, call)
|
| 821 |
+
results.append(f"[{call['tool']}] {result}")
|
| 822 |
+
agent_messages.append({"role": "user", "content": "Tool results:\n" + "\n\n".join(results)})
|
| 823 |
+
|
| 824 |
+
return full_response
|
| 825 |
+
|
| 826 |
+
|
| 827 |
+
# ============================================================
|
| 828 |
+
# P1-4: AUTO GIT COMMIT
|
| 829 |
+
# ============================================================
|
| 830 |
+
def auto_git_commit(tools, model, edited_files, console):
|
| 831 |
+
"""自動 stage 修改的文件並 commit"""
|
| 832 |
+
if not edited_files:
|
| 833 |
+
console.print("[dim]沒有修改的文件[/]")
|
| 834 |
+
return
|
| 835 |
+
|
| 836 |
+
# 只 stage 明確修改過的文件(不用 git add -A)
|
| 837 |
+
rel_files = []
|
| 838 |
+
for f in edited_files:
|
| 839 |
+
try:
|
| 840 |
+
rel = os.path.relpath(f, tools.project_dir)
|
| 841 |
+
rel_files.append(rel)
|
| 842 |
+
except: continue
|
| 843 |
+
|
| 844 |
+
if not rel_files:
|
| 845 |
+
return
|
| 846 |
+
|
| 847 |
+
console.print(f" [dim]📁 Stage: {', '.join(rel_files[:5])}{'...' if len(rel_files)>5 else ''}[/]")
|
| 848 |
+
|
| 849 |
+
# git add 個別文件
|
| 850 |
+
for f in rel_files:
|
| 851 |
+
subprocess.run(["git", "add", f], cwd=tools.project_dir, capture_output=True)
|
| 852 |
+
|
| 853 |
+
# 用模型生成 commit message
|
| 854 |
+
diff = subprocess.run(["git", "diff", "--cached", "--stat"],
|
| 855 |
+
cwd=tools.project_dir, capture_output=True, text=True).stdout
|
| 856 |
+
|
| 857 |
+
with console.status("[dim]生成 commit message..."):
|
| 858 |
+
msg_prompt = f"Generate a concise git commit message (1 line, max 72 chars) for:\n\n{diff[:2000]}"
|
| 859 |
+
try:
|
| 860 |
+
commit_msg = model.chat([{"role": "user", "content": msg_prompt}], max_tokens=100)
|
| 861 |
+
# 清理:取第一行,去掉引號
|
| 862 |
+
commit_msg = commit_msg.strip().split("\n")[0].strip('"').strip("'")
|
| 863 |
+
if len(commit_msg) > 72: commit_msg = commit_msg[:69] + "..."
|
| 864 |
+
except:
|
| 865 |
+
commit_msg = f"codepilot: update {len(rel_files)} file(s)"
|
| 866 |
+
|
| 867 |
+
console.print(f" [dim]💬 {commit_msg}[/]")
|
| 868 |
+
|
| 869 |
+
from rich.prompt import Confirm
|
| 870 |
+
if Confirm.ask(" Commit?", default=True):
|
| 871 |
+
result = subprocess.run(["git", "commit", "-m", commit_msg],
|
| 872 |
+
cwd=tools.project_dir, capture_output=True, text=True)
|
| 873 |
+
if result.returncode == 0:
|
| 874 |
+
console.print(f" [green]✅ Committed[/]")
|
| 875 |
+
else:
|
| 876 |
+
console.print(f" [red]❌ {result.stderr[:200]}[/]")
|
| 877 |
+
else:
|
| 878 |
+
# unstage
|
| 879 |
+
subprocess.run(["git", "reset", "HEAD"] + rel_files,
|
| 880 |
+
cwd=tools.project_dir, capture_output=True)
|
| 881 |
+
console.print(" [dim]已取消[/]")
|
| 882 |
+
|
| 883 |
+
|
| 884 |
def build_system_prompt(tools, project_memory=""):
|
| 885 |
memory_section = f"\n\n## Project Memory (CODEPILOT.md)\n{project_memory}" if project_memory else ""
|
| 886 |
return f"""You are CodePilot, an expert AI programming assistant working in the user's project.
|
|
|
|
| 1096 |
else:
|
| 1097 |
messages = [{"role": "system", "content": system_prompt}]
|
| 1098 |
|
| 1099 |
+
hooks = Hooks(project_dir)
|
| 1100 |
+
bg_tasks = BackgroundTaskManager() # P1-2: 背景任務
|
| 1101 |
+
custom_agents = load_custom_agents(project_dir) # P1-3: 自訂代理
|
| 1102 |
+
approval_mode = args.approval or "auto" # P1-1: 審批模式
|
| 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")
|
|
|
|
| 1183 |
n = Prompt.ask("刷幾題?", default="50")
|
| 1184 |
run_grind(args, int(n)); continue
|
| 1185 |
|
| 1186 |
+
elif cmd == "/commit":
|
| 1187 |
+
# P1-4: 自動 git commit
|
| 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)
|
| 1194 |
+
if len(parts) < 2:
|
| 1195 |
+
console.print("[bold]可用代理:[/]")
|
| 1196 |
+
if custom_agents:
|
| 1197 |
+
for name, cfg in custom_agents.items():
|
| 1198 |
+
desc = cfg.get("description", "")
|
| 1199 |
+
console.print(f" 🤖 {name}: {desc}")
|
| 1200 |
+
console.print(f"\n[dim]用法: /agent <名稱> <任務>[/]")
|
| 1201 |
+
else:
|
| 1202 |
+
console.print("[dim]無自訂代理。建立 .codepilot/agents/*.md[/]")
|
| 1203 |
+
console.print("[dim]範例: .codepilot/agents/reviewer.md[/]")
|
| 1204 |
+
continue
|
| 1205 |
+
agent_name = parts[1]
|
| 1206 |
+
agent_task = parts[2] if len(parts) > 2 else Prompt.ask("任務")
|
| 1207 |
+
if agent_name in custom_agents:
|
| 1208 |
+
result = run_custom_agent(custom_agents[agent_name], agent_task, model, tools, console)
|
| 1209 |
+
elif agent_name == "explore":
|
| 1210 |
+
# 內建 Explore agent(只讀)
|
| 1211 |
+
result = run_custom_agent(
|
| 1212 |
+
{"name": "explore", "prompt": "You are an exploration agent. Read and search files to investigate. NEVER modify or create files.",
|
| 1213 |
+
"tools": ["read_file", "search_files", "list_files", "git_status"]},
|
| 1214 |
+
agent_task, model, tools, console)
|
| 1215 |
+
elif agent_name == "plan":
|
| 1216 |
+
# 內建 Plan agent
|
| 1217 |
+
result = run_custom_agent(
|
| 1218 |
+
{"name": "plan", "prompt": "You are a planning agent. Analyze the task and create a detailed step-by-step plan. Do NOT execute any changes.",
|
| 1219 |
+
"tools": ["read_file", "search_files", "list_files", "git_status"]},
|
| 1220 |
+
agent_task, model, tools, console)
|
| 1221 |
+
else:
|
| 1222 |
+
console.print(f"[red]未知代理: {agent_name}[/]")
|
| 1223 |
+
console.print(f"[dim]可用: {', '.join(list(custom_agents.keys()) + ['explore', 'plan'])}[/]")
|
| 1224 |
+
continue
|
| 1225 |
+
|
| 1226 |
+
elif cmd.startswith("/bg"):
|
| 1227 |
+
# P1-2: 背景任務
|
| 1228 |
+
parts = cmd.split(None, 1)
|
| 1229 |
+
if len(parts) < 2 or parts[1] == "list":
|
| 1230 |
+
console.print(bg_tasks.list_tasks())
|
| 1231 |
+
elif parts[1].startswith("run "):
|
| 1232 |
+
bg_cmd = parts[1][4:]
|
| 1233 |
+
tid = bg_tasks.start(bg_cmd, tools.cwd)
|
| 1234 |
+
console.print(f" [green]🚀 背景任務 {tid}: {bg_cmd}[/]")
|
| 1235 |
+
elif parts[1].startswith("check "):
|
| 1236 |
+
tid = parts[1][6:].strip()
|
| 1237 |
+
info = bg_tasks.check(tid)
|
| 1238 |
+
console.print(f" 狀態: {info['status']} | 耗時: {info.get('elapsed',0)}s")
|
| 1239 |
+
if info.get("output"): console.print(Panel(info["output"][:500], title=f"bg:{tid}", border_style="dim"))
|
| 1240 |
+
elif parts[1].startswith("kill "):
|
| 1241 |
+
tid = parts[1][5:].strip()
|
| 1242 |
+
if bg_tasks.kill(tid): console.print(f" [red]⛔ 已終止 {tid}[/]")
|
| 1243 |
+
else: console.print(f" [dim]任務不存在或已結束[/]")
|
| 1244 |
+
else:
|
| 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()
|
| 1251 |
+
if len(parts) > 1 and parts[1] in APPROVAL_MODES:
|
| 1252 |
+
approval_mode = parts[1]
|
| 1253 |
+
console.print(f" [green]審批模式: {approval_mode} — {APPROVAL_MODES[approval_mode]}[/]")
|
| 1254 |
+
else:
|
| 1255 |
+
console.print(f" 目前: [bold]{approval_mode}[/] — {APPROVAL_MODES.get(approval_mode,'')}")
|
| 1256 |
+
for k, v in APPROVAL_MODES.items():
|
| 1257 |
+
marker = "→" if k == approval_mode else " "
|
| 1258 |
+
console.print(f" {marker} /approval {k}: {v}")
|
| 1259 |
+
continue
|
| 1260 |
+
|
| 1261 |
elif cmd == "/status":
|
| 1262 |
s = db.count()
|
| 1263 |
t = Table(title="📊 統計"); t.add_column("", style="cyan"); t.add_column("", style="green")
|
|
|
|
| 1361 |
results = []
|
| 1362 |
for call in tool_calls:
|
| 1363 |
console.print(f" [dim]🔧 {call['tool']}[/]")
|
| 1364 |
+
|
| 1365 |
+
# P1-1: 審批檢查
|
| 1366 |
+
if not check_approval(call["tool"], call["params"], approval_mode, console):
|
| 1367 |
+
results.append(f"[{call['tool']}] ⛔ 用戶拒絕執行")
|
| 1368 |
+
continue
|
| 1369 |
+
|
| 1370 |
result = execute_tool(tools, call) # 已含 P0-2 截斷
|
| 1371 |
tools_used_this_turn.append(call["tool"])
|
| 1372 |
|
|
|
|
| 1478 |
help="模型: local, openai, anthropic, openrouter, ollama, codex")
|
| 1479 |
p.add_argument("--api-key", type=str); p.add_argument("--cloud-model", type=str)
|
| 1480 |
p.add_argument("--duel", action="store_true", help="啟動時開啟 Duel 模式")
|
| 1481 |
+
p.add_argument("--approval", type=str, choices=["auto","auto-edit","ask"], default="auto",
|
| 1482 |
+
help="審批模式: auto=全自動, auto-edit=指令要確認, ask=全部確認")
|
| 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="刷幾題")
|