qwen_agent/skills/bot-self-modifier/scripts/bot_modifier.py
2026-03-23 10:37:17 +08:00

437 lines
16 KiB
Python
Executable File
Raw 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
"""
Bot Self-Modifier Script
通过 API 读取和修改 bot 配置系统提示词、MCP服务器、技能、环境变量
"""
import argparse
import json
import os
import sys
import urllib.request
import urllib.error
import urllib.parse
def get_config():
"""获取配置下面的MASTERKEY和ASSISTANT_ID是从环境变量自动获取的不需要用户提供"""
masterkey = os.environ.get("MASTERKEY", "master")
bot_id = str(os.environ.get("ASSISTANT_ID", ""))
if not masterkey:
print("ERROR: MASTERKEY environment variable is required")
sys.exit(1)
return masterkey, "http://localhost:8001", bot_id
def api_request(method, url, headers=None, data=None, is_multipart=False):
"""发送 HTTP 请求"""
if headers is None:
headers = {}
if data is not None and not is_multipart:
data = json.dumps(data).encode("utf-8")
headers.setdefault("Content-Type", "application/json")
req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req) as resp:
body = resp.read().decode("utf-8")
return json.loads(body) if body else {}
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8")
try:
detail = json.loads(body)
except Exception:
detail = body
print(f"ERROR: HTTP {e.code} - {detail}")
sys.exit(1)
def build_multipart_data(fields, files):
"""构建 multipart/form-data 请求体"""
boundary = "----BotModifierBoundary"
lines = []
for key, value in fields.items():
lines.append(f"--{boundary}".encode())
lines.append(f'Content-Disposition: form-data; name="{key}"'.encode())
lines.append(b"")
lines.append(value.encode() if isinstance(value, str) else value)
for key, (filename, file_data, content_type) in files.items():
lines.append(f"--{boundary}".encode())
lines.append(
f'Content-Disposition: form-data; name="{key}"; filename="{filename}"'.encode()
)
lines.append(f"Content-Type: {content_type}".encode())
lines.append(b"")
lines.append(file_data)
lines.append(f"--{boundary}--".encode())
lines.append(b"")
body = b"\r\n".join(lines)
content_type = f"multipart/form-data; boundary={boundary}"
return body, content_type
# ============ 系统提示词 ============
def get_prompt(bot_id, masterkey, api_base):
"""读取系统提示词"""
url = f"{api_base}/api/v1/bots/{bot_id}/settings"
headers = {"Authorization": f"Bearer {masterkey}"}
result = api_request("GET", url, headers)
prompt = result.get("system_prompt", "")
print("=== System Prompt ===")
print(prompt if prompt else "(empty)")
return prompt
def set_prompt(bot_id, masterkey, api_base, value):
"""修改系统提示词"""
url = f"{api_base}/api/v1/bots/{bot_id}/settings"
headers = {"Authorization": f"Bearer {masterkey}"}
data = {"system_prompt": value}
result = api_request("PUT", url, headers, data)
print(f"OK: System prompt updated. {result.get('message', '')}")
# ============ Bot 基本信息 ============
def get_info(bot_id, masterkey, api_base):
"""读取 bot 基本信息(头像、描述、建议问题)"""
url = f"{api_base}/api/v1/bots/{bot_id}/settings"
headers = {"Authorization": f"Bearer {masterkey}"}
result = api_request("GET", url, headers)
print("=== Bot Info ===")
print(f" Name: {result.get('name', '(empty)')}")
print(f" Avatar URL: {result.get('avatar_url') or '(empty)'}")
print(f" Description: {result.get('description') or '(empty)'}")
suggestions = result.get("suggestions") or []
print(f" Suggestions: {json.dumps(suggestions, ensure_ascii=False) if suggestions else '(empty)'}")
return result
def set_avatar(bot_id, masterkey, api_base, value):
"""设置头像 URL"""
url = f"{api_base}/api/v1/bots/{bot_id}/settings"
headers = {"Authorization": f"Bearer {masterkey}"}
data = {"avatar_url": value}
result = api_request("PUT", url, headers, data)
print(f"OK: Avatar updated. {result.get('message', '')}")
def set_name(bot_id, masterkey, api_base, value):
"""设置 bot 标题名称"""
url = f"{api_base}/api/v1/bots/{bot_id}"
headers = {"Authorization": f"Bearer {masterkey}"}
data = {"name": value}
result = api_request("PUT", url, headers, data)
print(f"OK: Name updated to '{result.get('name', value)}'.")
def set_description(bot_id, masterkey, api_base, value):
"""设置描述"""
url = f"{api_base}/api/v1/bots/{bot_id}/settings"
headers = {"Authorization": f"Bearer {masterkey}"}
data = {"description": value}
result = api_request("PUT", url, headers, data)
print(f"OK: Description updated. {result.get('message', '')}")
def set_suggestions(bot_id, masterkey, api_base, value):
"""设置建议问题JSON 数组)"""
try:
suggestions = json.loads(value)
except json.JSONDecodeError:
print("ERROR: --value must be a valid JSON array for set_suggestions")
sys.exit(1)
if not isinstance(suggestions, list):
print("ERROR: --value must be a JSON array, e.g. '[\"question1\", \"question2\"]'")
sys.exit(1)
url = f"{api_base}/api/v1/bots/{bot_id}/settings"
headers = {"Authorization": f"Bearer {masterkey}"}
data = {"suggestions": suggestions}
result = api_request("PUT", url, headers, data)
print(f"OK: Suggestions updated. {result.get('message', '')}")
# ============ MCP 服务器 ============
def list_mcp(bot_id, masterkey, api_base):
"""列出 MCP 服务器"""
url = f"{api_base}/api/v1/bots/{bot_id}/mcp"
headers = {"Authorization": f"Bearer {masterkey}"}
result = api_request("GET", url, headers)
if not result:
print("No MCP servers configured.")
return
print("=== MCP Servers ===")
for server in result:
enabled_str = "enabled" if server.get("enabled") else "disabled"
print(f" [{enabled_str}] {server['name']} (id: {server['id']}, type: {server.get('type', 'N/A')})")
config = server.get("config", {})
if config:
print(f" config: {json.dumps(config, ensure_ascii=False)}")
return result
def add_mcp(bot_id, masterkey, api_base, name, mcp_type, config_str):
"""添加 MCP 服务器"""
try:
config = json.loads(config_str)
except json.JSONDecodeError:
print("ERROR: --config must be valid JSON")
sys.exit(1)
url = f"{api_base}/api/v1/bots/{bot_id}/mcp"
headers = {"Authorization": f"Bearer {masterkey}"}
data = {
"name": name,
"type": mcp_type,
"config": config,
"enabled": True,
}
result = api_request("POST", url, headers, data)
print(f"OK: MCP server '{name}' added (id: {result.get('id', 'N/A')})")
def delete_mcp(bot_id, masterkey, api_base, mcp_id):
"""删除 MCP 服务器"""
url = f"{api_base}/api/v1/bots/{bot_id}/mcp/{mcp_id}"
headers = {"Authorization": f"Bearer {masterkey}"}
result = api_request("DELETE", url, headers)
print(f"OK: MCP server deleted. {result.get('message', '')}")
# ============ 技能操作 ============
def list_skills(bot_id, masterkey, api_base):
"""列出所有技能"""
# 获取技能文件列表
url = f"{api_base}/api/v1/skill/list?bot_id={urllib.parse.quote(bot_id)}"
headers = {"Authorization": f"Bearer {masterkey}"}
result = api_request("GET", url, headers)
# 获取当前启用的技能
settings_url = f"{api_base}/api/v1/bots/{bot_id}/settings"
settings = api_request("GET", settings_url, headers)
enabled_skills_str = settings.get("skills", "") or ""
enabled_skills = [s.strip() for s in enabled_skills_str.split(",") if s.strip()]
skills = result.get("skills", [])
print(f"=== Skills (total: {result.get('total', len(skills))}) ===")
print(f"Enabled: {', '.join(enabled_skills) if enabled_skills else '(none)'}")
print()
for skill in skills:
is_user = "[user]" if skill.get("user_skill") else "[official]"
is_enabled = "*" if skill["name"] in enabled_skills else " "
print(f" {is_enabled} {is_user} {skill['name']}: {skill.get('description', '')}")
return skills
def upload_skill(bot_id, masterkey, api_base, file_path):
"""上传技能 zip"""
if not os.path.exists(file_path):
print(f"ERROR: File not found: {file_path}")
sys.exit(1)
filename = os.path.basename(file_path)
with open(file_path, "rb") as f:
file_data = f.read()
body, content_type = build_multipart_data(
fields={"bot_id": bot_id},
files={"file": (filename, file_data, "application/zip")},
)
url = f"{api_base}/api/v1/skill/upload"
headers = {
"Authorization": f"Bearer {masterkey}",
"Content-Type": content_type,
}
result = api_request("POST", url, headers, body, is_multipart=True)
print(f"OK: Skill uploaded. {result.get('message', '')} (name: {result.get('skill_name', 'N/A')})")
def enable_skill(bot_id, masterkey, api_base, skill_names):
"""启用技能 - 将技能名添加到 settings.skills"""
# 先获取当前启用的技能
settings_url = f"{api_base}/api/v1/bots/{bot_id}/settings"
headers = {"Authorization": f"Bearer {masterkey}"}
settings = api_request("GET", settings_url, headers)
current = settings.get("skills", "") or ""
current_list = [s.strip() for s in current.split(",") if s.strip()]
new_skills = [s.strip() for s in skill_names.split(",") if s.strip()]
for s in new_skills:
if s not in current_list:
current_list.append(s)
updated = ",".join(current_list)
data = {"skills": updated}
result = api_request("PUT", settings_url, headers, data)
print(f"OK: Skills enabled. Current: {updated}")
def disable_skill(bot_id, masterkey, api_base, skill_name):
"""禁用技能 - 从 settings.skills 中移除"""
settings_url = f"{api_base}/api/v1/bots/{bot_id}/settings"
headers = {"Authorization": f"Bearer {masterkey}"}
settings = api_request("GET", settings_url, headers)
current = settings.get("skills", "") or ""
current_list = [s.strip() for s in current.split(",") if s.strip()]
to_remove = [s.strip() for s in skill_name.split(",") if s.strip()]
current_list = [s for s in current_list if s not in to_remove]
updated = ",".join(current_list)
data = {"skills": updated}
result = api_request("PUT", settings_url, headers, data)
print(f"OK: Skills disabled. Current: {updated if updated else '(none)'}")
def delete_skill(bot_id, masterkey, api_base, skill_name):
"""删除已上传的技能"""
url = (
f"{api_base}/api/v1/skill/remove"
f"?bot_id={urllib.parse.quote(bot_id)}"
f"&skill_name={urllib.parse.quote(skill_name)}"
)
headers = {"Authorization": f"Bearer {masterkey}"}
result = api_request("DELETE", url, headers)
print(f"OK: Skill '{skill_name}' deleted. {result.get('message', '')}")
# 同时从启用列表中移除
disable_skill(bot_id, masterkey, api_base, skill_name)
# ============ 环境变量 ============
def get_env(bot_id, masterkey, api_base):
"""读取环境变量"""
url = f"{api_base}/api/v1/bots/{bot_id}/settings"
headers = {"Authorization": f"Bearer {masterkey}"}
result = api_request("GET", url, headers)
shell_env = result.get("shell_env", {})
print("=== Environment Variables ===")
if not shell_env:
print("(none)")
else:
for key, value in shell_env.items():
display_val = value if value else "(empty)"
print(f" {key}={display_val}")
return shell_env
def set_env(bot_id, masterkey, api_base, env_json):
"""修改环境变量(合并更新,不会覆盖未指定的变量)"""
# 先获取当前环境变量
settings_url = f"{api_base}/api/v1/bots/{bot_id}/settings"
headers = {"Authorization": f"Bearer {masterkey}"}
settings = api_request("GET", settings_url, headers)
current_env = settings.get("shell_env", {}) or {}
try:
new_env = json.loads(env_json)
except json.JSONDecodeError:
print("ERROR: --value must be valid JSON for set_env")
sys.exit(1)
# 合并
current_env.update(new_env)
data = {"shell_env": current_env}
result = api_request("PUT", settings_url, headers, data)
print(f"OK: Environment variables updated. {result.get('message', '')}")
for key, value in current_env.items():
print(f" {key}={value if value else '(empty)'}")
# ============ Main ============
def main():
parser = argparse.ArgumentParser(description="Bot Self-Modifier: read and modify bot configuration via API")
parser.add_argument(
"--action",
required=True,
choices=[
"get_prompt", "set_prompt",
"get_info", "set_name", "set_avatar", "set_description", "set_suggestions",
"list_mcp", "add_mcp", "delete_mcp",
"list_skills", "upload_skill", "enable_skill", "disable_skill", "delete_skill",
"get_env", "set_env",
],
help="Action to perform",
)
parser.add_argument("--value", help="Value for the action (prompt text, skill name, env JSON, etc.)")
parser.add_argument("--name", help="MCP server name (for add_mcp)")
parser.add_argument("--mcp-type", default="sse", help="MCP server type (for add_mcp, default: sse)")
parser.add_argument("--config", help="MCP server config JSON (for add_mcp)")
parser.add_argument("--mcp-id", help="MCP server ID (for delete_mcp)")
parser.add_argument("--file", help="Skill zip file path (for upload_skill)")
args = parser.parse_args()
masterkey, api_base, bot_id = get_config()
action_map = {
"get_prompt": lambda: get_prompt(bot_id, masterkey, api_base),
"set_prompt": lambda: set_prompt(bot_id, masterkey, api_base, args.value),
"get_info": lambda: get_info(bot_id, masterkey, api_base),
"set_name": lambda: set_name(bot_id, masterkey, api_base, args.value),
"set_avatar": lambda: set_avatar(bot_id, masterkey, api_base, args.value),
"set_description": lambda: set_description(bot_id, masterkey, api_base, args.value),
"set_suggestions": lambda: set_suggestions(bot_id, masterkey, api_base, args.value),
"list_mcp": lambda: list_mcp(bot_id, masterkey, api_base),
"add_mcp": lambda: add_mcp(bot_id, masterkey, api_base, args.name, args.mcp_type, args.config),
"delete_mcp": lambda: delete_mcp(bot_id, masterkey, api_base, args.mcp_id),
"list_skills": lambda: list_skills(bot_id, masterkey, api_base),
"upload_skill": lambda: upload_skill(bot_id, masterkey, api_base, args.file),
"enable_skill": lambda: enable_skill(bot_id, masterkey, api_base, args.value),
"disable_skill": lambda: disable_skill(bot_id, masterkey, api_base, args.value),
"delete_skill": lambda: delete_skill(bot_id, masterkey, api_base, args.value),
"get_env": lambda: get_env(bot_id, masterkey, api_base),
"set_env": lambda: set_env(bot_id, masterkey, api_base, args.value),
}
# 参数验证
if args.action == "set_prompt" and not args.value:
print("ERROR: --value is required for set_prompt")
sys.exit(1)
if args.action in ("set_avatar", "set_name", "set_description", "set_suggestions") and not args.value:
print(f"ERROR: --value is required for {args.action}")
sys.exit(1)
if args.action == "add_mcp" and (not args.name or not args.config):
print("ERROR: --name and --config are required for add_mcp")
sys.exit(1)
if args.action == "delete_mcp" and not args.mcp_id:
print("ERROR: --mcp-id is required for delete_mcp")
sys.exit(1)
if args.action == "upload_skill" and not args.file:
print("ERROR: --file is required for upload_skill")
sys.exit(1)
if args.action in ("enable_skill", "disable_skill", "delete_skill") and not args.value:
print(f"ERROR: --value is required for {args.action}")
sys.exit(1)
if args.action == "set_env" and not args.value:
print("ERROR: --value is required for set_env (JSON format)")
sys.exit(1)
action_map[args.action]()
if __name__ == "__main__":
main()