# -*- coding: utf-8 -*-
"""
百家号一键存草稿（最终版）
===========================
使用 OpenClaw browser CDP（端口 28800）

关键元素：
  - 标题输入：DIV._editor（contenteditable）
  - 正文编辑：iframe 内的 UE 编辑器 (ueditorInstant0, key=content)
  - 存草稿按钮：button 文字="存草稿"
  - 封面：选择封面按钮

使用前：
  1. 确保已运行 OpenClaw browser（profile=openclaw）
  2. 登录百家号
  3. 导航到编辑页面
  4. 修改下方 TITLE 和 PARAGRAPHS
  5. 运行本脚本
"""
import sys, socket, json, time, urllib.request, base64, os, glob, random
sys.stdout.reconfigure(encoding='utf-8')

HOST = "localhost"
PORT = 28800
PAGE_ID = "0C993BDF32DDC631BFC69F41B0755EF3"

# ════════════════════════════════════════════════════════════
#  ★ 每次发布前修改这里 ★
# ════════════════════════════════════════════════════════════
TITLE = '请在这里填写文章标题'

PARAGRAPHS = [
    ('normal',  '这里是第一段正文内容。'),
    ('normal',  '这里是第二段正文内容。'),
    ('divider', ''),
    ('heading', '小标题示例'),
    ('normal',  '小标题下的内容。'),
]
# ════════════════════════════════════════════════════════════

def rand_sleep(min_sec, max_sec=0):
    if min_sec == max_sec == 0:
        return
    t = random.uniform(min_sec, max_sec)
    time.sleep(t)

def get_latest_image():
    image_dir = r'D:\wxpic'
    exts = ['*.jpg', '*.jpeg', '*.png', '*.gif', '*.bmp', '*.webp']
    all_images = []
    for ext in exts:
        all_images.extend(glob.glob(os.path.join(image_dir, ext)))
    if not all_images:
        raise FileNotFoundError(f"在 {image_dir} 目录下没有找到图片文件")
    return max(all_images, key=os.path.getmtime)

IMAGE_PATH = get_latest_image()
print(f"[INFO] 使用最新图片: {IMAGE_PATH}")

def format_paragraph(style, text):
    if style == 'normal':
        return '<p style="margin:16px 0">　　' + text + '</p>'
    elif style == 'heading':
        return '<p style="font-weight:bold;font-size:18px;margin:20px 0 10px;color:#222">' + text + '</p>'
    elif style == 'divider':
        return '<p style="text-align:center;color:#ccc;margin:12px 0">————————————————————</p>'
    return ''

# ── CDP 连接 ──────────────────────────────────────────────────────────────────
def ws_connect():
    import urllib.parse
    ws_url = "ws://%s:%s/devtools/page/%s" % (HOST, PORT, PAGE_ID)
    parsed = urllib.parse.urlparse(ws_url)
    sock = socket.create_connection((parsed.hostname, parsed.port or 80), timeout=15)
    key = 'dGhlIHNhbXBsZSBub25jZQ=='
    req = ("GET %s HTTP/1.1\r\nHost: %s:%s\r\n"
           "Upgrade: websocket\r\nConnection: Upgrade\r\n"
           "Sec-WebSocket-Key: %s\r\nSec-WebSocket-Version: 13\r\n\r\n"
           % (parsed.path, parsed.hostname, parsed.port, key))
    sock.send(req.encode())
    resp = b''
    while b'\r\n\r\n' not in resp:
        resp += sock.recv(4096)
    return sock

def send_frame(sock, data):
    data = data.encode('utf-8')
    n = len(data)
    mask = b'\x01\x02\x03\x04'
    if n <= 125: hdr = bytes([0x81, n | 0x80]) + mask
    elif n <= 65535: hdr = bytes([0x81, 0xFE]) + n.to_bytes(2, 'big') + mask
    else: hdr = bytes([0x81, 0xFF]) + n.to_bytes(8, 'big') + mask
    sock.send(hdr + bytes(data[i] ^ mask[i % 4] for i in range(n)))

def recv_frame(sock):
    def rx(n):
        buf = b''
        while len(buf) < n: buf += sock.recv(n - len(buf))
        return buf
    h = rx(2)
    n = h[1] & 0x7f
    if n == 126: n = int.from_bytes(rx(2), 'big')
    elif n == 127: n = int.from_bytes(rx(8), 'big')
    return rx(n).decode('utf-8')

