Skill: liangshenghzj888-stack/wanjie-openclaw-video-v2-0-1 (version 2.0.0)

# Skill: wanjie-video-skill ### 作者: 何振杰 ## 功能描述 一个高性能 Veo 视频生成技能，支持通过自然语言一键生成。现已升级为全自动守护模式。 ## 适用场景 - 自动化视频创作。 - 无需命令行参数，通过自然语言对话生成视频。 - 具备自动依赖修复、超时自愈、任务防重及全自动后台监控功能。 ## 安装说明 1. 使用 `clawhub install wanjie-openclaw-video-v2-0-1` 安装。 2. 确保在 `~/.openclaw/openclaw.json` 中配置了有效的 API Key。 ## 使用方法 安装后，直接在聊天窗口输入： > 生成视频：[您的提示词] 后台会启动定时监控（每5分钟一次），若检测到空闲，会自动处理任务并将结果通过日志文件同步。 ## 运行机制 - 拦截指令后，自动调用后台进程生成。 - 具备 30 分钟任务超时强制清理机制，防止任务卡死。 - 支持自动依赖安装（requests 库）。 - 生成结果会自动存入 `veo_result.txt`，支持定时任务闭环管理。 ## 注意事项 - 请确保系统环境已安装 Python。 - 本插件部署了 Windows 任务计划程序 `OpenClaw_Veo_Monitor` 进行自动化监控。

path: hooks.js
content: module.exports = async (ctx) => {
    const text = ctx.message.text || "";
    if (text.startsWith('生成视频：')) {
        const prompt = text.replace('生成视频：', '').trim();
        const { spawn } = require('child_process');
        const path = require('path');
        
        // 修正为指向项目根目录下的 scripts/veo_worker.py
        const scriptPath = path.join(__dirname, 'model', 'scripts', 'veo_worker.py');
        
        const child = spawn('python', [scriptPath, '--prompt', prompt, '--model', 'veo3.1', '--seconds', '8'], {
            detached: true,
            stdio: 'ignore'
        });
        child.unref();
        
        return { reply: "正在后台为您生成视频，完成后会自动在浏览器打开。" };
    }
};

path: manifest.json
content: {
  "name": "wanjie-video-skill",
  "version": "1.0.0",
  "author": "何振杰",
  "description": "基于 Veo 模型的视频生成技能，支持通过“生成视频：[提示词]”唤起。",
  "dependencies": ["requests"],
  "main": "veo_worker.py",
  "hooks": {
    "onMessage": "hooks.js"
  }
}

path: model/scripts/veo_worker.py
content: # veo_worker.py
import requests, time, argparse, os, json, sys, re, platform, subprocess, traceback

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
LOG = os.path.join(BASE_DIR, "veo_log.txt")
LOCK = os.path.join(BASE_DIR, "veo.lock")
RESULT = os.path.join(BASE_DIR, "veo_result.txt")

URL = "https://maas-openapi.wanjiedata.com/api/v1/chat/completions"


def log(msg: str):
    """每条日志独立追加写入,确保实时落盘;同时输出到原始 stdout。"""
    ts = time.strftime('%Y-%m-%d %H:%M:%S')
    line = f"[{ts}] {msg}"
    try:
        with open(LOG, "a", encoding="utf-8") as f:
            f.write(line + "\n")
            f.flush()
            os.fsync(f.fileno())
    except Exception:
        # 写日志失败也不要影响主流程
        pass

    try:
        print(line, file=sys.__stdout__, flush=True)
    except Exception:
        pass


def log_exc(prefix: str, e: Exception):
    tb = traceback.format_exc()
    log(f"{prefix}: {repr(e)}")
    for tline in tb.rstrip().splitlines():
        log(f"{prefix} TB: {tline}")


def open_url(url):
    """跨平台打开 URL"""
    try:
        if platform.system() == 'Windows':
            os.startfile(url)
        elif platform.system() == 'Darwin':  # macOS
            subprocess.Popen(['open', url])
        else:  # Linux
            subprocess.Popen(['xdg-open', url])
    except Exception as e:
        log_exc("[!] 打开URL失败", e)


def is_process_running(pid):
    """检查指定 PID 的进程是否在运行"""
    try:
        import psutil
        return psutil.pid_exists(pid)
    except ImportError:
        # 没有 psutil:Windows 用 tasklist 粗略判断;其他平台保守不删锁
        try:
            if platform.system() == 'Windows':
                res = subprocess.run(['tasklist', '/FI', f'PID eq {pid}'],
                                     capture_output=True, text=True)
                return str(pid) in res.stdout
        except Exception:
            pass
        return True


