Skill: kelcey2023/zeelin-weibo-autopost (version 0.1.0)

--- name: ZeeLin 微博自动发布 description: "通过浏览器操作网页版微博，用户登录后 Agent 撰写内容并自动发布。无需 API，需 Browser Relay 挂上微博标签页。Keywords: 微博, 发微博, 自动发布, Weibo, ZeeLin." user-invocable: true metadata: {"openclaw":{"emoji":"📱","skillKey":"zeelin-weibo-autopost"}} --- # ZeeLin 微博自动发布 📱 通过**网页版微博**（weibo.com）自动发微博：用户先登录，Agent 撰写内容后用脚本打开微博、填入正文、点击发布。 --- ## 何时触发 - 「发一条微博」「帮我发微博」「自动发微博」 - 「把这段话发到微博」「写一条微博并发布」 - 「微博自动发布」 **爬取微博**：用户说「爬取微博」「抓取微博内容」时，见 TOOLS.md「爬取微博（防卡住）」——用 web_fetch 抓**至多 1～2 条具体链接**，或 browser **只 open + snapshot 一次**即返回，不要无限滚动或多次操作，否则易卡住。 --- ## 使用前准备 1. **登录微博**：在 Chrome 打开 https://weibo.com 并登录。 2. **挂上 Browser Relay**：在该标签页点击 OpenClaw Browser Relay 扩展，Badge 显示 **ON**。 3. 执行发微博时脚本会打开微博页、找发微博入口、输入内容、点发布。 --- ## 工作流程 ### Step 1：确认内容 用户给文案或主题。若只给主题，Agent 生成一条微博正文（建议 140～500 字，可带话题 #xxx）。 ### Step 2：执行发布脚本 用 `exec` 执行脚本，传入微博正文。**必须传 timeout**（建议 60000）避免 request timed out： ```json {"tool": "exec", "args": {"command": "bash /Users/youke/.openclaw/workspace/skills/zeelin-weibo-autopost/scripts/post_weibo.sh \"微博正文内容\"", "timeout": 60000}} ``` （若报脚本未找到，请把路径中的 `/Users/youke` 改成你本机用户家目录；Windows 用户改为实际 SKILL 路径，如 `C:/Users/xxx/AppData/Roaming/ZeeLinClaw/SKILLs/zeelin-weibo-autopost/scripts/post_weibo.sh`。） ### Step 3：回报结果 根据脚本输出告诉用户「已提交发布，请到微博页面确认」；若脚本报错，提示检查是否已登录、Relay 是否挂上。 --- ## exec 速查 | 操作 | 命令 | |------|------| | 发微博 | `bash .../zeelin-weibo-autopost/scripts/post_weibo.sh "正文"`，exec 加 `"timeout": 60000` | --- ## 故障排查 | 现象 | 处理 | |------|------| | 脚本未找到 | 使用绝对路径，与 openclaw.json / ZeeLin Claw 的 workspace 一致。 | | openclaw browser 无输出 | 需已用 Browser Relay 挂上微博标签页（Badge ON），且 Gateway 已启动。 | | **脚本执行了但微博没发出去** | ① 确认发微博时**当前 Relay 挂上的就是微博页**（weibo.com），且已登录。② 脚本会把每次快照写入 ` /tmp/weibo_snap.txt`，若仍失败请打开该文件，看是否有「发微博」「说点什么」「发布」等字样及对应的 `ref=e数字`；若页面结构变了，可把快照中相关几行发给开发者以更新选择器。③ 微博首页有时是信息流，需先点击顶栏或左侧的「发微博」打开发布框，脚本已包含该步骤；若你看到发布框已打开但未填入或未点发布，多半是输入框/发布按钮的 ref 与当前页面不匹配，需根据快照调整。 |

path: _meta.json
content: slug: zeelin-weibo-autopost
version: 1.0.0

path: scripts/cdp_weibo_ops.py
content: #!/usr/bin/env python3
"""
Weibo post via CDP (no OpenClaw CLI). Requires Chrome --remote-debugging-port + --remote-allow-origins=*.
User must be logged in; opens / publishes from home composer when possible.
"""
from __future__ import annotations

