diff --git a/skills/linggan/static-site-deploy/SKILL.md b/skills/developing/static-site-deploy/SKILL.md similarity index 100% rename from skills/linggan/static-site-deploy/SKILL.md rename to skills/developing/static-site-deploy/SKILL.md diff --git a/skills/linggan/static-site-deploy/scripts/deploy.sh b/skills/developing/static-site-deploy/scripts/deploy.sh similarity index 100% rename from skills/linggan/static-site-deploy/scripts/deploy.sh rename to skills/developing/static-site-deploy/scripts/deploy.sh diff --git a/skills/linggan/static-site-deploy/scripts/download.sh b/skills/developing/static-site-deploy/scripts/download.sh similarity index 100% rename from skills/linggan/static-site-deploy/scripts/download.sh rename to skills/developing/static-site-deploy/scripts/download.sh diff --git a/skills/linggan/static-site-deploy/scripts/list.sh b/skills/developing/static-site-deploy/scripts/list.sh similarity index 100% rename from skills/linggan/static-site-deploy/scripts/list.sh rename to skills/developing/static-site-deploy/scripts/list.sh diff --git a/skills/linggan/static-site-deploy/scripts/read.sh b/skills/developing/static-site-deploy/scripts/read.sh similarity index 100% rename from skills/linggan/static-site-deploy/scripts/read.sh rename to skills/developing/static-site-deploy/scripts/read.sh diff --git a/skills/linggan/static-site-deploy/scripts/static-site-deploy.yml b/skills/developing/static-site-deploy/scripts/static-site-deploy.yml similarity index 100% rename from skills/linggan/static-site-deploy/scripts/static-site-deploy.yml rename to skills/developing/static-site-deploy/scripts/static-site-deploy.yml diff --git a/skills/linggan/bot-self-modifier/SKILL.md b/skills/linggan/bot-self-modifier/SKILL.md index 6435d8b..9ed38cc 100644 --- a/skills/linggan/bot-self-modifier/SKILL.md +++ b/skills/linggan/bot-self-modifier/SKILL.md @@ -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` | 设置建议问题(需要 --value,JSON 数组) | | `list_mcp` | 列出 MCP 服务器 | diff --git a/skills/linggan/bot-self-modifier/scripts/bot_modifier.py b/skills/linggan/bot-self-modifier/scripts/bot_modifier.py index b956536..f247c57 100755 --- a/skills/linggan/bot-self-modifier/scripts/bot_modifier.py +++ b/skills/linggan/bot-self-modifier/scripts/bot_modifier.py @@ -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) diff --git a/skills/linggan/bot-self-modifier/skill.yaml b/skills/linggan/bot-self-modifier/skill.yaml index aff1adc..19300f4 100644 --- a/skills/linggan/bot-self-modifier/skill.yaml +++ b/skills/linggan/bot-self-modifier/skill.yaml @@ -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 diff --git a/upload.php b/upload.php new file mode 100644 index 0000000..df7e9c2 --- /dev/null +++ b/upload.php @@ -0,0 +1,91 @@ + 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);