def _read_api_key_from_openclaw():
    config_path = os.path.join(os.path.expanduser("~"), ".openclaw", "openclaw.json")
    with open(config_path, 'r', encoding='utf-8') as f:
        cfg = json.load(f)
    return list(cfg['models']['providers'].values())[0]['apiKey']


def generate_video(prompt, model, seconds):
    log("任务初始化")
    log(f"--- 新任务开始 ---")
    log(f"任务输入: prompt='{prompt}', model='{model}', seconds={seconds}")

    log(f"检查锁文件: {LOCK}")
    if os.path.exists(LOCK):
        try:
            with open(LOCK, "r", encoding="utf-8") as f:
                pid = int(f.read().strip() or "0")

            mtime = os.path.getmtime(LOCK)
            if time.time() - mtime > 1800:
                log(f"[!] 任务超时 (PID: {pid}),清理锁...")
                os.remove(LOCK)
            elif pid and is_process_running(pid):
                log(f"[!] 发现活跃任务 (PID: {pid}),当前任务退出。")
                return
            else:
                log(f"发现僵尸锁文件 (PID: {pid}),清理...")
                os.remove(LOCK)
        except Exception as e:
            log_exc("[!] 锁文件处理异常", e)

    log(f"创建锁文件 (PID: {os.getpid()})")
    try:
        with open(LOCK, "w", encoding="utf-8") as f:
            f.write(str(os.getpid()))
            f.flush()
    except Exception as e:
        log_exc("[!] 无法写入锁文件", e)
        return

    try:
        # APIKEY获取方式保持不变
        key = _read_api_key_from_openclaw()
        log(f"API Key 指纹: {key[:6]}...{key[-4:]}")  # 仅日志中打印指纹,避免泄露

        payload = {
            "model": "veo3.1",
            "messages": [{"role": "user", "content": prompt}],
            "stream": True
        }
        log(f"Request URL: {URL}")
        log(f"Payload: {json.dumps(payload, ensure_ascii=False)}")

        headers = {
            "Authorization": f"Bearer {key}",
            "Content-Type": "application/json",
            "Accept": "text/event-stream",
        }

        success = False
        session = requests.Session()

        for attempt in range(1, 21):
            log(f"尝试获取视频 (第 {attempt}/20 次)...")

            try:
                # 请求发起打点
                start_t = time.time()
                log("发送请求...")

                res = session.post(
                    URL,
                    headers=headers,
                    json=payload,
                    stream=True,
                    timeout=60
                )

                log(f"收到响应: HTTP {res.status_code} (耗时 {time.time() - start_t:.2f}s)")
                if res.status_code != 200:
                    # 400/401等这里通常有明确原因
                    try:
                        log(f"Response headers: {dict(res.headers)}")
                        log(f"Response body: {res.text}")
                    except Exception as e:
                        log_exc("[!] 读取错误响应失败", e)

                res.raise_for_status()

                full = ""
                got_first_chunk = False
                chunks = 0

                # 读取流式内容
                for raw in res.iter_lines(decode_unicode=True):
                    if not raw:
                        continue

                    if not got_first_chunk:
                        got_first_chunk = True
                        log("已收到首个分块(开始流式读取)")

                    line = raw.strip()
                    chunks += 1

                    # 兼容 SSE: "data: {...}" / 或直接就是 json
                    if line.startswith("data:"):
                        data_str = line[5:].strip()
                    else:
                        data_str = line

                    if data_str == "[DONE]":
                        log("收到 [DONE],流结束")
                        break

                    try:
                        d = json.loads(data_str)
                    except Exception:
                        # 可能是 event:/id: 等非json行,忽略
                        continue

                    # 兼容多种返回结构:delta.content / message.content
                    try:
                        choice = (d.get("choices") or [None])[0] or {}
                        delta = choice.get("delta") or {}
                        msg = choice.get("message") or {}
                        piece = (delta.get("content") or msg.get("content") or "")
                        if piece:
                            full += piece
                    except Exception:
                        pass

                    # 每隔一些chunk打一次点(避免日志爆炸)
                    if chunks % 50 == 0:
                        log(f"流读取中... chunks={chunks}, accumulated_len={len(full)}")

                log(f"流读取完成: chunks={chunks}, accumulated_len={len(full)}")

                match = re.search(r'(https?://\S+)', full)
                if match:
                    url = match.group(1).rstrip(')"\'').replace(')', '').replace(']', '')
                    with open(RESULT, "w", encoding="utf-8") as f:
                        f.write(f"SUCCESS|{url}")
                        f.flush()
                    open_url(url)
                    log(f"成功获取并打开链接: {url}")
                    success = True
                    break
                else:
                    log("[!] 未在返回内容中提取到URL,将重试。")
                    time.sleep(2)

            except Exception as e:
                log_exc(f"[!] 第 {attempt} 次尝试失败", e)
                time.sleep(2)

        if not success:
            with open(RESULT, "w", encoding="utf-8") as f:
                f.write("FAILED|生成失败,请重试")
                f.flush()
            log("[!] 20次重试失败,生成任务结束。")

    finally:
        if os.path.exists(LOCK):
            try:
                os.remove(LOCK)
                log("锁文件已清理。")
            except Exception as e:
                log_exc("[!] 锁文件清理失败", e)


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--prompt", required=True)
    parser.add_argument("--model", default="veo3.1")
    parser.add_argument("--seconds", type=int, default=8)
    args = parser.parse_args()
    
    generate_video(args.prompt, args.model, args.seconds)