import json
import os
import sys
import time
import urllib.request

import websocket


def port() -> int:
    return int(os.environ.get("OPENCLAW_CDP_PORT", "9222"))


def list_tabs(p: int):
    with urllib.request.urlopen(f"http://127.0.0.1:{p}/json", timeout=8) as r:
        return json.loads(r.read())


def activate(p: int, tid: str):
    try:
        urllib.request.urlopen(f"http://127.0.0.1:{p}/json/activate/{tid}", timeout=5)
    except Exception:
        pass


def find_weibo(p: int):
    for t in list_tabs(p):
        if t.get("type") == "page" and "weibo.com" in t.get("url", ""):
            return t
    return None


def connect_ws(ws_url: str, p: int):
    return websocket.create_connection(ws_url, timeout=15, origin=f"http://127.0.0.1:{p}")


def cdp_send(ws, method: str, params=None, timeout=20):
    msg_id = int(time.time() * 1000) % 1_000_000_000
    ws.send(json.dumps({"id": msg_id, "method": method, "params": params or {}}))
    deadline = time.time() + timeout
    while time.time() < deadline:
        ws.settimeout(max(0.5, deadline - time.time()))
        try:
            raw = ws.recv()
        except websocket.WebSocketTimeoutException:
            continue
        data = json.loads(raw)
        if data.get("id") == msg_id:
            if "error" in data:
                raise RuntimeError(str(data["error"]))
            return data.get("result", {})
    raise TimeoutError(method)


def js_eval(ws, expr: str, timeout=15):
    r = cdp_send(
        ws,
        "Runtime.evaluate",
        {"expression": expr, "returnByValue": True, "awaitPromise": False},
        timeout=timeout,
    )
    res = r.get("result", {})
    if res.get("type") == "string":
        return res.get("value", "")
    if res.get("type") == "boolean":
        return res.get("value", False)
    if res.get("type") == "number":
        return res.get("value", 0)
    return res.get("value", "")


def js_escape(s: str) -> str:
    return json.dumps(s)


def set_composer_text(ws, text: str) -> str:
    expr = f"""
    (() => {{
        const sels = [
            'textarea[placeholder*="新鲜事"]',
            'textarea[placeholder*="有什么新鲜事"]',
            'textarea.Form_textarea',
            'textarea',
            '[contenteditable="true"]'
        ];
        for (const s of sels) {{
            const el = document.querySelector(s);
            if (!el || el.offsetParent === null) continue;
            el.focus();
            el.click();
            if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {{
                el.value = {js_escape(text)};
                el.dispatchEvent(new Event('input', {{ bubbles: true }}));
                el.dispatchEvent(new Event('change', {{ bubbles: true }}));
                return 'OK:VALUE:' + el.value.length;
            }}
            el.textContent = {js_escape(text)};
            el.dispatchEvent(new InputEvent('input', {{ bubbles: true, data: {js_escape(text)}, inputType: 'insertText' }}));
            return 'OK:TEXT:' + (el.innerText || el.textContent || '').length;
        }}
        return 'ERROR:NO_COMPOSER';
    }})()
    """
    return str(js_eval(ws, expr))


def composer_state(ws) -> dict:
    raw = js_eval(
        ws,
        """(() => {
            const ta = document.querySelector('textarea[placeholder*="新鲜事"]') || document.querySelector('textarea');
            const ce = document.querySelector('[contenteditable="true"]');
            const send = [...document.querySelectorAll('button')].find(x => (x.textContent || '').replace(/\s+/g,'').trim() === '发送');
            return JSON.stringify({
                textareaValue: ta ? ta.value || '' : '',
                editableText: ce ? (ce.innerText || ce.textContent || '') : '',
                sendDisabled: send ? !!send.disabled : null,
                sendText: send ? (send.textContent || '').trim() : '',
            });
        })()""",
    )
    try:
        return json.loads(raw) if isinstance(raw, str) else {}
    except Exception:
        return {}