_msg_id = 0
def call(sock, expr, timeout=20):
    global _msg_id
    _msg_id += 1
    mid = _msg_id
    send_frame(sock, json.dumps({'id': mid, 'method': 'Runtime.evaluate', 'params': {'expression': expr, 'returnByValue': True}}))
    sock.settimeout(timeout)
    while True:
        try:
            raw = recv_frame(sock)
            obj = json.loads(raw)
            if obj.get('id') == mid:
                res = obj.get('result', {}).get('result', {})
                return res.get('value', res.get('description', ''))
        except:
            return 'CDP_ERROR'

def dispatch_mouse(sock, x, y, click_count=1):
    global _msg_id
    for t in ['mousePressed', 'mouseReleased']:
        _msg_id += 1
        send_frame(sock, json.dumps({'id': _msg_id, 'method': 'Input.dispatchMouseEvent', 'params': {'type': t, 'x': x, 'y': y, 'button': 'left', 'clickCount': click_count}}))
        recv_frame(sock)
        time.sleep(0.05)

def dispatch_key(sock, key, text=''):
    global _msg_id
    params = {'type': 'keyDown', 'key': key}
    if text: params['text'] = text
    _msg_id += 1
    send_frame(sock, json.dumps({'id': _msg_id, 'method': 'Input.dispatchKeyEvent', 'params': params}))
    recv_frame(sock)
    _msg_id += 1
    send_frame(sock, json.dumps({'id': _msg_id, 'method': 'Input.dispatchKeyEvent', 'params': {'type': 'keyUp', 'key': key}}))
    recv_frame(sock)
    time.sleep(0.06)

# ── 主流程 ────────────────────────────────────────────────────────────────────
sock = ws_connect()
print("[OK] WebSocket 已连接")

# ── Step 1: 填写标题（通过键盘输入）─────────────────────────────────────────
print("\n[Step 1] 填写标题...")
# 先清空标题
call(sock, """
(function(){
    var els = document.querySelectorAll('div[class*="_editor"]');
    for(var el of els){
        if(el.getAttribute('contenteditable') === 'true'){
            el.innerText = '';
            return 'cleared: ' + el.className.substring(0,40);
        }
    }
    return 'not_found';
})()
""")
time.sleep(0.3)

# 找标题编辑器坐标
r = call(sock, """
(function(){
    var els = document.querySelectorAll('div[class*="_editor"]');
    for(var el of els){
        if(el.getAttribute('contenteditable') === 'true'){
            var t = (el.innerText||'').trim();
            var rect = el.getBoundingClientRect();
            return JSON.stringify({found: true, x: Math.round(rect.left + rect.width/2), y: Math.round(rect.top + rect.height/2), w: Math.round(rect.width), text: t.substring(0,20)});
        }
    }
    return '{"found":false}';
})()
""")
info = json.loads(r)
if info.get('found'):
    x, y = info['x'], info['y']
    print("  [点击] 标题编辑器 (%d, %d)..." % (x, y))
    dispatch_mouse(sock, x, y)
    time.sleep(0.4)
    # 清空
    dispatch_key(sock, 'Control', 'a')
    time.sleep(0.1)
    dispatch_key(sock, 'Delete')
    time.sleep(0.2)
    # 键盘输入
    print("  [输入] %s" % TITLE)
    for ch in TITLE:
        dispatch_key(sock, ch, ch)
    time.sleep(0.5)
    # 验证
    r = call(sock, """
(function(){
    var els = document.querySelectorAll('div[class*="_editor"]');
    for(var el of els){
        var t = (el.innerText||'').trim();
        if(t && t.length < 100 && !t.includes('请输入')){
            return 'TITLE_OK:' + t.substring(0,40);
        }
    }
    return 'TITLE_EMPTY';
})()
""")
    print("  [验证] %s" % r)
else:
    print("  [错误] 未找到标题编辑器")

