Skill: zhenghuanluck/openclaw-oss-skills (version 1.0.0)

--- name: openclaw-oss-skills description: Upload generated artifacts from an OpenClaw workspace to an Alibaba Cloud OSS bucket using credentials from environment variables, then return a temporary signed download link in the conversation. Use when the user asks to upload, share, publish, or return downloadable links for generated files, archives, reports, images, documents, videos, or build outputs via OSS. version: 1.0.0 metadata: openclaw: requires: bins: - python3 env: - OSS_ACCESS_KEY_ID - OSS_ACCESS_KEY_SECRET - OSS_BUCKET - OSS_ENDPOINT primaryEnv: OSS_ACCESS_KEY_ID emoji: "☁️" homepage: https://docs.openclaw.ai/tools/skills install: - kind: brew formula: python@3 bins: - python3 label: Install Python 3 - kind: uv package: oss2 label: Install Alibaba Cloud OSS Python SDK --- # OpenClaw OSS Artifact Use this skill after creating a file, directory, or set of files that the user needs to download from an OSS signed URL. ## Requirements Credentials and defaults come from environment variables: - `OSS_ACCESS_KEY_ID` (required) - `OSS_ACCESS_KEY_SECRET` (required) - `OSS_BUCKET` (required) - `OSS_ENDPOINT` (required, for example `https://oss-cn-hangzhou.aliyuncs.com`) - `OSS_STS_TOKEN` (optional, for temporary credentials) - `OSS_PREFIX` (optional, object key prefix; default `openclaw-artifacts`) - `OSS_EXPIRES` (optional, signed URL lifetime in seconds; default `3600`) - `OSS_PUBLIC_ENDPOINT` (optional, endpoint used in the returned URL if it differs from upload endpoint) - `OSS_IS_CNAME` (optional, set to `1` when the endpoint is a custom OSS CNAME) The access key must have permission to put objects into the configured bucket and read them through signed URLs. The uploader uses the Alibaba Cloud `oss2` Python SDK. If it is not installed, install it with `python3 -m pip install oss2`. ## Workflow 1. Finish generating the artifact locally. 2. Confirm the local artifact path exists. 3. Run the bundled uploader: ```bash python3 skills/openclaw-oss-skills/scripts/upload_to_oss.py /absolute/path/to/artifact ``` For multiple files or a directory, pass each path. The script automatically creates a zip archive before upload: ```bash python3 skills/openclaw-oss-skills/scripts/upload_to_oss.py /path/to/file-a.pdf /path/to/output-dir ``` If the skill folder is the current directory, run the script directly: ```bash python3 scripts/upload_to_oss.py /absolute/path/to/artifact ``` 4. Return the `download_url` shown by the script to the user as a Markdown link. Mention the expiration time if the user may need to know it. ## Options - `--object-key <key>`: upload with an explicit OSS object key. Use only for a single file. - `--prefix <prefix>`: override `OSS_PREFIX`. - `--bucket <bucket>`: override `OSS_BUCKET`. - `--endpoint <endpoint>`: override `OSS_ENDPOINT`. - `--public-endpoint <endpoint>`: override `OSS_PUBLIC_ENDPOINT`. - `--expires <seconds>`: override `OSS_EXPIRES`. - `--json`: print machine-readable JSON only. ## Output Contract The script prints: - `object_key`: object path inside the bucket - `download_url`: signed URL for direct download - `expires_at`: ISO-8601 UTC expiration time - `source`: uploaded local file or temporary zip path In the final answer, include the signed link and keep the local path available for debugging if upload fails.