def main():
    if len(sys.argv) < 2:
        print("Usage: cdp_weibo_ops.py <content>", file=sys.stderr)
        sys.exit(1)

    text = (sys.argv[1] or "").strip()
    p = port()
    tab = find_weibo(p)
    if not tab:
        print("ERROR: No weibo.com tab. Open Weibo in CDP Chrome.", file=sys.stderr)
        sys.exit(1)

    activate(p, tab["id"])
    time.sleep(0.3)
    ws = connect_ws(tab["webSocketDebuggerUrl"], p)
    try:
        cdp_send(ws, "Page.enable", {})

        # Ensure home (composer on feed)
        url = tab.get("url", "")
        if "weibo.com" in url and "/compose" not in url and "/status" not in url:
            pass
        else:
            cdp_send(ws, "Page.navigate", {"url": "https://weibo.com/"})
            time.sleep(4)

        focus_js = f"""
        (() => {{
            const sels = [
                'textarea[placeholder*="新鲜事"]',
                'textarea[placeholder*="有什么新鲜事"]',
                'textarea.Form_textarea',
                'textarea',
                '[contenteditable="true"]'
            ];
            for (const s of sels) {{
                const el = document.querySelector(s);
                if (el && el.offsetParent !== null) {{
                    el.focus();
                    el.click();
                    return "OK:" + s;
                }}
            }}
            return "ERROR:NO_COMPOSER";
        }})()
        """
        r = js_eval(ws, focus_js)
        print("Focus:", r)
        if "ERROR" in str(r):
            cdp_send(ws, "Page.navigate", {"url": "https://weibo.com/"})
            time.sleep(4)
            r = js_eval(ws, focus_js)
            print("Focus retry:", r)
            if "ERROR" in str(r):
                sys.exit(1)

        time.sleep(0.2)
        set_result = set_composer_text(ws, text)
        print("Composer:", set_result)
        if "ERROR" in set_result:
            sys.exit(1)

        state = composer_state(ws)
        if len((state.get("textareaValue") or state.get("editableText") or "").strip()) < min(20, len(text)):
            print("ERROR: Composer text did not stick.", file=sys.stderr)
            sys.exit(1)

        for _ in range(15):
            time.sleep(0.3)
            state = composer_state(ws)
            if state.get("sendText") == "发送" and not state.get("sendDisabled"):
                break

        click_js = """
        (() => {
            function visible(el) {
                return el && el.offsetParent !== null && getComputedStyle(el).visibility !== 'hidden';
            }
            function textOf(el) {
                return (el.textContent || '').replace(/\\s+/g, '').trim();
            }
            function hardClick(el) {
                el.scrollIntoView({block: 'center'});
                el.click();
                el.dispatchEvent(new MouseEvent('mousedown', {bubbles: true}));
                el.dispatchEvent(new MouseEvent('mouseup', {bubbles: true}));
                el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
            }
            const exactSend = [...document.querySelectorAll('button')].find(n => visible(n) && textOf(n) === '发送' && !n.disabled);
            if (exactSend) {
                hardClick(exactSend);
                return 'CLICK:SEND_BUTTON';
            }
            const candidates = [];
            for (const sel of ['[node-type="publishBtn"]','a[action-type="publish"]','button','a','div[role="button"]','span']) {
                for (const n of document.querySelectorAll(sel)) {
                    if (!visible(n)) continue;
                    const t = textOf(n);
                    if ((t === '发送' || t === '发布' || t.endsWith('发布')) && n.getAttribute('aria-disabled') !== 'true') {
                        candidates.push(n);
                    }
                }
            }
            const pick = candidates[0];
            if (pick) {
                hardClick(pick);
                return 'CLICK:' + textOf(pick);
            }
            return 'ERROR:NO_PUBLISH_BTN';
        })()
        """
        out = js_eval(ws, click_js)
        print("Ops:", out)
        time.sleep(2)
        if "ERROR" in str(out):
            sys.exit(1)

        verify = composer_state(ws)
        remaining = (verify.get("textareaValue") or verify.get("editableText") or "").strip()
        if remaining:
            print("WARN: Composer still contains text after click; retrying send.")
            out = js_eval(ws, click_js)
            print("Ops retry:", out)
            time.sleep(3)
            verify = composer_state(ws)
            remaining = (verify.get("textareaValue") or verify.get("editableText") or "").strip()
            if remaining:
                print("ERROR: Weibo publish did not clear composer; likely not submitted.", file=sys.stderr)
                sys.exit(1)

        print("Weibo Ops flow completed.")
    finally:
        ws.close()


