196 lines
7.2 KiB
Python
196 lines
7.2 KiB
Python
"""
|
||
快速创建新 Agent 脚本
|
||
从 luna 模板复制出一个新 agent 目录,自动替换所有唯一标识符,并:
|
||
- 生成 agents/<dir>/ 下的 agent.py / app.py / api_server.py / chat.py / .env / __init__.py
|
||
- 生成 mcp_dev_agent/<dir>_server.py 入口
|
||
- 更新 C:/Users/nzy/.codebuddy/.mcp.json 追加 MCP server 条目
|
||
|
||
命名约定(沿用 luna/qwen):
|
||
DIR = 用户输入(如 claude)
|
||
AGENT = {DIR}_agent (App name / agent name)
|
||
DB = sessions_{DIR}.db
|
||
MCP = kebab-case(如 claude-agent)
|
||
|
||
用法:
|
||
python create_agent.py claude --model opcode/claude-sonnet
|
||
python create_agent.py claude --model opcode/claude-sonnet --port 8005 --display "Claude Dev Agent"
|
||
"""
|
||
import argparse
|
||
import json
|
||
import os
|
||
import re
|
||
import shutil
|
||
import sys
|
||
|
||
# 强制 UTF-8(Windows 控制台默认 GBK,无法打印 ✓/→ 等字符)
|
||
if sys.platform == "win32":
|
||
os.environ.setdefault("PYTHONUTF8", "1")
|
||
try:
|
||
sys.stdout.reconfigure(encoding="utf-8")
|
||
sys.stderr.reconfigure(encoding="utf-8")
|
||
except Exception:
|
||
pass
|
||
|
||
# 仓库根目录
|
||
REPO_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||
AGENTS_DIR = os.path.join(REPO_ROOT, "agents")
|
||
MCP_DIR = os.path.join(REPO_ROOT, "mcp_dev_agent")
|
||
TEMPLATE_DIR = os.path.join(AGENTS_DIR, "luna")
|
||
MCP_CONFIG_PATH = os.path.join(os.path.expanduser("~"), ".codebuddy", ".mcp.json")
|
||
VENV_PYTHON = os.path.join(REPO_ROOT, ".venv", "Scripts", "python.exe")
|
||
|
||
# 模板中需要替换的标识符(按顺序执行,先长后短避免误伤)
|
||
REPLACEMENTS = [
|
||
("luna_agent", "{AGENT}"), # 覆盖所有 app/agent name 引用
|
||
("luna", "{DIR}"), # 剩余:模块路径、db 名、instruction 内称呼
|
||
("Luna", "{TITLE}"), # 首字母大写:Luna Agent / Luna: / === Luna
|
||
("8002", "{PORT}"), # 默认端口
|
||
("opcode/deepseek-v4-flash", "{MODEL}"), # .env 的 VLLM_MODEL
|
||
]
|
||
|
||
# 复制时排除的目录/文件
|
||
SKIP_DIR_NAMES = {"__pycache__", ".adk", ".git", ".idea"}
|
||
SKIP_FILENAMES = {".gitignore"}
|
||
|
||
|
||
def interact(arg_dir: str, model: str, port: int, display: str) -> tuple[str, str, int, str]:
|
||
"""补齐缺失参数(缺省时交互式询问)"""
|
||
d = arg_dir
|
||
if not d:
|
||
d = input("Agent 目录名(如 claude): ").strip()
|
||
if not model:
|
||
model = input("模型名(如 opcode/claude-sonnet): ").strip()
|
||
if port is None:
|
||
port = auto_next_port()
|
||
if not display:
|
||
display = f"{d.title()} Agent ({model}) 全栈开发助手"
|
||
return d, model, port, display
|
||
|
||
|
||
def auto_next_port() -> int:
|
||
"""扫描 agents/*/.env 的 API_SERVER_PORT,取最大值 +1"""
|
||
max_port = 8000
|
||
if os.path.isdir(AGENTS_DIR):
|
||
for entry in os.listdir(AGENTS_DIR):
|
||
env_path = os.path.join(AGENTS_DIR, entry, ".env")
|
||
if os.path.isfile(env_path):
|
||
m = re.search(r"API_SERVER_PORT\s*=\s*(\d+)", open(env_path, encoding="utf-8").read())
|
||
if m:
|
||
max_port = max(max_port, int(m.group(1)))
|
||
return max_port + 1
|
||
|
||
|
||
def copy_tree(src: str, dst: str) -> None:
|
||
"""递归复制,跳过 __pycache__/.adk/.git 等"""
|
||
os.makedirs(dst, exist_ok=True)
|
||
for name in os.listdir(src):
|
||
s = os.path.join(src, name)
|
||
d = os.path.join(dst, name)
|
||
if os.path.isdir(s):
|
||
if name in SKIP_DIR_NAMES:
|
||
continue
|
||
copy_tree(s, d)
|
||
else:
|
||
if name in SKIP_FILENAMES:
|
||
continue
|
||
shutil.copy2(s, d)
|
||
|
||
|
||
def apply_replacements(path: str, mapping: dict) -> None:
|
||
"""对文件内容按顺序做字符串替换"""
|
||
with open(path, encoding="utf-8") as f:
|
||
content = f.read()
|
||
for old, key in REPLACEMENTS:
|
||
content = content.replace(old, mapping.get(key, ""))
|
||
with open(path, "w", encoding="utf-8") as f:
|
||
f.write(content)
|
||
|
||
|
||
def update_mcp_config(mcp_key: str, server_file: str, display: str) -> None:
|
||
"""在 .mcp.json 的 mcpServers 中追加一条"""
|
||
if not os.path.isfile(MCP_CONFIG_PATH):
|
||
print(f"[warn] 未找到 {MCP_CONFIG_PATH},跳过 MCP 配置更新")
|
||
return
|
||
|
||
with open(MCP_CONFIG_PATH, encoding="utf-8") as f:
|
||
cfg = json.load(f)
|
||
|
||
if mcp_key in cfg.get("mcpServers", {}):
|
||
print(f"[warn] .mcp.json 已存在 {mcp_key} 条目,跳过")
|
||
return
|
||
|
||
cfg.setdefault("mcpServers", {})[mcp_key] = {
|
||
"type": "stdio",
|
||
"command": VENV_PYTHON,
|
||
"args": [server_file],
|
||
"description": display,
|
||
}
|
||
|
||
with open(MCP_CONFIG_PATH, "w", encoding="utf-8") as f:
|
||
json.dump(cfg, f, ensure_ascii=False, indent=2)
|
||
|
||
print(f" ✓ 已更新 {MCP_CONFIG_PATH}")
|
||
|
||
|
||
def main() -> None:
|
||
parser = argparse.ArgumentParser(description="创建一个新的 Dev Agent(从 luna 模板)")
|
||
parser.add_argument("dir", nargs="?", help="agent 目录名(如 claude)")
|
||
parser.add_argument("--model", default=None, help="模型名(如 opcode/claude-sonnet)")
|
||
parser.add_argument("--port", type=int, default=None, help="API 端口,默认自动取最大+1")
|
||
parser.add_argument("--display", default=None, help="MCP 描述,默认 '{Dir} Agent ({model}) 全栈开发助手'")
|
||
args = parser.parse_args()
|
||
|
||
d, model, port, display = interact(args.dir, args.model, args.port, args.display)
|
||
|
||
if not d or not model:
|
||
print("错误:目录名和模型名不能为空")
|
||
sys.exit(1)
|
||
|
||
agent = f"{d}_agent"
|
||
title = d.title()
|
||
db = f"sessions_{d}.db"
|
||
mcp_key = f"{d.replace('_', '-')}-agent"
|
||
target_dir = os.path.join(AGENTS_DIR, d)
|
||
server_file = os.path.join(MCP_DIR, f"{d}_server.py")
|
||
|
||
mapping = {"{AGENT}": agent, "{DIR}": d, "{TITLE}": title,
|
||
"{PORT}": str(port), "{MODEL}": model}
|
||
|
||
# 1. 校验目标目录不存在
|
||
if os.path.exists(target_dir):
|
||
print(f"错误:目录已存在 {target_dir}")
|
||
sys.exit(1)
|
||
if os.path.exists(server_file):
|
||
print(f"错误:MCP server 已存在 {server_file}")
|
||
sys.exit(1)
|
||
|
||
print(f"创建 Agent: {d} (agent={agent}, port={port}, model={model})")
|
||
print(f" 数据库: {db}")
|
||
|
||
# 2. 复制模板目录
|
||
copy_tree(TEMPLATE_DIR, target_dir)
|
||
# 3. 替换所有文件里的标识符
|
||
for root, _dirs, files in os.walk(target_dir):
|
||
for fn in files:
|
||
apply_replacements(os.path.join(root, fn), mapping)
|
||
# 4. 重写 __init__.py
|
||
with open(os.path.join(target_dir, "__init__.py"), "w", encoding="utf-8") as f:
|
||
f.write(f"# {d} package\nfrom . import agent\n")
|
||
print(f" ✓ 已生成 agents/{d}/")
|
||
|
||
# 5. 生成 MCP server 入口
|
||
shutil.copy2(os.path.join(MCP_DIR, "luna_server.py"), server_file)
|
||
apply_replacements(server_file, mapping)
|
||
print(f" ✓ 已生成 {server_file}")
|
||
|
||
# 6. 更新 .mcp.json
|
||
update_mcp_config(mcp_key, server_file, display)
|
||
|
||
print("\n完成!启动方式:")
|
||
print(f" cd agents/{d} && python api_server.py # 启动 API Server(自动注册到网关)")
|
||
print(f" cd agents/{d} && python chat.py # 命令行对话")
|
||
print(f" python agent_status.py --agent {agent} # 查看状态(需先手动加入 AGENTS 映射)")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main() |