set_avatar_file

This commit is contained in:
朱潮 2026-06-14 16:46:27 +08:00
parent e3c6408802
commit 0e90b550a4
10 changed files with 173 additions and 11 deletions

View File

@ -27,7 +27,7 @@ bot-self-modifier/
支持以下功能:
1. **系统提示词** - 读取和修改
2. **Bot 基本信息** - 头像、描述、建议问题的读取和修改
2. **Bot 基本信息** - 头像(通过本地图片文件上传图床)、描述、建议问题的读取和修改
3. **MCP 服务器** - 列表查看、添加和删除
4. **技能列表** - 读取、上传、启用、禁用、删除
5. **环境变量** - 读取和修改
@ -55,8 +55,8 @@ scripts/bot_modifier.py --action get_info
# 修改 bot 标题名称
scripts/bot_modifier.py --action set_name --value "智能客服助手"
# 设置头像(URL
scripts/bot_modifier.py --action set_avatar --value "https://example.com/avatar.png"
# 设置头像(本地图片文件)—— 上传到图床获取 URL 后自动设置
scripts/bot_modifier.py --action set_avatar_file --file /path/to/avatar.png
# 设置描述
scripts/bot_modifier.py --action set_description --value "这是一个智能客服助手"
@ -127,7 +127,7 @@ scripts/bot_modifier.py [OPTIONS]
| `--mcp-type` | For add_mcp | MCP 服务器类型 (sse/streamable-http) | sse |
| `--config` | For add_mcp | MCP 服务器配置 JSON | - |
| `--mcp-id` | For delete_mcp | MCP 服务器 ID | - |
| `--file` | For upload_skill | Skill zip 文件路径 | - |
| `--file` | For upload_skill / set_avatar_file | 文件路径(技能 zip / 头像图片) | - |
**Available Actions:**
@ -137,7 +137,7 @@ scripts/bot_modifier.py [OPTIONS]
| `set_prompt` | 修改系统提示词(需要 --value |
| `get_info` | 读取 bot 基本信息(名称、头像、描述、建议问题) |
| `set_name` | 修改 bot 标题名称(需要 --value |
| `set_avatar` | 设置头像 URL需要 --value |
| `set_avatar_file` | 上传本地图片作为头像(需要 --file自动上传图床获取 URL 后设置 |
| `set_description` | 设置描述(需要 --value |
| `set_suggestions` | 设置建议问题(需要 --valueJSON 数组) |
| `list_mcp` | 列出 MCP 服务器 |

View File

@ -5,6 +5,9 @@ Bot Self-Modifier Script
"""
import argparse
import ftplib
import hashlib
import io
import json
import os
import sys
@ -13,6 +16,27 @@ import urllib.error
import urllib.parse
# ============ Avatar FTP upload config ============
# Avatar files are uploaded to an FTP server and served via a public base URL.
# Defaults can be overridden by environment variables.
AVATAR_FTP_HOST = os.environ.get("AVATAR_FTP_HOST", "175.27.142.79")
AVATAR_FTP_USER = os.environ.get("AVATAR_FTP_USER", "zhuchaowe")
AVATAR_FTP_PASSWORD = os.environ.get("AVATAR_FTP_PASSWORD", "zhu305750624")
AVATAR_BASE_URL = os.environ.get("AVATAR_BASE_URL", "https://engine.aitravelmaster.com/avatar")
def upload_avatar_to_ftp(file_data, remote_name):
"""Upload avatar bytes to the FTP server and return the public access URL."""
try:
with ftplib.FTP(AVATAR_FTP_HOST, timeout=30) as ftp:
ftp.login(AVATAR_FTP_USER, AVATAR_FTP_PASSWORD)
ftp.storbinary(f"STOR {remote_name}", io.BytesIO(file_data))
except ftplib.all_errors as e:
print(f"ERROR: FTP upload failed - {e}")
sys.exit(1)
return f"{AVATAR_BASE_URL.rstrip('/')}/{remote_name}"
def get_config():
"""获取配置下面的MASTERKEY和ASSISTANT_ID是从环境变量自动获取的不需要用户提供"""
masterkey = os.environ.get("MASTERKEY", "master")
@ -116,8 +140,8 @@ def get_info(bot_id, masterkey, api_base):
return result
def set_avatar(bot_id, masterkey, api_base, value):
"""设置头像 URL"""
def _set_avatar_url(bot_id, masterkey, api_base, value):
"""Apply an avatar URL to the bot. Internal helper, only called by set_avatar_file."""
url = f"{api_base}/api/v1/bots/{bot_id}/settings"
headers = {"Authorization": f"Bearer {masterkey}"}
data = {"avatar_url": value}
@ -125,6 +149,30 @@ def set_avatar(bot_id, masterkey, api_base, value):
print(f"OK: Avatar updated. {result.get('message', '')}")
def set_avatar_file(bot_id, masterkey, api_base, file_path):
"""Upload a local image file as the avatar.
The file is uploaded to the FTP server, then the resulting public URL is
saved as the bot's avatar. The remote filename is derived from the content
hash so identical images are reused and different images never collide.
"""
if not os.path.exists(file_path):
print(f"ERROR: File not found: {file_path}")
sys.exit(1)
with open(file_path, "rb") as f:
file_data = f.read()
ext = os.path.splitext(file_path)[1].lower() or ".png"
content_hash = hashlib.md5(file_data).hexdigest()[:12]
remote_name = f"avatar_{content_hash}{ext}"
public_url = upload_avatar_to_ftp(file_data, remote_name)
print(f"OK: Avatar file uploaded -> {public_url}")
_set_avatar_url(bot_id, masterkey, api_base, public_url)
def set_name(bot_id, masterkey, api_base, value):
"""设置 bot 标题名称"""
url = f"{api_base}/api/v1/bots/{bot_id}"
@ -369,7 +417,7 @@ def main():
required=True,
choices=[
"get_prompt", "set_prompt",
"get_info", "set_name", "set_avatar", "set_description", "set_suggestions",
"get_info", "set_name", "set_avatar_file", "set_description", "set_suggestions",
"list_mcp", "add_mcp", "delete_mcp",
"list_skills", "upload_skill", "enable_skill", "disable_skill", "delete_skill",
"get_env", "set_env",
@ -381,7 +429,7 @@ def main():
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)")
parser.add_argument("--file", help="File path (skill zip for upload_skill, image for set_avatar_file)")
args = parser.parse_args()
masterkey, api_base, bot_id = get_config()
@ -391,7 +439,7 @@ def main():
"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_avatar_file": lambda: set_avatar_file(bot_id, masterkey, api_base, args.file),
"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),
@ -410,7 +458,7 @@ def main():
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:
if args.action in ("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):
@ -422,6 +470,9 @@ def main():
if args.action == "upload_skill" and not args.file:
print("ERROR: --file is required for upload_skill")
sys.exit(1)
if args.action == "set_avatar_file" and not args.file:
print("ERROR: --file is required for set_avatar_file")
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)

View File

@ -23,6 +23,26 @@ env:
required: false
default: http://localhost:8001
description: API server base URL
AVATAR_FTP_HOST:
type: string
required: false
default: 175.27.142.79
description: FTP host for avatar image uploads (set_avatar_file)
AVATAR_FTP_USER:
type: string
required: false
default: zhuchaowe
description: FTP username for avatar uploads
AVATAR_FTP_PASSWORD:
type: string
required: false
default: zhu305750624
description: FTP password for avatar uploads
AVATAR_BASE_URL:
type: string
required: false
default: https://engine.aitravelmaster.com/avatar
description: Public base URL where uploaded avatars are served
config:
bot_id:
type: string

91
upload.php Normal file
View File

@ -0,0 +1,91 @@
<?php
// Simple file upload API. Saves uploaded files into the avatar directory.
header('Content-Type: application/json; charset=utf-8');
// Only allow POST requests
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['code' => 405, 'message' => 'Method Not Allowed, use POST']);
exit;
}
// Form field name for the uploaded file
$field = 'file';
// Validate that a file was actually uploaded
if (!isset($_FILES[$field]) || $_FILES[$field]['error'] === UPLOAD_ERR_NO_FILE) {
http_response_code(400);
echo json_encode(['code' => 400, 'message' => 'No file uploaded, field name should be "file"']);
exit;
}
$file = $_FILES[$field];
// Check upload errors reported by PHP
if ($file['error'] !== UPLOAD_ERR_OK) {
http_response_code(400);
echo json_encode(['code' => 400, 'message' => 'Upload failed, error code: ' . $file['error']]);
exit;
}
// Limit file size to 5MB
$maxSize = 5 * 1024 * 1024;
if ($file['size'] > $maxSize) {
http_response_code(400);
echo json_encode(['code' => 400, 'message' => 'File too large, max 5MB']);
exit;
}
// Allow only common image types (detected by actual content, not the client-provided type)
$allowed = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'image/webp' => 'webp',
];
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
if (!isset($allowed[$mime])) {
http_response_code(400);
echo json_encode(['code' => 400, 'message' => 'Unsupported file type: ' . $mime]);
exit;
}
// Ensure the avatar directory exists
$uploadDir = __DIR__ . '/avatar';
if (!is_dir($uploadDir)) {
if (!mkdir($uploadDir, 0755, true) && !is_dir($uploadDir)) {
http_response_code(500);
echo json_encode(['code' => 500, 'message' => 'Failed to create avatar directory']);
exit;
}
}
// Generate a safe, unique filename to avoid collisions and path traversal
$ext = $allowed[$mime];
$filename = bin2hex(random_bytes(16)) . '.' . $ext;
$target = $uploadDir . '/' . $filename;
// Move the uploaded file into place
if (!move_uploaded_file($file['tmp_name'], $target)) {
http_response_code(500);
echo json_encode(['code' => 500, 'message' => 'Failed to save file']);
exit;
}
// Build a public URL relative to the current script location
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
$basePath = rtrim(dirname($_SERVER['SCRIPT_NAME'] ?? ''), '/\\');
$url = $scheme . '://' . $host . $basePath . '/avatar/' . $filename;
http_response_code(200);
echo json_encode([
'code' => 0,
'message' => 'success',
'filename' => $filename,
'url' => $url,
], JSON_UNESCAPED_SLASHES);