path: .idea/workspace.xml
content: <?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="AutoImportSettings">
    <option name="autoReloadType" value="ALL" />
  </component>
  <component name="ChangeListManager">
    <list default="true" id=[REDACTED_SECRET] name="Changes" comment="" />
    <option name="SHOW_DIALOG" value="false" />
    <option name="HIGHLIGHT_CONFLICTS" value="true" />
    <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
    <option name="LAST_RESOLUTION" value="IGNORE" />
  </component>
  <component name="EmbeddingIndexingInfo">
    <option name="cachedIndexableFilesCount" value="5" />
  </component>
  <component name="McpProjectServerCommands">
    <commands />
    <urls />
  </component>
  <component name="ProjectColorInfo"><![CDATA[{
  "associatedIndex": 4,
  "fromUser": false
}]]></component>
  <component name="ProjectId" id="3CxzhqgRTGImnFSXE0Va9k8v129" />
  <component name="ProjectViewState">
    <option name="hideEmptyMiddlePackages" value="true" />
    <option name="showLibraryContents" value="true" />
  </component>
  <component name="PropertiesComponent"><![CDATA[{
  "keyToString": {
    "ModuleVcsDetector.initialDetectionPerformed": "true",
    "RunOnceActivity.MCP Project settings loaded": "true",
    "RunOnceActivity.ShowReadmeOnStart": "true",
    "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
    "RunOnceActivity.typescript.service.memoryLimit.init": "true",
    "codeWithMe.voiceChat.enabledByDefault": "false",
    "node.js.detected.package.eslint": "true",
    "node.js.detected.package.tslint": "true",
    "node.js.selected.package.eslint": "(autodetect)",
    "node.js.selected.package.tslint": "(autodetect)",
    "nodejs_package_manager_path": "npm",
    "vue.rearranger.settings.migration": "true"
  }
}]]></component>
  <component name="SharedIndexes">
    <attachedChunks>
      <set>
        <option value="bundled-python-sdk-b63d5a1f7c97-b61e75351b1f-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-261.22158.340" />
      </set>
    </attachedChunks>
  </component>
  <component name="TaskManager">
    <task active="true" id="Default" summary="Default task">
      <changelist id=[REDACTED_SECRET] name="Changes" comment="" />
      <created>1777342131243</created>
      <option name="number" value="Default" />
      <option name="presentableId" value="Default" />
      <updated>1777342131243</updated>
      <workItem from="1777342132359" duration="2228000" />
    </task>
    <servers />
  </component>
  <component name="TypeScriptGeneratedFilesManager">
    <option name="version" value="3" />
  </component>
</project>

path: scripts/upload_to_oss.py
content: #!/usr/bin/env python3
"""Upload generated artifacts to Alibaba Cloud OSS and print a signed URL."""

from __future__ import annotations

import argparse
import datetime as dt
import json
import mimetypes
import os
from pathlib import Path
import tempfile
import time
from typing import Iterable
import uuid
import zipfile


def env(name: str, default: str | None = None) -> str | None:
    value = os.environ.get(name)
    return value if value not in (None, "") else default


def require(value: str | None, name: str) -> str:
    if not value:
        raise SystemExit(f"Missing required configuration: {name}")
    return value


def make_bucket(
    endpoint: str,
    bucket: str,
    access_key_id: str,
    access_key_secret: str,
    security_token: str | None,
) -> object:
    try:
        import oss2
    except ImportError as exc:
        raise SystemExit(
            "Missing Python dependency: oss2. Install it with `python3 -m pip install oss2`."
        ) from exc

    auth = (
        oss2.StsAuth(access_key_id, access_key_secret, security_token)
        if security_token
        else oss2.Auth(access_key_id, access_key_secret)
    )
    return oss2.Bucket(auth, endpoint, bucket, is_cname=env("OSS_IS_CNAME") == "1")


def build_signed_get_url(
    endpoint: str,
    bucket: str,
    key: str,
    access_key_id: str,
    access_key_secret: str,
    expires_in: int,
    security_token: str | None,
) -> tuple[str, int]:
    expires = int(time.time()) + expires_in
    client = make_bucket(endpoint, bucket, access_key_id, access_key_secret, security_token)
    return client.sign_url("GET", key, expires_in), expires


def iter_files(path: Path) -> Iterable[Path]:
    for item in sorted(path.rglob("*")):
        if item.is_file():
            yield item


def make_archive(paths: list[Path]) -> Path:
    timestamp = dt.datetime.now(dt.UTC).strftime("%Y%m%dT%H%M%SZ")
    archive = Path(tempfile.gettempdir()) / f"openclaw-artifact-{timestamp}-{uuid.uuid4().hex[:8]}.zip"
    with zipfile.ZipFile(archive, "w", compression=zipfile.ZIP_DEFLATED) as zf:
        for source in paths:
            if source.is_dir():
                base = source.parent
                for file_path in iter_files(source):
                    zf.write(file_path, file_path.relative_to(base).as_posix())
            else:
                zf.write(source, source.name)
    return archive


