auto-sync: tencent-cos-upload 2026-04-13_15:45

This commit is contained in:
OpenClaw Skill Sync Bot 2026-04-13 15:45:12 +08:00
commit 252c980795
3 changed files with 247 additions and 0 deletions

94
SKILL.md Normal file
View File

@ -0,0 +1,94 @@
---
name: tencent-cos-upload
description: 上传文件到腾讯云COS并生成可直接访问的URL。当需要将本地文件图片、视频、音频、文档等上传到腾讯云对象存储并获取公开可访问链接时使用。触发场景上传文件到COS、生成文件外链、存储媒体资源、备份文件到云端。
---
# 腾讯云COS文件上传
将本地文件上传到腾讯云COS返回可直接访问的URL。
## 前置依赖
```bash
pip3 install cos-python-sdk-v5 --break-system-packages
```
## 使用方式
### 方式一:调用脚本(推荐)
```bash
python3 scripts/cos_upload.py <local_file> <cos_key> [--content-type <mime_type>]
```
- `local_file`:本地文件路径
- `cos_key`COS上的存储路径`vala_llm/data/image.png`
- `--content-type`可选MIME类型`image/png`),不指定则自动检测
输出一行URL`https://<domain>/<cos_key>`
示例:
```bash
python3 scripts/cos_upload.py /tmp/photo.png vala_llm/user_feedback/image/2026-04-10/abc123.png
# 输出: https://static.valavala.com/vala_llm/user_feedback/image/2026-04-10/abc123.png
```
### 方式二Python代码引用
```python
import sys
sys.path.insert(0, '/root/.openclaw/skills/tencent-cos-upload/scripts')
from cos_upload import CosUploader
uploader = CosUploader()
url = uploader.upload('/tmp/photo.png', 'vala_llm/images/photo.png')
print(url) # https://static.valavala.com/vala_llm/images/photo.png
```
批量上传:
```python
results = uploader.upload_batch([
('/tmp/a.png', 'path/a.png'),
('/tmp/b.mp4', 'path/b.mp4'),
])
# results = [('path/a.png', 'https://...'), ('path/b.png', 'https://...')]
```
## 配置
凭证从工作区 `secrets.md` 的腾讯云COS部分读取脚本自动加载**无硬编码密钥**。
如需修改配置(换桶、换域名),修改 `secrets.md` 中的腾讯云COS配置项即可无需修改脚本代码。
## COS路径规范
建议按 `{业务}/{类型}/{日期}/` 组织:
```
vala_llm/
├── user_feedback/
│ ├── image/2026-04-10/
│ ├── video/2026-04-10/
│ ├── audio/2026-04-10/
│ └── file/2026-04-10/
└── asr_audio_backup/
├── online/20260410/
└── test/20260410/
```
## 文件名规范
- 避免中文使用纯ASCII字符字母、数字、短横线、下划线
- 推荐格式:`{唯一ID}{扩展名}`,如 `abc123def456.png`
## 常见MIME类型
| 扩展名 | Content-Type |
|--------|-------------|
| .png | image/png |
| .jpg/.jpeg | image/jpeg |
| .mp4 | video/mp4 |
| .mov | video/quicktime |
| .mp3 | audio/mpeg |
| .ogg | audio/ogg |
| .wav | audio/wav |
| .pdf | application/pdf |

Binary file not shown.

153
scripts/cos_upload.py Normal file
View File