path: model/scripts/video_interface.py
content: import subprocess, os, sys, platform, importlib

# 基于脚本所在目录动态定位 worker 脚本
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
WORKER_PATH = os.path.join(BASE_DIR, "veo_worker.py")

def ensure_dependencies():
    """自动确保必要的依赖已安装"""
    try:
        importlib.import_module("requests")
    except ImportError:
        print("[*] 正在安装缺失依赖: requests...")
        subprocess.check_call([sys.executable, "-m", "pip", "install", "requests"])

def trigger_veo_generation(prompt, model="veo3.1-fast", seconds=8):
    # 在触发前确保环境就绪
    ensure_dependencies()
    
    # 使用 Python 解释器调用相对路径下的脚本
    cmd = [
        sys.executable,
        WORKER_PATH,
        "--prompt", prompt,
        "--model", model,
        "--seconds", str(seconds)
    ]
    
    # 跨平台进程启动设置
    if platform.system() == 'Windows':
        # 将输出重定向到 NUL 以防止 OpenClaw 报错
        with open(os.devnull, 'w') as devnull:
            subprocess.Popen(cmd, stdout=devnull, stderr=devnull, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP)
    else:
        # Linux/macOS 上启动新进程
        with open(os.devnull, 'w') as devnull:
            subprocess.Popen(cmd, stdout=devnull, stderr=devnull, start_new_session=True)
        
    return f"[*] 任务已提交: {prompt}，请在聊天窗口耐心等待结果通知。"

if __name__ == "__main__":
    if len(sys.argv) > 1:
        print(trigger_veo_generation(" ".join(sys.argv[1:])))
    else:
        print("Usage: python video_interface.py <prompt>")


path: README.md
content: # wanjie OpenClaw Video - Veo 视频生成技能

本技能提供了一个基于[万界方舟模型即服务平台](https://www.wjark.com/) Veo 模型的全自动视频生成工作流，专为 OpenClaw 平台开发。现已全面升级为全自动守护模式，支持环境自愈与异常自动重启。

## 功能特性
- **自然语言触发**：无需复杂命令行，支持直接对话式唤起生成。
- **Veo 模型集成**：适配 veo3.1 系列，支持高性能快速生成。
- **全自动守护进程**：生成任务在后台独立运行，具备 30 分钟超时清理与自动轮询调度功能。
- **环境自愈**：初次启动自动检测缺失的 Python 依赖 (requests)。
- **任务防重**：集成 PID 锁机制，防止并发冲突及多个结果链接重复弹出。
- **结果可视化**：通过 `veo_result.txt` 闭环管理任务结果。

## 目录结构
```text
wanjie-video-skill/
├── manifest.json       # 技能入口定义
├── hooks.js            # 对话拦截器
├── SKILL.md            # 技能元数据
├── README.md           # 使用说明
└── model/
    └── scripts/
        ├── veo_worker.py        # 生成逻辑引擎
        └── video_interface.py   # 接口调用层
```

## 快速开始
1. **安装**: 在 OpenClaw 终端中执行 `clawhub install wanjie-openclaw-video-v2-0-1`。
2. **触发**: 在聊天框中输入：`生成视频：[您的视频描述]`。
3. **自动化**: 系统会每 5 分钟自动巡检任务状态。若任务异常终止，守护进程会自动触发重新运行。

## 故障排查
- **任务阻塞**: 系统现在具备自动超时清理机制，若手动清理锁文件请删除 `model/scripts/veo.lock`。
- **依赖异常**: 若初次运行报错，请确保环境已连通网络以自动下载缺失的 Python 库。


path: requirements.txt
content: requests>=2.25.1


path: _meta.json
content: ownerId: kn7bjvzr0sw0ys43dxg81g1w2984cwpk
slug: wanjie-openclaw-video-v2-0-1
version: 1.0.0
publishedAt: 1775800667828

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