if __name__ == "__main__":
    main()


path: scripts/cdp_weibo_publish.py
content: #!/usr/bin/env python3
"""
Weibo post via CDP (no OpenClaw CLI). Requires Chrome --remote-debugging-port + --remote-allow-origins=*.
User must be logged in; opens / publishes from home composer when possible.
"""
from __future__ import annotations

import json
import os
import sys
import time
import urllib.request

import websocket


def port() -> int:
    return int(os.environ.get("OPENCLAW_CDP_PORT", "9222"))


def list_tabs(p: int):
    with urllib.request.urlopen(f"http://127.0.0.1:{p}/json", timeout=8) as r:
        return json.loads(r.read())


def activate(p: int, tid: str):
    try:
        urllib.request.urlopen(f"http://127.0.0.1:{p}/json/activate/{tid}", timeout=5)
    except Exception:
        pass


def find_weibo(p: int):
    for t in list_tabs(p):
        if t.get("type") == "page" and "weibo.com" in t.get("url", ""):
            return t
    return None


def connect_ws(ws_url: str, p: int):
    return websocket.create_connection(ws_url, timeout=15, origin=f"http://127.0.0.1:{p}")


def cdp_send(ws, method: str, params=None, timeout=20):
    msg_id = int(time.time() * 1000) % 1_000_000_000
    ws.send(json.dumps({"id": msg_id, "method": method, "params": params or {}}))
    deadline = time.time() + timeout
    while time.time() < deadline:
        ws.settimeout(max(0.5, deadline - time.time()))
        try:
            raw = ws.recv()
        except websocket.WebSocketTimeoutException:
            continue
        data = json.loads(raw)
        if data.get("id") == msg_id:
            if "error" in data:
                raise RuntimeError(str(data["error"]))
            return data.get("result", {})
    raise TimeoutError(method)


def js_eval(ws, expr: str, timeout=15):
    r = cdp_send(
        ws,
        "Runtime.evaluate",
        {"expression": expr, "returnByValue": True, "awaitPromise": False},
        timeout=timeout,
    )
    res = r.get("result", {})
    if res.get("type") == "string":
        return res.get("value", "")
    if res.get("type") == "boolean":
        return res.get("value", False)
    if res.get("type") == "number":
        return res.get("value", 0)
    return res.get("value", "")


def js_escape(s: str) -> str:
    return json.dumps(s)


def set_composer_text(ws, text: str) -> str:
    expr = f"""
    (() => {{
        const sels = [
            'textarea[placeholder*="新鲜事"]',
            'textarea[placeholder*="有什么新鲜事"]',
            'textarea.Form_textarea',
            'textarea',
            '[contenteditable="true"]'
        ];
        for (const s of sels) {{
            const el = document.querySelector(s);
            if (!el || el.offsetParent === null) continue;
            el.focus();
            el.click();
            if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {{
                el.value = {js_escape(text)};
                el.dispatchEvent(new Event('input', {{ bubbles: true }}));
                el.dispatchEvent(new Event('change', {{ bubbles: true }}));
                return 'OK:VALUE:' + el.value.length;
            }}
            el.textContent = {js_escape(text)};
            el.dispatchEvent(new InputEvent('input', {{ bubbles: true, data: {js_escape(text)}, inputType: 'insertText' }}));
            return 'OK:TEXT:' + (el.innerText || el.textContent || '').length;
        }}
        return 'ERROR:NO_COMPOSER';
    }})()
    """
    return str(js_eval(ws, expr))