def resolve_source(paths: list[str]) -> Path:
    if not paths:
        raise SystemExit("At least one artifact path is required")
    resolved = [Path(p).expanduser().resolve() for p in paths]
    missing = [str(p) for p in resolved if not p.exists()]
    if missing:
        raise SystemExit("Artifact path not found: " + ", ".join(missing))
    if len(resolved) == 1 and resolved[0].is_file():
        return resolved[0]
    return make_archive(resolved)


def default_object_key(prefix: str, source: Path) -> str:
    clean_prefix = prefix.strip("/")
    timestamp = dt.datetime.now(dt.UTC).strftime("%Y/%m/%d")
    safe_name = source.name.replace("\\", "_").replace("/", "_")
    unique = uuid.uuid4().hex[:12]
    parts = [part for part in [clean_prefix, timestamp, f"{unique}-{safe_name}"] if part]
    return "/".join(parts)


def upload_file(
    source: Path,
    bucket: str,
    key: str,
    endpoint: str,
    access_key_id: str,
    access_key_secret: str,
    security_token: str | None,
) -> None:
    try:
        import oss2
    except ImportError as exc:
        raise SystemExit(
            "Missing Python dependency: oss2. Install it with `python3 -m pip install oss2`."
        ) from exc

    content_type = mimetypes.guess_type(source.name)[0] or "application/octet-stream"
    client = make_bucket(endpoint, bucket, access_key_id, access_key_secret, security_token)
    try:
        client.put_object_from_file(key, str(source), headers={"Content-Type": content_type})
    except oss2.exceptions.OssError as exc:
        raise SystemExit(f"OSS upload failed: {exc}") from exc


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("paths", nargs="+", help="Files or directories to upload")
    parser.add_argument("--bucket", default=env("OSS_BUCKET"))
    parser.add_argument("--endpoint", default=env("OSS_ENDPOINT"))
    parser.add_argument("--public-endpoint", default=env("OSS_PUBLIC_ENDPOINT") or env("OSS_ENDPOINT"))
    parser.add_argument("--prefix", default=env("OSS_PREFIX", "openclaw-artifacts"))
    parser.add_argument("--expires", type=int, default=int(env("OSS_EXPIRES", "3600") or "3600"))
    parser.add_argument("--object-key", help="Explicit object key. Allowed only for a single file.")
    parser.add_argument("--json", action="store_true", help="Print JSON only")
    return parser.parse_args()


def main() -> None:
    args = parse_args()
    [REDACTED_SECRET]"OSS_ACCESS_KEY_ID"), "OSS_ACCESS_KEY_ID")
    [REDACTED_SECRET]"OSS_ACCESS_KEY_SECRET"), "OSS_ACCESS_KEY_SECRET")
    bucket = require(args.bucket, "OSS_BUCKET")
    endpoint = require(args.endpoint, "OSS_ENDPOINT")
    public_endpoint = require(args.public_endpoint, "OSS_PUBLIC_ENDPOINT or OSS_ENDPOINT")
    security_token = env("OSS_STS_TOKEN")

    source = resolve_source(args.paths)
    original_paths = [Path(p).expanduser().resolve() for p in args.paths]
    if args.object_key and (len(original_paths) != 1 or original_paths[0].is_dir()):
        raise SystemExit("--object-key is only supported for a single file upload")
    key = args.object_key or default_object_key(args.prefix, source)

    upload_file(source, bucket, key, endpoint, access_key_id, access_key_secret, security_token)
    download_url, expires_epoch = build_signed_get_url(
        public_endpoint,
        bucket,
        key,
        access_key_id,
        access_key_secret,
        args.expires,
        security_token,
    )
    result = {
        "bucket": bucket,
        "object_key": key,
        "download_url": download_url,
        "expires_at": dt.datetime.fromtimestamp(expires_epoch, dt.UTC)
        .replace(microsecond=0)
        .isoformat()
        .replace("+00:00", "Z"),
        "source": str(source),
    }
    if args.json:
        print(json.dumps(result, ensure_ascii=False, indent=2))
        return
    print(f"object_key: {result['object_key']}")
    print(f"download_url: {result['download_url']}")
    print(f"expires_at: {result['expires_at']}")
    print(f"source: {result['source']}")


if __name__ == "__main__":
    main()


path: agents/openai.yaml
content: interface:
  display_name: "OSS Artifact Upload"
  short_description: "Uploads artifacts and returns signed OSS links"
  brand_color: "#2563EB"
  default_prompt: "Use $openclaw-oss-skills to upload the generated artifact to OSS and return a signed download link."

policy:
  allow_implicit_invocation: true


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)