@ -0,0 +1,153 @@
#!/usr/bin/env python3
"""
腾讯云COS文件上传工具
用法:
python3 cos_upload.py <local_file> <cos_key> [--content-type <mime>]
"""
import os
import sys
import re
import mimetypes
import argparse
from qcloud_cos import CosConfig, CosS3Client
# ============ 配置(从 secrets.md 读取,不再硬编码) ============
SECRETS_PATH = "/root/.openclaw/workspace-xiaokui/secrets.md"
def _load_cos_config():
"""从 secrets.md 加载COS配置"""
if not os.path.exists(SECRETS_PATH):
raise RuntimeError(f"密钥文件不存在: {SECRETS_PATH}")
with open(SECRETS_PATH, 'r', encoding='utf-8') as f:
content = f.read()
# 提取腾讯云COS配置
patterns = {
'secret_id': r'TENCENT_SECRET_ID.*?`([^`]+)`',
'secret_key': r'TENCENT_SECRET_KEY.*?`([^`]+)`',
'region': r'TENCENT_COS_REGION.*?`([^`]+)`',
'bucket': r'TENCENT_COS_BUCKET.*?`([^`]+)`',
'domain': r'TENCENT_COS_DOWNLOAD_PATH.*?`([^`]+)`'
}
config = {}
for key, pattern in patterns.items():
match = re.search(pattern, content, re.MULTILINE)
if not match:
raise RuntimeError(f"secrets.md 中未找到 COS 配置项: {key}")
config[key] = match.group(1)
return config
_cos_config = _load_cos_config()
COS_SECRET_ID = _cos_config['secret_id']
COS_SECRET_KEY = _cos_config['secret_key']
COS_REGION = _cos_config['region']
COS_BUCKET = _cos_config['bucket']
COS_DOWNLOAD_DOMAIN = _cos_config['domain']
class CosUploader:
"""腾讯云COS上传器"""
def __init__(self, secret_id=None, secret_key=None, region=None, bucket=None, domain=None):
self.bucket = bucket or COS_BUCKET
self.domain = domain or COS_DOWNLOAD_DOMAIN
config = CosConfig(
Region=region or COS_REGION,
SecretId=secret_id or COS_SECRET_ID,
SecretKey=secret_key or COS_SECRET_KEY,
Scheme='https'
)
self.client = CosS3Client(config)
def upload(self, local_path: str, cos_key: str, content_type: str = None) -> str:
"""
上传文件到COS
Args:
local_path: 本地文件路径
cos_key: COS存储路径
content_type: MIME类型不指定则自动检测
Returns:
可访问的URL
"""
if not os.path.exists(local_path):
raise FileNotFoundError(f"文件不存在: {local_path}")
if not content_type:
content_type = self._detect_content_type(local_path)
kwargs = {
'Bucket': self.bucket,
'Key': cos_key,
'LocalFilePath': local_path,
}
if content_type:
kwargs['ContentType'] = content_type
self.client.upload_file(**kwargs)
return f"https://{self.domain}/{cos_key}"
def upload_bytes(self, data: bytes, cos_key: str, content_type: str = None) -> str:
"""上传字节数据到COS"""
kwargs = {
'Bucket': self.bucket,
'Key': cos_key,
'Body': data,
}
if content_type:
kwargs['ContentType'] = content_type
self.client.put_object(**kwargs)
return f"https://{self.domain}/{cos_key}"
def upload_batch(self, items: list) -> list:
"""
批量上传
Args:
items: [(local_path, cos_key), ...] [(local_path, cos_key, content_type), ...]
Returns:
[(cos_key, url), ...]
"""
results = []
for item in items:
local_path = item[0]
cos_key = item[1]
content_type = item[2] if len(item) > 2 else None
try:
url = self.upload(local_path, cos_key, content_type)
results.append((cos_key, url))
except Exception as e:
print(f"[ERROR] 上传失败 {cos_key}: {e}", file=sys.stderr)
results.append((cos_key, None))
return results
def delete(self, cos_key: str):
"""删除COS上的文件"""
self.client.delete_object(Bucket=self.bucket, Key=cos_key)
def list_objects(self, prefix: str, max_keys: int = 100) -> list:
"""列出COS上的文件"""
resp = self.client.list_objects(Bucket=self.bucket, Prefix=prefix, MaxKeys=max_keys)
return [item['Key'] for item in resp.get('Contents', []) if not item['Key'].endswith('/')]
@staticmethod
def _detect_content_type(filepath: str) -> str:
mime, _ = mimetypes.guess_type(filepath)
return mime or 'application/octet-stream'
def main():
parser = argparse.ArgumentParser(description='上传文件到腾讯云COS')
parser.add_argument('local_file', help='本地文件路径')
parser.add_argument('cos_key', help='COS存储路径')
parser.add_argument('--content-type', help='MIME类型自动检测', default=None)
args = parser.parse_args()
uploader = CosUploader()
url = uploader.upload(args.local_file, args.cos_key, args.content_type)
print(url)
if __name__ == '__main__':
main()