def composer_state(ws) -> dict:
    raw = js_eval(
        ws,
        """(() => {
            const ta = document.querySelector('textarea[placeholder*="新鲜事"]') || document.querySelector('textarea');
            const ce = document.querySelector('[contenteditable="true"]');
            const send = [...document.querySelectorAll('button')].find(x => (x.textContent || '').replace(/\s+/g,'').trim() === '发送');
            return JSON.stringify({
                textareaValue: ta ? ta.value || '' : '',
                editableText: ce ? (ce.innerText || ce.textContent || '') : '',
                sendDisabled: send ? !!send.disabled : null,
                sendText: send ? (send.textContent || '').trim() : '',
            });
        })()""",
    )
    try:
        return json.loads(raw) if isinstance(raw, str) else {}
    except Exception:
        return {}


def main():
    if len(sys.argv) < 2:
        print("Usage: cdp_weibo_ops.py <content>", file=sys.stderr)
        sys.exit(1)

    text = (sys.argv[1] or "").strip()
    p = port()
    tab = find_weibo(p)
    if not tab:
        print("ERROR: No weibo.com tab. Open Weibo in CDP Chrome.", file=sys.stderr)
        sys.exit(1)

    activate(p, tab["id"])
    time.sleep(0.3)
    ws = connect_ws(tab["webSocketDebuggerUrl"], p)
    try:
        cdp_send(ws, "Page.enable", {})

        # Ensure home (composer on feed)
        url = tab.get("url", "")
        if "weibo.com" in url and "/compose" not in url and "/status" not in url:
            pass
        else:
            cdp_send(ws, "Page.navigate", {"url": "https://weibo.com/"})
            time.sleep(4)

        focus_js = f"""
        (() => {{
            const sels = [
                'textarea[placeholder*="新鲜事"]',
                'textarea[placeholder*="有什么新鲜事"]',
                'textarea.Form_textarea',
                'textarea',
                '[contenteditable="true"]'
            ];
            for (const s of sels) {{
                const el = document.querySelector(s);
                if (el && el.offsetParent !== null) {{
                    el.focus();
                    el.click();
                    return "OK:" + s;
                }}
            }}
            return "ERROR:NO_COMPOSER";
        }})()
        """
        r = js_eval(ws, focus_js)
        print("Focus:", r)
        if "ERROR" in str(r):
            cdp_send(ws, "Page.navigate", {"url": "https://weibo.com/"})
            time.sleep(4)
            r = js_eval(ws, focus_js)
            print("Focus retry:", r)
            if "ERROR" in str(r):
                sys.exit(1)

        time.sleep(0.2)
        set_result = set_composer_text(ws, text)
        print("Composer:", set_result)
        if "ERROR" in set_result:
            sys.exit(1)

        state = composer_state(ws)
        if len((state.get("textareaValue") or state.get("editableText") or "").strip()) < min(20, len(text)):
            print("ERROR: Composer text did not stick.", file=sys.stderr)
            sys.exit(1)

        for _ in range(15):
            time.sleep(0.3)
            state = composer_state(ws)
            if state.get("sendText") == "发送" and not state.get("sendDisabled"):
                break

        click_js = """
        (() => {
            function visible(el) {
                return el && el.offsetParent !== null && getComputedStyle(el).visibility !== 'hidden';
            }
            function textOf(el) {
                return (el.textContent || '').replace(/\\s+/g, '').trim();
            }
            function hardClick(el) {
                el.scrollIntoView({block: 'center'});
                el.click();
                el.dispatchEvent(new MouseEvent('mousedown', {bubbles: true}));
                el.dispatchEvent(new MouseEvent('mouseup', {bubbles: true}));
                el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
            }
            const exactSend = [...document.querySelectorAll('button')].find(n => visible(n) && textOf(n) === '发送' && !n.disabled);
            if (exactSend) {
                hardClick(exactSend);
                return 'CLICK:SEND_BUTTON';
            }
            const candidates = [];
            for (const sel of ['[node-type="publishBtn"]','a[action-type="publish"]','button','a','div[role="button"]','span']) {
                for (const n of document.querySelectorAll(sel)) {
                    if (!visible(n)) continue;
                    const t = textOf(n);
                    if ((t === '发送' || t === '发布' || t.endsWith('发布')) && n.getAttribute('aria-disabled') !== 'true') {
                        candidates.push(n);
                    }
                }
            }
            const pick = candidates[0];
            if (pick) {
                hardClick(pick);
                return 'CLICK:' + textOf(pick);
            }
            return 'ERROR:NO_PUBLISH_BTN';
        })()
        """
        out = js_eval(ws, click_js)
        print("Ops:", out)
        time.sleep(2)
        if "ERROR" in str(out):
            sys.exit(1)

        verify = composer_state(ws)
        remaining = (verify.get("textareaValue") or verify.get("editableText") or "").strip()
        if remaining:
            print("WARN: Composer still contains text after click; retrying send.")
            out = js_eval(ws, click_js)
            print("Ops retry:", out)
            time.sleep(3)
            verify = composer_state(ws)
            remaining = (verify.get("textareaValue") or verify.get("editableText") or "").strip()
            if remaining:
                print("ERROR: Weibo publish did not clear composer; likely not submitted.", file=sys.stderr)
                sys.exit(1)

        print("Weibo Ops flow completed.")
    finally:
        ws.close()