# ── Step 2: 插入文首图片 ─────────────────────────────────────────────────────
print("\n[Step 2] 插入文首图片...")
if os.path.exists(IMAGE_PATH):
    with open(IMAGE_PATH, 'rb') as f:
        img_data = f.read()
    img_b64 = base64.b64encode(img_data).decode('utf-8')
    ext = os.path.splitext(IMAGE_PATH)[1].lower()
    img_mime = 'image/jpeg' if ext in ['.jpg', '.jpeg'] else 'image/png'

    r = call(sock, """
(function(){
    var iframes = document.querySelectorAll('iframe');
    for(var iframe of iframes){
        try{
            var doc = iframe.contentDocument || iframe.contentWindow.document;
            var body = doc.querySelector('body.view');
            if(!body) continue;
            body.focus();
            var sel = iframe.contentWindow.getSelection();
            var range = doc.createRange();
            range.setStart(body, 0);
            range.collapse(true);
            sel.removeAllRanges();
            sel.addRange(range);
            var byteString = atob('""" + img_b64 + """');
            var ab = new ArrayBuffer(byteString.length);
            var ia = new Uint8Array(ab);
            for(var i=0; i<byteString.length; i++) ia[i] = byteString.charCodeAt(i);
            var blob = new Blob([ab], {type: '""" + img_mime + """'});
            var file = new File([blob], 'cover.jpg', {type: '""" + img_mime + """'});
            var dt = new DataTransfer();
            dt.items.add(file);
            var evt = new ClipboardEvent('paste', {bubbles:true, cancelable:true, clipboardData:dt});
            Object.defineProperty(evt, 'dataTransfer', {value: dt});
            body.dispatchEvent(evt);
            return 'image_paste_ok';
        }catch(e){ return 'err:' + e.message.substring(0,30); }
    }
    return 'iframe_inaccessible';
})()
""", timeout=30)
    print("  [图片] %s" % r)
    rand_sleep(6, 10)
else:
    print("  [跳过] 图片不存在: %s" % IMAGE_PATH)

# ── Step 3: 逐段插入正文 ─────────────────────────────────────────────────────
print("\n[Step 3] 插入正文（共 %d 段）..." % len(PARAGRAPHS))
ok_count = 0
for i, (style, text) in enumerate(PARAGRAPHS):
    html = format_paragraph(style, text)
    if not html.strip():
        continue
    escaped = html.replace("'", "\\'")
    r = call(sock, """
(function(){
    var iframes = document.querySelectorAll('iframe');
    for(var iframe of iframes){
        try{
            if(iframe.contentWindow.UE_V2 && iframe.contentWindow.UE_V2.instants){
                var keys = Object.keys(iframe.contentWindow.UE_V2.instants);
                for(var k of keys){
                    var inst = iframe.contentWindow.UE_V2.instants[k];
                    if(inst && inst.key && inst.key.includes('content')){
                        try{ inst.execCommand('inserthtml', '""" + escaped + """'); }catch(e){}
                        return 'ok';
                    }
                }
            }
        }catch(e){}
    }
    return 'failed';
})()
""")
    if 'ok' in str(r):
        ok_count += 1
    else:
        print("  [para %d] %s" % (i, r))
    rand_sleep(0.3, 0.6)
print("  [OK] %d/%d 段插入完成" % (ok_count, len(PARAGRAPHS)))

# ── Step 4: 点击"存草稿" ────────────────────────────────────────────────────
print("\n[Step 4] 点击存草稿...")
r = call(sock, """
(function(){
    var btn = null;
    var all = document.querySelectorAll('button, a');
    for(var el of all){
        var t = (el.innerText||'').trim();
        if(t === '存草稿'){
            var s = window.getComputedStyle(el);
            if(s.display !== 'none'){ btn = el; break; }
        }
    }
    if(!btn) return '{"found":false}';
    var rect = btn.getBoundingClientRect();
    return JSON.stringify({found: true, x: Math.round(rect.left + rect.width/2), y: Math.round(rect.top + rect.height/2)});
})()
""")
print("  [按钮] %s" % r)

if '{"found":true}' in r:
    info = json.loads(r)
    x, y = info['x'], info['y']
    print("  [点击] (%d, %d)" % (x, y))
    dispatch_mouse(sock, x, y)
    time.sleep(1)
    print("  [等待] 保存中...")
    time.sleep(5)
    # 验证
    r = call(sock, """
(function(){
    // 找字数统计
    var all = document.querySelectorAll('*');
    for(var el of all){
        var t = (el.innerText||'').trim();
        if(t.includes('字数') && t.includes('已保存')) return 'SAVED:' + t;
    }
    return 'CHECK_MANUALLY';
})()
""")
    print("  [结果] %s" % r)
else:
    print("  [错误] 未找到存草稿按钮")

sock.close()
print("\n✅ 执行完毕，请在浏览器中确认内容后手动发布")
