From 252c98079533cd4f9ce297782652f330843b5991 Mon Sep 17 00:00:00 2001 From: OpenClaw Skill Sync Bot Date: Mon, 13 Apr 2026 15:45:12 +0800 Subject: [PATCH] auto-sync: tencent-cos-upload 2026-04-13_15:45 --- SKILL.md | 94 +++++++++++ .../__pycache__/cos_upload.cpython-312.pyc | Bin 0 -> 7511 bytes scripts/cos_upload.py | 153 ++++++++++++++++++ 3 files changed, 247 insertions(+) create mode 100644 SKILL.md create mode 100644 scripts/__pycache__/cos_upload.cpython-312.pyc create mode 100644 scripts/cos_upload.py diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..e765165 --- /dev/null +++ b/SKILL.md @@ -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 [--content-type ] +``` + +- `local_file`:本地文件路径 +- `cos_key`:COS上的存储路径(如 `vala_llm/data/image.png`) +- `--content-type`:可选,MIME类型(如 `image/png`),不指定则自动检测 + +输出一行URL:`https:///` + +示例: +```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 | diff --git a/scripts/__pycache__/cos_upload.cpython-312.pyc b/scripts/__pycache__/cos_upload.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0eff2795f4ee689a35f269d9ddb8a5793b8a8c65 GIT binary patch literal 7511 zcmb_hdvFv-dhgkp*`0l_Rzebp$I9lA77)wp%%S42K@#SRC zNM8ALE;v?LoXk0Q5;n;xIb0V@rX4T!({dG@If8Y1}9{iKtZXr-MzI8vsmJssq_>e7JrZf8ybOwn?0z_m) zLzD?Hv^4|_v}FST z-@%YBLzltpjK_f5^6=b84{we>_}!(tgH2PvIRD`NzkYD*!h;WnC*ON}^4xotFaBg? z>Mws@Yw>tGy3|v#_D!B}ObNz2qOp*;uA|GdEgB1lqQT}!RNC&@hI6N-uI-*>Bf#hU{9EKt4OyC38P%Gl$y101k-foP$MZn>F6ki& z(rVYA3hOLSdKi(%e_0NKd>BQOD2Qg!B3iHTJ%$MBVH1W{XW?qSpWUOLobVHJ9KN=P z>*3X+EM==Z+sn5J{zr`J%09P(oX;=Wv-cPiM#WKZ54Hq1_xlsZ*0TAl;lAwI8%nSV zZW|EvAL2-YZ6Z&T4Mbr&4GcL-82H!!$7k51q?7SF;>&^T!TUe;SKTO(up1)xGm5dcP^3Lx1vgP}Q4emtGj-+HQ+*#zCdWMRDvy3)(|`WkTF*2JwAZE?Vv5Ff zgw#`-Q92V*R7JDtchW@9quCnc?J8($gDl5njYB2WI9Upb8Xr-mHbpbYl4ew-kQ_dx znGYPLR1bV z1ywJ3V6_iy>Dw}}y>I)KwRf#m_Z>@8%QnAze9WJ!sGD%qr-b?kW_y2hsBWa{u6g4O zVfc!%P}*HQxa!iXH>#7SuUQzGv5?Yd(xqF{?oISp`^ai^8GConxQN5myDMGp`K9Bs zW1_tJG2!^{^x2YZ^2Pfu_n`Tbd8lf5%Z;6*`>*YvaBWTUpE*nJyO*abcYjj+37=Zw zpKu>c*$$>lD+W(rIz3$ZMtjnjR4!O&%&^aljo9412fnVt6X3y~C^q=0+d@&DgZ(=P zUsq<51sISK#$>c>GJ=(iM03D13v;tWFf&Lb0tStsF#|(ZEV5@AFk~enCjmskpAr5{ z65lL{!dX6Ggc-ABnm1#C89}s4W}q}TbE54m7qB2rvjL0~7qE(pLtCZS`Bh>gva(oe-wa|QJ4Aex_K$0^IH|e(}xHF7m zWC{#Kf&?>wPe_8#%w|nJ0CzA0Yv zW^@>$%+`wM;7tRZEiVx6=NmyPi$> zaKC6tx_I%O;;Jiw^rGc=7WuB$q)R<_N;h8hr;C?-W#-+Mr12{oDSK*o)kLW`$z8P0 z!jAB%y0bz75MYY>Dw-{WS3z`C5Ny@B+#b%cAJ(C-fLAqZ12Or(coqpJ%U-agh>Sbzt7_4#p|O8;WI-b8Z&EhadJgP4(D21? z-kBZ@xL1y!`O|uO}~zO#P~N>aSkQy`+&ceck~l2Sy*>{F~{) zn_s+n_DJLY9B~|t$2Xi0FRK}M>IRxcg|zQS$9Na`#fQ+|f|E;+cNm21i9l7l*5`Y| z_^uJJSw>#S=-SD6<603e^T*U(v3R?v+v1X-D5+9dMI(l8HT_H-%-7=*t8VeABTkju z%gY;Z!_8QI7b{dP&3L*q1gua{rYSFUtu*S(yWNF4sgLadO64L{z2vJB;&Kmk_H_=t z()Y?x>s_b!zWeFasx7xHV~Nzt`U!VK%GU62&Wd!!ihC8C$166E1tuzGsYG~AVPZb%6m%nzdMLp#4DP*<2ax?;%b%0kgXAsS>h z6ZryNL7gl#IT;$z6)vYM#&|glXKd6vgAXsfI`#9hsf#ytMe(xoR+yG+q0+dWF|kX= z6UpDlYCBfXLgnQOSZ?WvtHIdGR*;H09B)AC9LME4c%08}TSi(=cBzumUH(m`2kFdl zTzUkmUNVP;CFKjVFfA<4vT*sh0Ke+t(mO)+|HMKWjd-~5&e30?X!|{iwk@OwxCODz zZis8x1jN-C^&-lN2HN9Qpwml&y`Jfd?CE;A1;9a(MXGj2P@>=)#v9{3a$-C%@KntU3?Q zYiYm_j=OiQXWhDWCp=ThUo1?3lzD8*kmp{uAjBT4&OrNGPh5`9Um~Me$QU(GW6w_= zzGDrIjRzY~WNgjk)!#gP_iazNq1FR2ABSlOa7@^!)Gh=60q#N_5W|3{)Nm#&b*LCd z@f~ucUDa5n3jz~G6(v~)1g2PtvZ$G4Nr^`xG~)0!=Hn*|=hPTUK^vX3WjcoDlnTR| zg_R0tPQgm$CRCtQE>gTGX-bzYeoV~f^~w75;$=gj%Vo*E>Cz=b8!ojZpTED#JF@au zk+I6ZXdBu*%)GhfRtey#fw!**5*iV#9%&g%OqABY&Zb>ugN{p%Vbe(Q&?}$1*3H;q z(;2|*vZUibzA}-LWsix$zW%;zQL4OVq-Esj*vVT(x7bwaw!h!>54E>zKXv)@Ta*m_ z=$^|r?(+TD3=fN*C>VH;tlHr=+;*FHF6M7nY~ESI|7{5m{Wd4p)GU<^I=N35Oi|!$Unnw{~s5WFu)aM!3!X=HRoc&3o=DCw247f>(CS)8Vg_=HU>cDlOoU=tCMUml~qQ^393nV}wwIeE>5%pBo$2kT^ zlF?XUvO?*Q4?}mJ$&*7cHdiSNcmFZ2co8b#uCsXH<-V8iIjhE`J@CsV@7|AaBkLm*!Nn~>3rMMey15_>W^2z)Vxr*Fii!HidB^KY$ZA*nXCi?LL5`| z-wcM)wK*k4JE&J5$2f6bAl&Vk`~NL*z;#j>0w)W@-Y-As0}FTtNHmiuHHX04fxM9b zabZVDRwT^&G(Eg93xVNP5AF^pm4rVK#UOYYkj-LtP8x35fX@ZYr`ggh!|xyO}_ zb7e!e)S72gT-jajx%`+pRlSvt)#k=h<<%+Qfz*q~==kxpwdkI8>9}?2gmrmu{R8Kc z*Lr$)r!DRQU!M=cOy6bS$mY>)*S6g%xwm!i_}0A>mVLc-_c?R&>2rIjIw3OQNrORe zi5!DN>aqI^*ek`V1*>kTG|Lae(O8_Oo@u6yrqAReoJVxjtc6MGbvW$9ss^hySgpkh z)t6q29K{|+fx2E-Lhp&NO$W#A@;P`!J*A=g49hUgBaUOZSqot-pA-0*KPRrwiSu7c z4a9P;?Y*|gTsh-@w3#?pOgKEf*0if)Xy3T2y4RjAS~_%myl8c=BW*7qDj&B$)oUr} yl~xW{kC(1Vk>an7KVX=Rj|kR}WrLY<&Xg0*mh8U6F8+)Y&h46E3AcnE_rC$u?`0eS literal 0 HcmV?d00001 diff --git a/scripts/cos_upload.py b/scripts/cos_upload.py new file mode 100644 index 0000000..9da1dda --- /dev/null +++ b/scripts/cos_upload.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +""" +腾讯云COS文件上传工具 +用法: + python3 cos_upload.py [--content-type ] +""" +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()