if __name__ == "__main__":
    main()


path: scripts/post_weibo.sh
content: #!/usr/bin/env bash
# 微博网页版自动发布（排查版：更多日志与备用选择器）
# Usage: bash post_weibo.sh "微博内容"
# 使用前：浏览器登录 weibo.com，Browser Relay 挂上该标签页。

set -e
CONTENT="$1"
WEIBO_BASE="${WEIBO_BASE:-https://weibo.com}"
CLI="${OPENCLAW_CLI:-openclaw browser}"
SNAP_DEBUG="${SNAP_DEBUG:-/tmp/weibo_snap.txt}"

if [ -z "$CONTENT" ]; then
  echo "用法: $0 \"微博内容\""
  exit 1
fi
[ ${#CONTENT} -gt 2000 ] && CONTENT="${CONTENT:0:1997}..."

echo "============================================"
echo "  微博自动发布"
echo "============================================"
echo "内容长度: ${#CONTENT} 字"
echo ""

echo "=== Step 1: 打开微博 ==="
$CLI open "${WEIBO_BASE}" 2>/dev/null
sleep 6

echo "=== Step 2: 获取页面快照 ==="
SNAP=$($CLI snapshot 2>/dev/null)
echo "$SNAP" > "$SNAP_DEBUG"
echo "  快照已保存到 $SNAP_DEBUG（若失败可查看）"

# 点击「发微博」/「写微博」打开发布框（优先匹配发布入口，排除搜索）
SEND_REF=$(echo "$SNAP" | grep -E '发微博|写微博' | grep -v '搜索' | grep -oE 'ref=e[0-9]+' | head -1 | sed 's/ref=//')
if [ -z "$SEND_REF" ]; then
  SEND_REF=$(echo "$SNAP" | grep -E 'button.*发微博|button.*写微博' | grep -oE 'ref=e[0-9]+' | head -1 | sed 's/ref=//')
fi
if [ -z "$SEND_REF" ]; then
  SEND_REF=$(echo "$SNAP" | grep -E '发微博|写微博' | grep -oE 'ref=e[0-9]+' | head -1 | sed 's/ref=//')
fi

if [ -n "$SEND_REF" ]; then
  echo "=== Step 3: 点击发微博入口 ref=$SEND_REF ==="
  $CLI click "$SEND_REF" 2>/dev/null
  sleep 4
  SNAP=$($CLI snapshot 2>/dev/null)
  echo "$SNAP" >> "$SNAP_DEBUG"
else
  echo "  未找到「发微博」按钮，尝试直接找输入框（可能已在首页发布框）"
fi

echo "=== Step 4: 查找输入框 ==="
# 优先：说点什么、分享新鲜事（排除搜索框）
INPUT_REF=$(echo "$SNAP" | grep -E '说点什么|分享新鲜事|分享你的' | grep -v '搜索' | grep -oE 'ref=e[0-9]+' | head -1 | sed 's/ref=//')
if [ -z "$INPUT_REF" ]; then
  INPUT_REF=$(echo "$SNAP" | grep -E 'contenteditable|textbox|textarea' | grep -v '搜索' | grep -oE 'ref=e[0-9]+' | head -1 | sed 's/ref=//')
fi
if [ -z "$INPUT_REF" ]; then
  INPUT_REF=$(echo "$SNAP" | grep -i 'placeholder.*输入\|placeholder.*说' | grep -oE 'ref=e[0-9]+' | head -1 | sed 's/ref=//')
fi
if [ -z "$INPUT_REF" ]; then
  # 取任意可编辑区域（可能误点到搜索，但先尝试）
  INPUT_REF=$(echo "$SNAP" | grep -E 'textbox|contenteditable|textarea' | grep -oE 'ref=e[0-9]+' | head -1 | sed 's/ref=//')
fi

if [ -z "$INPUT_REF" ]; then
  echo "ERROR: 未找到发微博输入框。"
  echo "请检查：1) 已登录 weibo.com  2) Browser Relay 已挂上该标签页  3) 页面已完全加载"
  echo "快照片段（含 发微博/输入/textbox）："
  echo "$SNAP" | grep -iE '发微博|写微博|textbox|contenteditable|输入|说点' | head -20
  exit 1
fi

echo "  使用输入框 ref=$INPUT_REF"
echo "=== Step 5: 填入正文 ==="
$CLI type "$INPUT_REF" "$CONTENT" 2>/dev/null
sleep 2

echo "=== Step 6: 查找并点击发布按钮 ==="
SNAP=$($CLI snapshot 2>/dev/null)
POST_REF=$(echo "$SNAP" | grep -E '"发布"|"发微博"|发布' | grep -oE 'ref=e[0-9]+' | head -1 | sed 's/ref=//')
if [ -z "$POST_REF" ]; then
  POST_REF=$(echo "$SNAP" | grep -E 'button.*发布|发布.*button' | grep -oE 'ref=e[0-9]+' | head -1 | sed 's/ref=//')
fi
if [ -z "$POST_REF" ]; then
  POST_REF=$(echo "$SNAP" | grep -E '发送|发微博' | grep -v '写微博' | grep -oE 'ref=e[0-9]+' | head -1 | sed 's/ref=//')
fi

if [ -n "$POST_REF" ]; then
  echo "  点击发布 ref=$POST_REF"
  $CLI click "$POST_REF" 2>/dev/null
  sleep 2
  echo "  已点击发布按钮。"
else
  echo "  未找到发布按钮，尝试快捷键 Ctrl+Enter / Cmd+Enter"
  $CLI press "Control+Enter" 2>/dev/null || $CLI press "Meta+Enter" 2>/dev/null
  sleep 2
fi

echo ""
echo "=== 完成 ==="
echo "请到微博页面确认是否出现「发布成功」或新微博。若未成功，可查看 $SNAP_DEBUG 排查页面结构。"


path: scripts/run_weibo_ops.sh
content: #!/bin/bash
# Weibo CDP 运营封装
CONTENT="$1"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if [ -z "$CONTENT" ]; then
  echo "Usage: run_weibo_ops.sh \"content\""
  exit 1
fi
python3 "$SCRIPT_DIR/cdp_weibo_ops.py" "$CONTENT"


path: scripts/run_weibo_publish.sh
content: #!/bin/bash
# 兼容旧路径：请改用 run_weibo_ops.sh
exec "$(cd "$(dirname "$0")" && pwd)/run_weibo_ops.sh" "$@"


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